From 8bf6bb7c242b84092d93593c5501bc2ef0004e46 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Wed, 10 Jul 2024 13:45:10 -0700 Subject: [PATCH] cmd/systray: WIP of a linux systray app This adds a systray app for linux, similar to the apps for macOS and windows. There are already a number of community-developed systray apps, but most of them are either long abandoned, are built for a specific desktop environment, or simply wrap the tailscale CLI. This uses fyne.io/systray (a fork of github.com/getlantern/systray) which uses newer D-Bus specifications to render the tray icon and menu. This results in a pretty broad support for modern Desktop Environments. --- This is a work in progress. Some things are still half-built, this is just where I stopped to take a break. - fast user switching works - connect and disconnecting work, and show correct icon (including the animated loading icon while connecting) - devices menu works, but seems to have issues with really large menus, either crashing or rending off-screen with no scroll option. - clipboard and notification integration works to copy device IPs - exit node menu is built, but currently non-functional (this is where I stopped). Exit nodes are not re-rendered when switching profiles. The code is currently "okay", but certainly needs cleanup, docs, etc. Signed-off-by: Will Norris --- cmd/systray/icons/connected.png | Bin 0 -> 645 bytes cmd/systray/icons/connecting-1.png | Bin 0 -> 1314 bytes cmd/systray/icons/connecting-10.png | Bin 0 -> 1058 bytes cmd/systray/icons/connecting-11.png | Bin 0 -> 1138 bytes cmd/systray/icons/connecting-12.png | Bin 0 -> 1219 bytes cmd/systray/icons/connecting-13.png | Bin 0 -> 1198 bytes cmd/systray/icons/connecting-14.png | Bin 0 -> 1269 bytes cmd/systray/icons/connecting-15.png | Bin 0 -> 1225 bytes cmd/systray/icons/connecting-16.png | Bin 0 -> 1310 bytes cmd/systray/icons/connecting-2.png | Bin 0 -> 1251 bytes cmd/systray/icons/connecting-3.png | Bin 0 -> 1216 bytes cmd/systray/icons/connecting-4.png | Bin 0 -> 1155 bytes cmd/systray/icons/connecting-5.png | Bin 0 -> 1060 bytes cmd/systray/icons/connecting-6.png | Bin 0 -> 1082 bytes cmd/systray/icons/connecting-7.png | Bin 0 -> 959 bytes cmd/systray/icons/connecting-8.png | Bin 0 -> 1070 bytes cmd/systray/icons/connecting-9.png | Bin 0 -> 1058 bytes cmd/systray/icons/disconnected.png | Bin 0 -> 410 bytes cmd/systray/systray.go | 502 ++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 + 21 files changed, 508 insertions(+) create mode 100644 cmd/systray/icons/connected.png create mode 100644 cmd/systray/icons/connecting-1.png create mode 100644 cmd/systray/icons/connecting-10.png create mode 100644 cmd/systray/icons/connecting-11.png create mode 100644 cmd/systray/icons/connecting-12.png create mode 100644 cmd/systray/icons/connecting-13.png create mode 100644 cmd/systray/icons/connecting-14.png create mode 100644 cmd/systray/icons/connecting-15.png create mode 100644 cmd/systray/icons/connecting-16.png create mode 100644 cmd/systray/icons/connecting-2.png create mode 100644 cmd/systray/icons/connecting-3.png create mode 100644 cmd/systray/icons/connecting-4.png create mode 100644 cmd/systray/icons/connecting-5.png create mode 100644 cmd/systray/icons/connecting-6.png create mode 100644 cmd/systray/icons/connecting-7.png create mode 100644 cmd/systray/icons/connecting-8.png create mode 100644 cmd/systray/icons/connecting-9.png create mode 100644 cmd/systray/icons/disconnected.png create mode 100644 cmd/systray/systray.go diff --git a/cmd/systray/icons/connected.png b/cmd/systray/icons/connected.png new file mode 100644 index 0000000000000000000000000000000000000000..274c7697b79e2826fd5c5c81cca0d34101860762 GIT binary patch literal 645 zcmV;00($+4P)lB{ zSn|#>;R7r*c?u3lj&DMwAGaosoaj>gE?(JXNIs5h9tRsmN0zO~fh5 zX)5xR#5^PZHPS@PNzSVxPe~n#m?`pZiZEHWZdH?_bfSTY^)!?}27@8lZ9JiW1QLsGan3>hsd#N^0G=WX4 zn>RFhVw2D{a{5|4>m*_tvGGugr;6yihP6=y+~X%2JXP?9${|g}h~zvf@{~vuF(x?! zMV>fJ-nh&J52l$W&srEXRKE^jYk0MM4LSNIvT@lr4*&oFAm^`UsBCHLakO1nY%4rq z&qIVq^$b z^LYw(Jww%{a1zL^xucrTQ)SmPR6UY&?YXCZhU${!v_1DQLsb{Mo}n6%9N(KI^)po4 zdb6Z{hRXhN8HLFLjg|TtD%%%y$e)bNt*QMyOHPW!Y;X3dOL;jhD#?iQkIul?NY!;7 f000000I(rnRm7CLiL?S600000NkvXXu0mjfsf`gL literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-1.png b/cmd/systray/icons/connecting-1.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8c2381cb4704f63c277e53fbc71f6cc8cd0e6f GIT binary patch literal 1314 zcmV+-1>O3IP)C@3f>C|DC*4p~GTFa!(&Tm09{zzty0IVVP)KHxC$7O>lYeFgX& zm~zhjWz^{hjsWApBfu)K089ZVoO6F$bReS;B62_Q4DdjJvINXJ=YC7*YzMvo_5~fk1C(jt4d>j?F`WvgLim~RJmDf#SOR98bE^TJeqaW8HlcG47zxb` z5orTwfTt2V)4)rXnbArSJcdXY0bm=jGcx`T=^}v6mLrkz_ed83U=)4)lCTzH(8x=1 zCo*(m+@^GTuohxVnquNUo}m-t?rBJ;2WugkLQa>e<@*1tHS63BYay01E*#@7Wa!j# zeT)7fL+6TxWG%#vj0?xOr!sV6+#H>g7E(_896`H63|-)xP`eV54L}EYGNE%3*yo)4 zBcQVhm;)Y7=yZVn7K3J*5q>&$P@fP&7d2GDp6#6b9XJ5YM|3U$uZD`?byk2e;?w>- z@VeEdWfS!_KEa~X0*1LW>FT1c^jav- zsL^Qy?*fNVbF~Uw295)tI_Lhh{j$7-YCD<;h4O^&fCJ9C#e`1VYlK|t63|H%RYZn? zuL!R$&jGKclCBC?i`|DNOM4U4d0=lSuJbctKWVF;O1d@zv*@gh;VdxPn8ike)>IJy zo&esDj1QA80-p0=Wc<MF4mXc-z1;7h>GVgLpL7RMOSd{q$fhgr)jDu3xRzVa-XG z*G{cE@3g4=)X}xjXYdqiv4ip2T{D9CP~E%YGB5dS?xb=BIl{btZZX!{)S zUsvt(YDPk}8JOhXGgN}M^C{q%H8F#{gsP2Z5?ce5Wi(k@N$70!I-!+l*X!I)=|B7xVhZ3sI=-y_i2o>gl7cAMOb#5@xWTvj-Vd`YoLNt{uHKmi2m|eDztcAFqap4%ZDV^gPIwo$;VX~VB z-V$&vGQNnWPbvK3xgi__zBTBa#(0=4=M2>x@nYOUjZpCU^*XcY4-Mfa>4d5}i!~R~ zISssNb*cCAn!+IU7CYxPg}T=rDvNzRL#H403FBxd5%fP{3OH#kLP0@6K|w)5K|urf Y2etCba2%EDDgXcg07*qoM6N<$f)P++#{d8T literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-10.png b/cmd/systray/icons/connecting-10.png new file mode 100644 index 0000000000000000000000000000000000000000..e0d3efa3f920582863d436f0cc5af26a97f6b2fb GIT binary patch literal 1058 zcmV+-1l{|IP)PM4b%*;*>_0YSGMP*!lgVWI z!lV=zMPYz>;1)39UOoewz`il2NYGhuI^!KW4WN;zv*>hgx|iEdr>WI}1S5)~xC*QR zvw@ORV9OZuJ)$!PtOL`5l4D@o81o~dbIs{Y1xk*b&QD3lGomSS&PyiX7PMXzMNre} zNG1TxIGx}{a&v4DHUYQRHA$xvl@Zw6K{CNJ1uO~Ui^wK;rm?q9h$UcBp)-rUZK7z6 zP>CsOQ*`Eg9P4RLtq~LP5>>6{Dr$7bg|VLI)EY5tB<*$@v__mIXiBy1igc=4Pl?uu zX2Qf$?LkF4RjsE)Ys5abPl(Y5>cV&(Xeo4Bz@9L^i|$IFVAmc+WrXX{YRG1^u?^L^ zpmPCiA>Se`pyp~rI_GXZk{Q0iQ+1cAE%pd@5BDv$!TwQApe~tkiOw>Ax9Yw%u1h9# z>5O*hi~~i7&Yo5$lgVTd=Rd-_E68p2p;G^+ z9;P|8MpTt7rRYetDG8No&Oszpjf!-JGNEclX0fE}hSLc>8LN^mkIIOmD6Rk>fV+{>9bhf=$#4zOChU5%wmG)WV32_quR)A;1_!7E&U=rIghFC_Ik;Ia&60H#rm36V&VN1G7v_`BZ zOuQllrd1_fDLSfJtr4o@e#eDyPo_KsNwrn!lxU6klrXVW``;&2uM#GfYE>bLqP8lX z60H%hfp?0!kH8CId>=X7@f;(6_oofu(!iOPN*`OOeT}b cWd0$)0aAE$PxT?=%>V!Z07*qoM6N<$f{HWX=>Px# literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-11.png b/cmd/systray/icons/connecting-11.png new file mode 100644 index 0000000000000000000000000000000000000000..10fea163b6b3a6bf092db0db09bf694156836303 GIT binary patch literal 1138 zcmV-&1daQNP)bpVa;ejA zU7%1X6bgkxp|CDoPO*qMU=-L541|wA0`tH-=bU8d>2WHIP`(R3;rmZIRi zW&&YBlOht;3_6+#07F41dXvH&Q^ZXmtaU=uX~$#){&CPus2BwHD&u45CRFUgKRPk? z0y_;l!}!N0iPZ>`n4vaDXSB<)uI98FF_11X)mrYNMWuOG`5gU!H-Ohs5h>sbX zTJ1_rI(4n5h1H1pjES|{_ciI%wVoDMBi`ZrgcK{lj50n0Gz~gUU{)D_i|tM?;n$wT zWJKuDrqRtXw!>f4JTRk~ zu!hb!|83Q))_6uUp+l#yO{X7_Hl10kPN7gJ6bgBClTcNOQ0Mk(-mb#-*{;JnbgJ#V zbNe(WW{{UqrIMv4zJXay=%kXRWqd=GN~pHs!Y!i{+MdWGFSrw5d zBJx`ZZmc?eBJ#6{oD`8$;pc>i?9=j$wa7`RMu5M7!;zBLz-i~)n}p67a0~b)Qt~3` zsArbJ_$lJwj7>>bg~r%CK>C}~UwXhm-AYJ|kHG9@MsdhF5~l<1Zq{ zG;l_H3EVmN4{#QLp)u|O=VB%VohqTylCA}Mm{2u<3&2HuXXx-v)Stk0ZSfsCn}Ws# zeD4A93b+Z}$aoiPt$%lD;va|iC7~)53WY+UQ1~DD4!JA1x+M}tn*aa+07*qoM6N<$ Ef)M%>O#lD@ literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-12.png b/cmd/systray/icons/connecting-12.png new file mode 100644 index 0000000000000000000000000000000000000000..429af4982e8d10f9689ccf440f0a2c7665f80f64 GIT binary patch literal 1219 zcmV;!1U&nRP)eZ?HzGZPcoaXleg-= zw{O?E@4W++N~Kb%R4SE!MJOp2kpOH3o&eVC$G3qFa5aP=1v*cv&UlZ`HQ-vI&V=eb zsvj?_PRFYQ1xAR-BfvDUIZ?6*oDCt|&FHKKb^{v|CAWa@LkPcTbk?ZOhD6Ct)%nBH zX&CVosq>Zz=z?~ONK#XEEE51WsZR1F=^T4Vn}Du$+S2L8Wd!kZuuN#!0BkeHC$LRu z*hsu|QfvbrbLea)UN%|0M!3WrwIw=Rha4MfPOlN`^ChlY&sp^7j2mM^&FMAb|3=Yn z=fP{l?E+1!cDW^;w${_dYec7DVypIgOFC_>r;FE!tHeGb$1*TyjL!i}4xJ@n-WdNC z-{K>`P<`nAO^p&V5~vWk%EBxw^~J7JG!ahc_*D zARbf)n6pe6pfkxoTlJ_dJA zP6EesVM>qA2!AI_z1wGpf^DyFJMZ5<-H92LB~*_CUl4A7l;j&>*TnM5RoxpMklto1nf!0b$SM0#7>qVnlY35W@sbz68U)G7VwSd zo8i6Jc#FIV06T$qjqwTcCZIJ|LN$pmBiV#A#PJ&Oy0Z@84P&gWq-#q{I$gX*OchK# zWdx>eC0%XlbnzPD{`Cc<)&1x zdCGcoXAmU`)fV7O;MqjU72rS!;g^ig1n?Es>I!!`l zC0#dI!Gvmrm`=S(>%Jh%V= literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-13.png b/cmd/systray/icons/connecting-13.png new file mode 100644 index 0000000000000000000000000000000000000000..375d706de47c3970553a83a29b96690c051f8fa0 GIT binary patch literal 1198 zcmV;f1X25mP) zI#5wjQBhG*QBm<&2%h2?V*nGE3Q zS+w$iLx>pT2(Ta6nJHNTWdCO8Vp&A zlJc4eqy-&_F=javk0t`Zj>MC_NokIQ%db{1HL zSa)!0wQDW$w6&fttU@e0Ce~_ix5U%ddb+R*aSQPYC9DA*WxNBd8hBQLo67hNYi~yek$AG=*@;q=F_|h7t^d0B@$EP_l17AY53pj%WD;wbo@J9v#X<`jmfrGiYE6Kg?Qgs2eR)sn`gI! zr`3dN+A;C85}3Mpl(DuFvo5ScnE(3%QQ4C&o|e{Qf>nrl2d7rs7LO;PGNocxAx=9c z)@s}0X+5Fx9G_=_!-dkzz@gmPosQ4*z`g>{EN~=*a4*9%1iVFg|1SYEtsS2sg!{lz z;DS>23u1(_1}6D}cM+eE!yIrlCqm+Rh$K{6?ALrc+?0fB82A{m*e@eO&I8{g7Tao# zM-ZQIj7O)#HDo({rHRnTGsL5hz+*gy#jdERsHmv0W6%#g6a2e}>W1S3&o|G)zSd?*56LbVmiBy#-*>&U0N-wQmGz>nnLF0TS_8`WQ!^jHoQ=19ZBxMmm7B;1f zAEzt=z^h1_mBSRau9^3VQF;*XZF+ zgBjo(WxRu}r|S;J1~yL*)f~DQ%anVc?jZ@)jKXscNvQ5CJWWESiE)QbOsE2K9BpK= zJwHxjiZ<*@1-{oG*{*$%gBLRC>wQBhG*QSk)vH$cgI)nrJ~-2eap M07*qoM6N<$f(Sq^G5`Po literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-14.png b/cmd/systray/icons/connecting-14.png new file mode 100644 index 0000000000000000000000000000000000000000..ac797407f9a179fc0855861b73b48477726e0427 GIT binary patch literal 1269 zcmVZBe^X0=w3UyI31F$ZP^p@Te25AQmr5jYCul-6 zN=%FkW8(i}bR&X)fW|#(N|j!Ve3Z3?R(DX^MnOJ~i#hjlX*y@lv~P)v`6YLA=j55U z&%M(*@AJ-uN~Kb%R4SFqst6^;A`*Zdz;<9z-(Cb}fomZIDbRURbq2b0W`LPOogvlf z*S8l`XV$9&1xAQSAFv15nkcymOoR|_Wpvg9dx6b~k{duPgm62fvq5z>B}%TV&cBvU zn;uV*I&YbPUeI0$cJCZN~4$I|J>Wdw0KSSGaD1ne}%hpTGiv^li?LtF3OpwWagEp~Xw8>TGT&X;bV?!*kr5~?SFABn-ra$E!shY-w8 zPDHi=KLIZ%N+wn3uZ+${VmOn!OPwP&RIU%lWrT?I0%JO{#U)^0Dz4LS!Y`yvXfp{M zNPRQ3k$QuCJa7Xz>-lDQ?=_B-Hv!;9;8SD#6Y?ei>?SV36vOy3k}Wtx9Ip{aoplh` z{aWj6E6`~)p&BWec*F=yi*$^!rV_IbUL)N9eE~7nWRXro*WupGn_cUIN~BBwbId&a;V<^QvPeU2VKAb|5A~)|n-UW{jpD{i&G~ z#AeLmA~ESowd(!AZ=_9V(*j=eG-%$Nt|9U!KnzfajqwxYO#pa_m}aH;jJydD55OIF z)VYl)NrbH0_X>2Z+NN|mJnA!4yNoectuE8&R;#m=HA$$9G1svHt5$EMN*?#S=JXm- za`wrpy8vLvUAr?nXMi_7Cud5|P_>BBSBg3EXP@p66RL4TXN;IoEo*}6+y)NoHKjNM zyz4zaw344GY$rZFTFuWCdVvp!N!RoG_7&iJ;AFvBtaZdb;V5w@5#s-Z?|{?w3{|C4 fsZ=VJ$|K}I%}vEYi;i)a00000NkvXXu0mjfv0X?r literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-15.png b/cmd/systray/icons/connecting-15.png new file mode 100644 index 0000000000000000000000000000000000000000..0af96b3def632e862677c939eae797578229d772 GIT binary patch literal 1225 zcmV;)1UCDLP)fGZaQM0t zIIC6;6c{0C2sjU%jFfBwbI!Sk8O}lA0&qN1vJT8U=l;%ch6CqVq+~5{{z-8f9k3Jy z=TjyScIbknC^K+UCIFlWoam1f#(0ys350z;pW<}OWCZ?lNSV;+7%-mLK1SMvM#u4& zPE6y#Q3K~B{<6u+YJ>@9s4d})_PMREF|9@%$(NXFEx)3LGm_ZW*O*o#o;MZU?JTky zv0cDS)plCqw6#AwvKp~kFmS5&x0X0`%&!Cg7RUWog7dfWL<~5!>Mp zY86;Ynb3oCj>n^V6OETrChXz7(8U=6)Wx}PKgNi&i@ z4-+m{&XASsdkQjchCVzKPCokbVq&)Eyfwjn9yxT^vsZSNYY&c zXGYRMn?`(W7_`xs3C=_T=W>GcRRQOtHjVhMVBq-#XS#qhmEc&;sz%>hj?wh2A?Z)R zdc8daUhMU-BWV-+EY*f{?nDoU10Dd;OK^|=aL#4_bF7cy=Jf$Ui6_1yPOmQm`>8p5 zWi{en!N957wm84G#MzP6h;Ir8PSrkdoHEJZK4%PnOg||RtmJQ>^9IhW5`joQNZm1T zrsU@=+LNSH@*yRr+mf>D4f26%A;FoGEl{164{i<^iU1s+jU}3kK6dAx(6A~p}>jtKXd}; zuMFo^;5y!bUb+pu>zw;N#c5=H40piWq z%>!>a=l;#qnIF2w$eRHEG(VTvK8~&X=4co*rU~*UfNy~N&`@VK;d6V0>FWiYH!MZ_GDEeWoG?RGvfOXFlUa=@>3vGow#6x#>AGtvvKp~c zFmS52EzZ{k98+ycGufJ{CpW!28ye+RtkWXXqWVOJ7si`?^CDu}fZ47{upKW>Pa3|3**_87O00000NkvXXu0mjfZ--A6 literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-16.png b/cmd/systray/icons/connecting-16.png new file mode 100644 index 0000000000000000000000000000000000000000..70435aa549a453362fb75d683cda4b8c8215f6e5 GIT binary patch literal 1310 zcmV+(1>yRMP)lFf74nn7Vxq$fkFu`q(u`&yl|-|E(I~T<6=S} zVhqN((m%y$G#U_Go2Xkuum##v1Sy`SB5h1q6zL%rFZ6XW^BqaUyz@@a*RJMCy7}hm z^UZnAch39HGjpJzprD|jprD|jBe)#0h&Z4RSO=^Qmu~}8z;)-G714g&{)&Eawr z_!2nboclLpJ{!V(dc)-wFbPaq=K~o=h{!Tv8}Mj^atj!9&fQ7qJO+FXybz(B2VQs1 z-AL%H0G8eh%O(YVc;blO;qglbY5 zJC&iMaqH4)Bd2`s03JzDXMi6P<#PmhHlZ^H?2I);p^+K|o=WHp120>0lAMI9fsejo zm;pw$?lSZRd+~ij3=RC2U$iR>+lsr=7JCriVqd|x*k4#TlM4z83JMAq5hAi&MBW#XaS@pnkvk%CNkl#nkq0w$ zx<%x15qU{OUJZXkBC;uLB3X5MMdU*fxg6#)D>~_xm zlF(TRJWqU;+QK(xsibQY@E!0(gz{sUkD7EIFoVW=U8o&-qgJyjUkB~M2d`@s&8Slq-`eqG2B#z--(k6gE1$fxNGaIqr$V+kS zO1kQrPa9Sv2Gf)_2Ev-;e&{&q3fi0RHx_|u?p7mAE4Y9;8UgdD>nok?n^4Sf1k1u^1#_{DUwlQPfD7p!Z#6Du7 zSsEc-$<6?;SaK3;LUk9|2b@)CqomJ12|k9qspw3TIQw+ZS*(Vla|U?B8Wt_&4Amk0 z?$MU;|4raD8HrHj=MGiUHJR~qhaP;NZ~$LP1pl3I0ytgHP!$vu6ciK`6f7bB13ntY Ub(WDC%m4rY07*qoM6N<$f*65Nt^fc4 literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-2.png b/cmd/systray/icons/connecting-2.png new file mode 100644 index 0000000000000000000000000000000000000000..954c60cef4948701a6ef9b126e9108d26187d314 GIT binary patch literal 1251 zcmV<91RVQ`P)=fzi;&P}L?d z1-zc{nE>9kjEqJ~;MqmG2mnt5&quC*M!E>#M$7TY^)E;l0bmUG`X#XzV#ugVX+O#E ziD~Qd>0&L!jLO>B>Z$eruQu%SDAqzOXWTfZoy+j)sr6Iz z%?zI#7Lm0O3mG?#X}{0#iD`3uzO#sOrq5B_tPrCE{1lq5RCNGo0ecfZSAqQ^@^`>z z5SRg8N%*vY1C|EOG$Q93x4}JPHffK+HTywPwTnEkp zr$ywU?VIH#R0Fsy;hBJB3D;0PNcc3pPpC_611;Zt#e7D9i-cE~mx05nq^lyQ#Xdl| z2o)V*B9c%wyiclCPbFPX0@J{&37<>ASZysfFig4#;D-6`$n_D@MZjx@BiB!oE&{;Y zz%hgFu_sh@`5evgsWqzGo^j(?Lbc(f%fF_nRp)vvBWF_*D%0LZzxb7K&OJ<8Yau!r zH;yG#IX*G1DRuJvY7uQlLUp$$pP2T1hL1^WEyP{m4})$An2%h)O+4JGxbL+gP7x1x zDkgEwRg#>9Y6ka!A(n8BP_QfX*QSXVquM$QzT19VaU8f7@tMMH`I+`V`)RR1tiP3hH53@GXnfbcvUzHjEl&f zgwLRVp3utJlyp_py$QUGOQ=Gdz)f6dnL5024cHg@5`H9%k&Xx2xJ~I~gJxZ8Sd*pg z#1pC$*iNwQ#dVA!c4Ny(Qb|`AYaw-nWnVvl-n6VPmnnDtTwyx*tk`vYaZqT)fCrhVsJ5VX6 zfHRTn^VpKEMc|%6mz_|>igBNGLiG?hjQgS?CUJ+-gQ1GQN9s)1A~0hOdHPAHhUnd5 z=l)FL818s*wMQ|BYq8H|wCYu2w!Xz)!o3dntj|*@6bgkxp-`xS{{VY$xCf8ywN3y4 N002ovPDHLkV1frrQtSW# literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-3.png b/cmd/systray/icons/connecting-3.png new file mode 100644 index 0000000000000000000000000000000000000000..45337599887b8cd2c1e43b1e8918c740f327db27 GIT binary patch literal 1216 zcmV;x1V8(UP)~DJUTz9vy)q z3Jrw|DDuaoH4tiqL?4+5*lY+0l2Dq&77`ME%i)}|FwR7f-_d24OpR~=+r|({! zJA3Ec@6JM{QmIrbl}cqGq(rQ$0vrY=fU)@XCa?tD5fOFzJOG>n-UW`uuWtdL0COVp zkJD!uI1ii!o&@dzH-S0eQxW;s;{yeSP}K*46To8$$r^A~ME=P5JPv#goJvT(0p1Xi z-!eXjfG>dO5|Vk~EfM+M@@a4tBF=;tNEe}D4Y(pA_YywCz!l*6jL)~gOloGR>Im>P z@NC9s9(dU^Glp`4$YJtD0C*UfGREI0UjzuV<-9Te0r?^ToF@GIvUm$I;nd}{?-lr1 z+O~XpcndL>m$-`9+IYVj*k>o+LTnZs*wWrC@ae0KOZ3kL zKDRs~Zy}Zn4s2;ZEAX+jB|eutqLSrvhOjDFYye-SmMc}=4|ITMGCtn}r$pqhgwFw> z3p|za=>RWz8Z_68h|{q{=o3Ewu;S3I1HTyKx3R;WhSjJ|v4HPNFJoVr9wiA?mvDk9)(MSJvMY>ZSFwxH zygE$Y2cl0W9y7%X(DingyJ@i}32XD9E%qeo7TfzWd1tG>>lV9GsZ=TzKc&lOlL$4- zCv5MbTJY{!ih05`+sP7aFP{qq%kV*9mhC-M3fv&%bWi`PK4iX!|AbCNOWj1Rf%6=gB=9uM=J-r}knUB)r>9 z6``REyeJ}nXLRmo4R5kEik(nR;mZfc<1tgrk}m>;U4Z8tI(H#T5+O_5Rzmeofsadj zzxPlLoOH#X>9Kxe30(WFyoD%vc(Jqt`@{z+*Qye6tz>(VP+8ixd~Ou@{Nxdp1T$VN zZCgGc7x=ieB~jh7L$`*VEZxBlcN$irHpNBYJBQC@oSVr?6Dq41tK<_ZB_vc3={fj esZ=VJ7WfZcnY-#7`=_4(0000!U7Ps6afp?ATHaG6%i;> zUWZkbNC?UB0PqGD7uhDO1Xy}vJFI-0u|f!mi$ztB6U%k#9J_62QQy+_>!0!I?m4Ib zu7*mbQmIrbmCBJ2k2r)NzyvT3jHW;D0BgXChy;hvWb)}e<+BQ`I(%LLz634WE)r%k%tAJ7l9ez<&0z#SQ3$c3qE7OkH9+_$vp76 zh^%Wq4W>e*BY25?5gN9EpG9O>@p+GY5gN9V&w*z~2;mIyEAUpqXCC;_a%K#b1c?dC zMF2PtTu|1hC>MdGnN-%VQ7!_&2f!Bwow*R>MqNqk@tIK8t~h*5T5BOj%Mz3JTuVN0 zIDFo;h^&QpZb-i~o^EH-S_`q`_+U-jKdMjq0sYJ2^QT2*EySARgEj5_mVAD2_ zSqrg3Y>g%QKv!As0zHFI54fkSe}nBvF96>fbhbjI({Ta47=7YEmF?MSzF0)RM0&tN zZWc*C4}g!*i}5?~nTR}AG<%*FQ9}q}h&U%SBeoF#sWqT$9ch!#G|!Ieed37S)rzps z=PYoYIAXsJ1mF&F#J(XSk4*nqrBbO>DlQI_P)%`g`@H99dn3SC#9QR%2I?*_2i&x_ zy?yPxLu{W-8#X6q;7h1V$x@FvQ2kTz83TR--p)vF10RdX-vytSfLZc)saxs$s7Y6Y zC!w+=OMPHoNvKAE-^drC;Wlt7cV>hThJp7f#{-+dFP1aIdap5wEm;~RPK-Ierd$Mw zIpkGkeHvRv@(OyuGvK5Xs>==^En*vU_?W)CwGf@MWPo<0E;@WPZCi=ifa6H0wCmCQ zE8e1y32PyaoGc~oI(#&(Cowa9_v1*YR+Y6mhmWRhD=`~jEyOCZ6_x0rCsa3yfk}x) z;Fhw!jO|EopdXk4o`h>-fS?VXBQqpxc`Q+}5x08=4p)yUD zX^@&h-{{c+X46s1`3#{KfwVT0v^k$)^ddmq#x$G0dlBos##Ayc(LqnBE?~W<3X2kZqrf|^f4n5-KaK9#0l}e>jsZ=T_$p6g5 VbdJ1Tmtg<^002ovPDHLkV1keA8883< literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-5.png b/cmd/systray/icons/connecting-5.png new file mode 100644 index 0000000000000000000000000000000000000000..6e216ddb3d40d8cbf7d7536224cd1a1675dcc7eb GIT binary patch literal 1060 zcmV+<1l#+GP)IfGh6sC*T?I)EM(KVm0IqvVUIDj_F`olElfW`C<0)wa8^)L)l1`4w z2kPTjZldxYVStqNVT;Hol=dM3YVyAuSMucwbck6&6A@x zqAX&%lvEql?KEhO*pJYZYClHkNVR3Hr$B2&E8@aZ?ehp7skW^36ljgu#YAfSsG23|NVWegp=ttqin<FT&z9}RSO zx{Z8e3Sttf4eSDa^ss}FcPez}){)H!CQDAI?yl)$54+_zl7z}&`+JSyN9;NLBer%k zd2m!e@`#;CBoc{)o}+H?9hw2uZc$mUI;y_cv5!C0#K(s#>iPF)uGt z?YQX_j3S|uYXAF$N)>J>YR65dASSA7E9$z)$jI_#u`Dv?Me e5{X3O82JND_Hb=E>rjXQ0000_x&yc3bIWL)jJE5hbD1w?! zM=}B6s?!NxBsaz~VH0rYx+v*%qA~*eI7lXVCV@HOcoo?M&lL922{8vuD0HT=k4+S< z5h^i7ZHmrJkH>l%Q)|R{yhK&2xr!Q{ig2u_F||ev8cDOA2CWgt37S%Et0bMW&Zmvm zh(^M|QtfU@I%S(*(AK<6G#i^da`#qo|B<9oibQ87*u> zbt34T0_(_+NE29dwIQ7ocRrFCzQI# z6S{PUJ9H{Q(V?@g)yZTsnM@{)UJ@#w2z5`N>gmc&pPf4Ns51&Y1s(tkz$x$vcm=#L z#{Aa(v$TXNmMk@~4b1n5PApkE!ZuW~gsKX>0d56KJ^=TOF<&JekID#lC#(<;Ra(H? z&=qxe!U}S-1lEjIL1zrxq27$>ya(tb%KtnR%=8Zy8;!XiJVYvqf5FDkrS%tz@|cH728}z zNlBGRR9guPJlzX3a5jBz^|8R?b&?i}AT6O9 z#ctOL?Ke2aHdNmtI%7^J^d{PJI29^XM{?Skl!-Ys58WU95J{lCC!Ydk9o0sgi5L{ODAOym1@kCU`C5nJ7_v>Qlh$jMcol{vb2L7?syK6 z_deCoC0%Xi7Nu08Cg^b6TTWw6uHc`bJ8XA0e) zJNOnm^)rR;&mEfB$6+_+GlfhhlgVTh($ literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-7.png b/cmd/systray/icons/connecting-7.png new file mode 100644 index 0000000000000000000000000000000000000000..081d2b250750c38469f8776833bae4ca93a4e7ea GIT binary patch literal 959 zcmV;w13>(VP)n`@aapKgQ8<#M@PE|+U1RhHtqt}EaWH~_Zd&(FXZxT~tFHuxMx zpTUC92pAcCj-$`l`13sajI};s2%@g*55O6)pDB3)uBxi~S@8J?oCCX=l1JdWs;XZF zpHIo@4`^pr67qEtb|4($>-j5(p9dJ%m709VM5$PBoQwK<=cI3FpD)ZnSQ zOKXe0L0H36i(L`^s4*~ed!k4<#M@PF1?oB;7Q9Tyq=-DF>LU(H)h~<`Mgz`RvmW9qD>N zd4SDIsH8BalnGT$AXF0eo1iCDQWy`cnNS_@c8hKOOria`Lu#?DpDDCIcbE|#hkGYf hE|<&Ya=BdZsK3@lb3R=zvXuY;002ovPDHLkV1h*QycYlf literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/connecting-8.png b/cmd/systray/icons/connecting-8.png new file mode 100644 index 0000000000000000000000000000000000000000..90c22c849e25e2f41475f8f6762c41f9e8208a7c GIT binary patch literal 1070 zcmV+}1kwA6P)lz zGMT1tbT+sxjt!#OEBa1Y8J6_JB2G%#VoA6z~?f5swAw-(CLCDOw)^#Q574g(K3_BtJXihveB2Q5iHEf^YTWo{8D|JVg_jmRV-O* zVH>J%5uZ`#GZ~N^U>mAfLUkTkCcH}BcjqJecvPLGyAz}$fL&rQyQ>uPsUa5uHx~05 zK`sLBTo>KBN71e|X3-@}m6m86}j$frbGhbHMqmAjf`w>rnMP>7~V< z#m>$Bw%C3XxLb>@-30Dz)q8HSGnq^#lhM=b29KXU;p82v^@I(c5l%LE{DRI;pX&+J zaL6ZVqX^Oxs!?nh5_-qs5Zh24MSNn(Qrr2&lCCl56IvOok}gkbLM6TEQO71rBZ5zj zc(ODOEGZKzDIV}iSEUJ+Hd(48Csgz3@_`9=%n%FcGLl%*RiZ6KN+Kj_hb`$UIqi3- zs!F<2d{kO(AyO7El6Kg9N(PZoN!tHDp;Cn#3hl7@l%zy;y9(U_aPM4b%*;*>_0YSGMP*!lgVWI z!lV=zMPYz>;1)39UOoewz`il2NYGhuI^!KW4WN;zv*>hgx|iEdr>WI}1S5)~xC*QR zvw@ORV9OZuJ)$!PtOL`5l4D@o81o~dbIs{Y1xk*b&QD3lGomSS&PyiX7PMXzMNre} zNG1TxIGx}{a&v4DHUYQRHA$xvl@Zw6K{CNJ1uO~Ui^wK;rm?q9h$UcBp)-rUZK7z6 zP>CsOQ*`Eg9P4RLtq~LP5>>6{Dr$7bg|VLI)EY5tB<*$@v__mIXiBy1igc=4Pl?uu zX2Qf$?LkF4RjsE)Ys5abPl(Y5>cV&(Xeo4Bz@9L^i|$IFVAmc+WrXX{YRG1^u?^L^ zpmPCiA>Se`pyp~rI_GXZk{Q0iQ+1cAE%pd@5BDv$!TwQApe~tkiOw>Ax9Yw%u1h9# z>5O*hi~~i7&Yo5$lgVTd=Rd-_E68p2p;G^+ z9;P|8MpTt7rRYetDG8No&Oszpjf!-JGNEclX0fE}hSLc>8LN^mkIIOmD6Rk>fV+{>9bhf=$#4zOChU5%wmG)WV32_quR)A;1_!7E&U=rIghFC_Ik;Ia&60H#rm36V&VN1G7v_`BZ zOuQllrd1_fDLSfJtr4o@e#eDyPo_KsNwrn!lxU6klrXVW``;&2uM#GfYE>bLqP8lX z60H%hfp?0!kH8CId>=X7@f;(6_oofu(!iOPN*`OOeT}b cWd0$)0aAE$PxT?=%>V!Z07*qoM6N<$f{HWX=>Px# literal 0 HcmV?d00001 diff --git a/cmd/systray/icons/disconnected.png b/cmd/systray/icons/disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..2705dbd10bac6dff5df4d07735f4370aa962592d GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|1|%PcFuVX#oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBIekwT$B+ufw>J#44m$|6J`D8eTH>+T%(sa9 z{{9cTlhoFBFA3;knG*RsbK}8nX@+e5KkOqyBKC+jPn?zxw78+WMAy9k@s9`7!{hgc zeSG_R@9SOX*I2q%2<&?~IX-Nkod2CqkF($M8>F0?c_;GDPG8^sYyU95*9|`xmLVYb zE$O-W-TQxZz0d7?VXq{jR=a49mQ_c3_iN#&A-eB7T)X*_k_+QIi&kaxJW>wy6P0!0 z?5^o=pV@q7?bDEjjuki7$6qs^&A&_C_nhN0(@W-iYW6KN|L(S0Cj0-UXABGi?#`+v zAx>Bc)6^(sDJ=$uj1?&#d8fYhn{Jj4r1hn=7BA#_9)hY1rj|kGE5E*auwJ*sCJtaw OGI+ZBxvX 0 { + menu.SetIcon(b) + } +} + +func updateDevicesMenu() { + var ownerSet set.Set[tailcfg.UserProfile] + ownerSet.Make() + + tagOwner := tailcfg.UserProfile{ + ID: -1, + DisplayName: "Tagged Devices", + LoginName: "tagged-devices", + } + ownedDevices := make(map[tailcfg.UserID][]*ipnstate.PeerStatus) + + for _, peer := range status.Peer { + if peer.ShareeNode { + continue + } + if peer.Tags != nil && peer.Tags.Len() > 0 { + ownerSet.Add(tagOwner) + ownedDevices[tagOwner.ID] = append(ownedDevices[tagOwner.ID], peer) + continue + } + if peer.UserID != status.Self.UserID { + ownerSet.Add(status.User[peer.UserID]) + } + ownedDevices[peer.UserID] = append(ownedDevices[peer.UserID], peer) + } + + owners := ownerSet.Slice() + sort.SliceStable(owners, func(i, j int) bool { + return strings.ToLower(owners[i].DisplayName) < strings.ToLower(owners[j].DisplayName) + }) + + myDevices := tailcfg.UserProfile{ + ID: status.Self.UserID, + DisplayName: "My Devices", + LoginName: status.User[status.Self.UserID].LoginName, + } + owners = append([]tailcfg.UserProfile{myDevices}, owners...) + + if menu.devicesDone != nil { + close(menu.devicesDone) + } + for _, sm := range menu.devicesSub { + sm.Remove() + } + menu.devicesSub = nil + menu.devicesDone = make(chan struct{}) + + var i int + for _, u := range owners { + i++ + if i > 50 { + // FIXME: systray crashes on even moderately large menus + more := menu.devices.AddSubMenuItem("too many items to show", "") + more.Disable() + menu.devicesSub = append(menu.devicesSub, more) + break + } + if i == 2 { + menu.devices.AddSeparator() + } + ownerMenu := menu.devices.AddSubMenuItem(u.DisplayName, "") + loginMenu := ownerMenu.AddSubMenuItem(u.LoginName, "") + loginMenu.Disable() + menu.devicesSub = append(menu.devicesSub, ownerMenu) + menu.devicesSub = append(menu.devicesSub, loginMenu) + ownerMenu.AddSeparator() + for _, device := range ownedDevices[u.ID] { + name := strings.Split(device.DNSName, ".")[0] + if name != device.HostName { + name += " (" + device.HostName + ")" + } + sm := ownerMenu.AddSubMenuItem(name, "") + menu.devicesSub = append(menu.devicesSub, sm) + // TODO: add click handler + go func() { + for { + select { + case <-menu.devicesDone: + return + case <-sm.ClickedCh: + copyTailscaleIP(device) + } + } + }() + } + } +} + +func loadingIcon() { + loadingDone = make(chan struct{}) + var icons [][]byte + for i := 1; i <= 16; i++ { + b, err := fs.ReadFile(iconFS, fmt.Sprintf("icons/connecting-%d.png", i)) + if err == nil { + icons = append(icons, b) + } + } + + t := time.NewTicker(300 * time.Millisecond) + var i int + for { + select { + case <-loadingDone: + return + case <-t.C: + systray.SetIcon(icons[i]) + i++ + if i >= len(icons) { + i = 0 + } + } + } +} + +func copyTailscaleIP(device *ipnstate.PeerStatus) { + if device == nil || len(device.TailscaleIPs) == 0 { + return + } + name := strings.Split(device.DNSName, ".")[0] + ip := device.TailscaleIPs[0].String() + err := clipboard.WriteAll(ip) + if err != nil { + log.Printf("clipboard error: %v", err) + } + + sendNotification(fmt.Sprintf("Copied Address for %v", name), ip) +} + +func sendNotification(title, content string) { + conn, err := dbus.SessionBus() + if err != nil { + log.Printf("dbus: %v", err) + return + } + timeout := 3 * time.Second + obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + call := obj.Call("org.freedesktop.Notifications.Notify", 0, "Tailscale", uint32(0), + appIcon.Name(), title, content, []string{}, map[string]dbus.Variant{}, int32(timeout.Milliseconds())) + if call.Err != nil { + log.Printf("dbus: %v", call.Err) + } +} + +func updateExitNodeMenu() { + msg := menu.exitNodes.AddSubMenuItem("Nothing in this menu currently works", "") + msg.Disable() + menu.exitNodes.AddSeparator() + + menu.noExitNode = menu.exitNodes.AddSubMenuItemCheckbox("None", "", true) + menu.exitNodes.AddSeparator() + tailnetNodes := menu.exitNodes.AddSubMenuItem("Tailnet Exit Nodes", "") + tailnetNodes.Disable() + for _, ps := range status.Peer { + if !ps.ExitNodeOption || ps.Location != nil { + continue + } + name := strings.Split(ps.DNSName, ".")[0] + if !ps.Online { + name += " (offline)" + } + sm := menu.exitNodes.AddSubMenuItemCheckbox(name, "", false) + menu.tailnetExitNodes = append(menu.tailnetExitNodes, sm) + if !ps.Online { + sm.Disable() + } + } + menu.exitNodes.AddSeparator() + locationNodes := menu.exitNodes.AddSubMenuItem("Location-based Exit Nodes", "") + locationNodes.Disable() + mullvadNodes := menu.exitNodes.AddSubMenuItem("Mullvad VPN", "") + menu.exitNodes.AddSeparator() + menu.runExitNode = menu.exitNodes.AddSubMenuItemCheckbox("Run Exit Node", "", false) + + for _, country := range mullvadExitNodes() { + cm := mullvadNodes.AddSubMenuItem(country.name, "") + for _, city := range country.cities { + cm.AddSubMenuItem(city.name, "") + } + } +} + +type country struct { + name string + cities map[string]*city +} + +type city struct { + name string + peers []*ipnstate.PeerStatus +} + +func mullvadExitNodes() (nodes map[string]*country) { + for _, ps := range status.Peer { + if !ps.ExitNodeOption || ps.Location == nil { + continue + } + if nodes == nil { + nodes = make(map[string]*country) + } + loc := ps.Location + if _, ok := nodes[loc.CountryCode]; !ok { + nodes[loc.CountryCode] = &country{ + name: loc.Country, + cities: make(map[string]*city), + } + } + if _, ok := nodes[loc.CountryCode].cities[loc.CityCode]; !ok { + nodes[loc.CountryCode].cities[loc.CityCode] = &city{ + name: loc.City, + } + } + c := nodes[loc.CountryCode].cities[loc.CityCode] + c.peers = append(c.peers, ps) + } + return nodes +} + +func onExit() { + log.Printf("exiting") + os.Remove(appIcon.Name()) +} diff --git a/go.mod b/go.mod index 0d5a69d6c..8d0b11ee0 100644 --- a/go.mod +++ b/go.mod @@ -120,7 +120,9 @@ require ( ) require ( + fyne.io/systray v1.11.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect diff --git a/go.sum b/go.sum index 863b9db02..acf96a0c6 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= fybrik.io/crdoc v0.6.3 h1:jNNAVINu8up5vrLa0jrV7z7HSlyHF/6lNOrAtrXwYlI= fybrik.io/crdoc v0.6.3/go.mod h1:kvZRt7VAzOyrmDpIqREtcKAVFSJYEBoAyniYebsJGtQ= +fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= +fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU= github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -112,6 +114,8 @@ github.com/ashanbrown/forbidigo v1.5.1 h1:WXhzLjOlnuDYPYQo/eFlcFMi8X/kLfvWLYu6CS github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=