From 615aeda3b9860b83c71be7fd57b0a17ca8e06576 Mon Sep 17 00:00:00 2001 From: Tim Bendt Date: Fri, 2 May 2025 01:16:13 -0400 Subject: [PATCH] excellent --- .../__pycache__/archive.cpython-311.pyc | Bin 1400 -> 2548 bytes .../__pycache__/delete.cpython-311.pyc | Bin 1586 -> 2166 bytes .../__pycache__/newest.cpython-311.pyc | Bin 1690 -> 1519 bytes .../actions/__pycache__/next.cpython-311.pyc | Bin 2815 -> 2608 bytes .../__pycache__/oldest.cpython-311.pyc | Bin 1669 -> 1850 bytes .../actions/__pycache__/open.cpython-311.pyc | Bin 1274 -> 1278 bytes .../__pycache__/previous.cpython-311.pyc | Bin 1996 -> 1856 bytes .../__pycache__/show_message.cpython-311.pyc | Bin 866 -> 974 bytes maildir_gtd/actions/archive.py | 41 ++- maildir_gtd/actions/delete.py | 33 +-- maildir_gtd/actions/newest.py | 17 +- maildir_gtd/actions/next.py | 16 +- maildir_gtd/actions/oldest.py | 15 +- maildir_gtd/actions/open.py | 4 +- maildir_gtd/actions/previous.py | 13 +- maildir_gtd/actions/show_message.py | 2 +- maildir_gtd/app.py | 255 ++++++++++++------ maildir_gtd/email_viewer.tcss | 22 +- maildir_gtd/widgets/EnvelopeHeader.py | 56 +++- .../EnvelopeHeader.cpython-311.pyc | Bin 1626 -> 2533 bytes 20 files changed, 313 insertions(+), 161 deletions(-) diff --git a/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc b/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc index 56252b1bb479f2a0df70a5101d28264db64826a4..0e567e35cb4548cc25a9193f43d96da285c5384c 100644 GIT binary patch literal 2548 zcmb6aZD8DP&wAy=VjyG?)B}S zm!y{xP)aHVtrTLbq50EP(H~MMw58BrmHuhjlLe23K%jr>zZ3l-{_5=QZSF2cYG>!o zn>TOX%)FWR-hLkp`VhchM>kJc9)$k32QIKR=E+T97LbTUN=D;OPmNPBre!+Ij5Cxi zV`NX39cQb&N9MBLaW92vgsDm1H7K$$W|v`j#}%Orb(dSFK?2Qz>MBO`-sP7mkVA7o z^ozbrp61o#Eh06B;(LN60M^FWkZnSa(Ez)|mn9l6v znzwoZ{%ig&vkbA{S&RQ75NgCID1iPPQW<5o;)DT&H zagkYS*jZiPZ1DyX+4V-?(eet~w33!oYfGA7LB}aAe>O+dG;A}aO<-BJe07QC zIWcr%!1AZnY&Ne*Y0$U)+Jst2X}X~2HH#B*S{1S7t37Ep-|}%Dlj)SIU}8V@7L!wR z7V8A*7t*?%Fo?N2DpKk&_=3KwDf%54#|Ek4h#@S!gPMtkB%=H6km|ASKBu-P|UY9B7~ zBPKs$@FOdoQL}SjIWTxDZT2290)v1nk=M*fj~Usu##FqXF5hDWT@GD;_uB*C9QgX+&4Whg?pxXFH>KoN_>yW_ZWOn*&jCj+m<#x@b57EJIdkqg~YYQcYG-vH^cFV z;lzV*;ubE2`^|8_5!e9ByWhEY==RY3;VZ{3A6tB@6xeCn7%vAxSB_skUJ7h81KSK2 zR~@Obb?7JVx8N_qpF_WfU;y~XzEWV!42&5rK3nl`t{{r-tCMnVi@~MWuLrLOmkK35 zY4S;9BYXx>VPHA*+IA@GrGdyGhkoyR8|Zz`H`veIZ|fRNGWU}#@Nt^h@7QSEN7|t$ zVIY=QcYgZB{()Jc+NjsPZ}tzjE-gr62J4!0^!;ZAO-dhDm2*<2da!PzC!mc;50K~l zds$V?%lLhgfT8^mXg>gfa4D*cTIZ_Y8ncQzjb<#PT}CsOQMb{It0*$(HBqGO3zu8B z&MT$X?PlwCBN(l)3`IXiK%Uy9QY8UIb90-ovIf;rqB=~fqe8b+ka?FpW)QvQMKRe8 GVgCzqAX3u+ literal 1400 zcmZ`(&1)M+6rcUl$F5}8j*IKYIO{Z*EkrWpVnS$%8@14aO{(+|U&KZ;Nb9b&VrJHI zgk4H0wB(XYX$&EtlbhoH0XejX`qWcc6&=KY!H`pL3hp7~)Hl+uW0&~x_V;Gq&il=q zH*fcseBMML&wsv9f370*LQWEa`ACinU>+fiu;QbJvaTp{torJ@3Rd&AhQ6+YFNKZo z^>qWQW#pv)k)e(vQf2OPueu6D%dK)RXjYnJi;EN`JfzL4GTLGcSuh$r$D81Ngtk$4 z?EfEPWgBgr#65;oo@je8-#_swD2ur9CTv5pjs$C1#|BPuxmPTw+0OFR44lU4-!gj; zzkkB9qiic1C+s-;^{7yEisutX1i7DOg7W0c=by*~#&i<>7(G$3wFj*G$rYe+{0E4? zldMAM3Zhqb)N`wp+g9JsbQf~jQNK}TM&nUiX`?OeE@~^zMCbBNTCIDV#O8HkS3^ok zliQ@>dcNHtjJY*Z=uD5-c+FbkvpZU$P!O5A*KmFJzAMu4Vxc#JO(HTeE4$;UVxsD{ z;Py!+2ze{yLg&yDR35f7SE4kawmZt<6#sQw7(~A6q(>{R+pA7iSganjD-2pJWMVo_ zwi54L@vtyic&|l+DkLs68a9Q-I2C5xd;um@hh0UAaTrpOEp1gvOLnQF3C(S_WS_Fo z=RzS&Hh^v0ufHgM#Rz3Z?lta_Cg#OkWRv(oi_l_?Mu!>NluEe!IryK~)7pML&__Ux1XjW2u4<^FOx%H0`Uyfm14cbL|v&6fz^UkN{^ zh9=5Q_pSM^^0&1ZS&N6c*FgRuPE7!DxYX*!RmCE|8Iw>(J+a`4VLfgYN!Woq}MCBJE-aA$HLR|KO{PB$L zdsO9L(J`zOU5?J`08K@S9-*b^tPa%)1!{2) IP?1}?wxUx zIu&ZGHqA>=Ujn63tx8Qn;ej7fcs@rbVV#5&sW0Wth&~{mI&1svm=Lv{o12-P-TBzr z*}LDHngR&m$p;WCA5s8 zUZswQQ8U9*iJDii^TPWLyiZ?)JU>DhGR&boD0TwDIW$wVy#@KNiH%ue38K_JJh}h_9O)x#~96~d)E6>!l|2BVDT8I2M+*-qy2>Mbyk5c(I~9(tc#VUeBfvvA#ulx3eQkPQHcKgq@x)K-P=$S2Filk+L|8=EMsq zizC6Yg|=a;QBzG#g_G1U;!`xdAPx)+EObq)$+#AukJt2sFQnm!;CKv^=oz!jDPxYL zrmpCWANyl|mlnE5K0*h$Qorkdsi>KaEnd;08% zqa^fXGLuphu-@?yn$QV#0@cq5m;GO4D4UPzDa!cmcEq%vc4W02P>Cl@RZqoIbk21A zBXbFw=HrOR5##AJ>*njd)eIQFO^ja*2UCMz`*0bae;EDLpv;JxYVtBo5i>eWuTo7< zQx=^vNxw;1QccCRepA;qBbtO-5|zbZDq4wdL@QMtNY6XX6){$=4+}#W{;e?{z)AcC z&o#6qdA}e3dfe(bbz8I#odjONf;4PP!>gu(-ju(SNdBZ+< z!U_%nK5Bo(Za-kRzgZJXUa33q1OfS*lM<5JcPC5UQUHYyKI}cZ*?Y9mJ81U~THP<= z56w%S>z*y2veL5La^sy}PW*J@=FrbWR@dR%R|>vkw(pqbJ62Rew$hVt+f?>h%D!S~ z&uagV{lEDNp@{HpfUY z*s?OdJYER)*ufsl#Vw_IWoUWGdiC->!#;inILC?49!^}`oVZw+xMWXUsw&BBLD6hQ zvs`@kNa-jcEFYEG}43qIP^Bq z`#phCMZABod(!X1jhSoEaG9Uz#Xp%{h5htjQ>2Gmg;CAtCA^d8s)Ywcgh{~ z6c!-X3*@=)y`)Yu8a>Glz>r^LV;TsgfpHNvUn@UF)MYhd5%pP(xP|3w9oJp!q6dk_PLKu^s{#XSU{`bHm?Me5=0dv9jm_j|KH zrBVq5EBxW&>L&_9e~C?R$zA5y0OlFO2un7~3o6@ESSg8n{W))e$; zG268;WWW=i|FA;cBIL#u>N{NV80CrXlW;MiVi5Vzt@R?Qh&Pi_xMEc*;?;P*&A3E7 zVNuuhlY8bD9-*GeO67;d!OYB&uZite2sKw2o@IoVOO9pFGS{^|Q`8ifXaTOw&U5t4 zPPRs6gQvPLv~xlyz=fE6?=rlsN9d&*dvgEr{a`Hnvl6~{184^tYA#fBftqVx`{+^j z$-?7>@5~=RXsB010rM?68JRm8nfU&4Gj*vxzIW%h;)|JZ{!YWV8ya_m_QDH4TmagI&zBooKGgC-4_+T>V=W{_=K81^*LKp|>3V8!x}E}D zZN$wmZU+AcuVJ+mPzUNwL7jd_pH630^vBE;;6pW$omUQr$FsA_;cNu>j7%dSwU~+0 zQJ_36hR?Tbqgui%gn6AIdHYO@jBbkz@yl$_M7t-ypkvTS^c_Ga@sGaZ2R kKyL{U1SUyMG!pcniQWwcp^4^#L1;xUNKlM(5EVK82OFx36#xJL diff --git a/maildir_gtd/actions/__pycache__/newest.cpython-311.pyc b/maildir_gtd/actions/__pycache__/newest.cpython-311.pyc index a186f51ae485c8ef6766dfdfc12e5d88e2a2079d..8777b5fd4c625e90f9a5ba799dfe2dd6db3633af 100644 GIT binary patch delta 851 zcmZ`%&1(}u6o0d``E(QMqSV9>0=2XYwvdZZKMDpnSBZ4S>Tg(>Ax(InzR9%(qUW+aLOQc>4_vr{isT+utFE0 zO0=k}O84CNk`?vB%v@$=tHRtgZQc+av1ieJ#nFULB8&6w^yb9#V#|4Hc9S!`LguNI?{=A`^j2X=E#_|QHWSRPX<`jy0xnw(D(6+dP zsaU>T)`xm**OD(bs7LdaibqR^?Ga{+=A}|8E_7nK&t$SHvmJgCG0`3SJ33!~UseKh zJIc|Xa=H(ZNS5rV$3Ll4AJwU@s`XT@qZ~57s3-ipy({#_=Wk)$O=Nrc5_cCns@_%g zo~pY`fsMx6mFY|fKFp4zZ-=6p^Rzvd%%o{M9l%(VxKMjjle|#geqxASQlq`y9GHC4_v2cyloNa-7Wd;cut>0S9!y0Zg#J3IG5A delta 999 zcmZuwO=uHA6rS0i{Ol&R*{V%{P(v*mq)HKl8tb7ItSCKLvGgEin;C3knhmp?rpm@v z@z9e{A}Fn;K_nvS!Gj0S)r+@8atI3*kKT;bgLv{y!bXd}nSJkjZ{FWLcD||W!H!qK zpn`xtJl5u8;#$X)E`qjK!U#P@eq@1kDcQ!i5y3Ao5s0uYmcYNvW15u5HckqzMx;AU z%;tzh)^OJ{0zcXH7xcZ4VpGdR@gvMW2gXvap5aL1d$3+h>0d7!i32&WnED?O8;8#J{cVuh{Y#K-R%O zxsM|@n3vtS7$T}IZ=g-#f6+5gBQM{Vd8HQ0;T&2LXHX7D1BK7{)@0P{_~sJ}dP={m zJ3g9Tq$wjq9VwMCt$V_d;Ydl-NCPq*X$0s-RB$@tddA8!I%{ODOxAKFi!ND=d- z44HX2dMm74J^;Wx4eTqd6;$b&fc@s==D@T0qV+so?H#K14z;X*^F4?_QS5jbU8KyU zg$Ls3dDT(O>|BN!acY{5$g*iiVHDa(#|=UmpS~lf44s&c$FvBv2GU>_C54}2Y}w<8 zdM4v|7&WsgtL5%@uM7DS5QnMBTR+w$*2yyl^|+NV(z9urqo&0M!6w0HcEG>1f*Mjt z9@!1`JvmYjOjMe?-gmSfI@It=n(`e1`@z|&=UPKS0j;L?7x5=GQc)xIK=dgE;g~cQK|8)~ z&Lhfrr?_*fcU%?Ut1{rI!1#-q1bG-_v+>u6X&m#5{qkK{Fi18CX?ycq=6|A`558xOWeO2n$odXuK z>2p{X{#8AL8msgH|Cx`FfA`&mJAD1v3#+Z^XHpJr=tfhKNLNhFPA-9V5S%Kleq%(h-UJ;=|(e- zV&Fx0JL}rKWeg&s;5G;FqFU-3q9ToalJ~jtkp@J=Qv~CWa?iHhQuTqIIFjk7n?gsXmu~h-nf{F@ zA%&dwf_d}4K3ga?wY&tQ8|5^W{s@iUDY3gn?YwaTG5#l+eH$Obl$=l&nyVr1CnF8` Ik85`N4cdUmz5oCK delta 997 zcmZ`%O=uHA6n?uqyGb@blJswDg4+6n)mEqlp|mP!Ac|;3Jy=0&%q*HT&4$^Ht+G)O z4?TDgR`4H#NJQ#E>BWP1Q1P5d4q>6*M30er@}h4NQw8^E=TyzkBTmU-U!qbu|z z7*ql6m?t}jNq4j;S-~~|fGXF(mf}#x(1J5TdSXRllQ^8* zUX*RwTJ<~AhK!dN2uu?mWSR{nAFsU@Y`KhD7gqENAOMw*!u1i%QF%@jsE2x=`O2to zVMVV1c<$%tPL~+=a!Q*p0#v2`zX?2NJy_arT^oRHEGli@7|?*N+=B<=zu1!~tEgO3 zc$JnnE6l>2Gzqi9K+t(DotPN#xZZQ=nPfJ3A?bSc+<84~{S7UOLmax{{Y*=ls18^j*(MaSHR@CC;esAvhC z&$R~~w~v?T{ZrN6!CLQNEBYrcBLkfM^6BxQtD1!~d1j<^({v?P$hj)h@$y{CpgN0T zo-1dKBsE>dv?vO~)nJxRS-0bGFw>6Ay8!qJ;+>VbWg=B!88*27&54~eKp zz5?Pm4)aPwg+Qd{kClWE{=SO8uO8@`Kfp&_i#=5Ds-+!|cUHCGnl?PYe<`B1{>aNo zT&r55h8RiIA_?b}@_GH$J;A*iya^9-+}(rtR#W31>23e&xG237<+x|@y(c7WWm`~c wQ}a_`o4C(A`tBW{F=!#H?_&FK%U`m&c~SC7!ce#Zzh;AU3!@GA4>XJY1aPzc*Z=?k diff --git a/maildir_gtd/actions/__pycache__/oldest.cpython-311.pyc b/maildir_gtd/actions/__pycache__/oldest.cpython-311.pyc index 319cbc03acb148dd0550eeb33186a5c9b7d5c5c1..9db22c4e5610d8834fd7fcf8204a9246603710b8 100644 GIT binary patch delta 808 zcmZ`%O=uHA6n?WmNp_n}+wD(ktzJwTQbmLyv?v}-@gfvK6nl_u+!<+Nn#k^!MBJEy zKX}zeDu|F%R3hS0dQx*2m4G1^y$BvxDjq!fCNV)FzS;N9oA>k0d&}I`b|TsnO;Z7d z#V6M<+|$k^6g=ES-Tg@bP6L5$P{4&8PwI*_>q3oqZ^%Ry>gZ|w@$)iDu8ZekTf8F? zaO6P%a$?`60u`x5Wg4JC8mb4e9G+(7L~MRdmdHUL%clf+(S&%on?!H4Y%j-+zBI3IQ3{g;m5cxN$Q8{wHLpd&Q= zT&rVajc#+CsF!m9oS7ak#~4`girlX|1~lTzkKnN|ta=sisK_^DepAmXkrI@}RVb0< zs233~k`-@5I4oQIN^eKlC1c(TvAnExG>hen8EO<5ql|XOGp3U*7L3cxG1rZJ)^<9< zjD4+OW{aI^(_$Hi8TRI-t5(ruwr#L7GrdLW0f~6;q`B%j{E6RiZGkW8@=`lI+6wRU zfhY@Yed?2*_^2m7=*gCzd>HM2fmBKjQRr*d=T{i z!`*-%@aE*t)mtYQ(h9si5=Cq(YI;#@PRyoLVlyS9ofItos8(>F!rh7?JOgWt+Yy9c oq2n7xx|wGuta&{0M`0gA@I^w%7l>?imoH6{B_D=C2%TNJ-;*D!WB>pF delta 628 zcmdnR*UHPgoR^o20SM~*gfes|@&?qi0y)e;{CN>01H*I%PKFX74Fg$lb~-}}<08fs zrWB?%%*%l4SA+C`K`mnqV-^#Pm%@?_W)~%-u%xi&FxN8GFfCw#$s*H0T|m1hCozih zGneopWK!5BhcK!evq-{)Y8c|#VVoL<1spIoGF`)v#XdQKQLLVkA%!)V0jP`TqZ1L$umR(9Z<_!#|m-= zgjLR1&QQS|$xzOy$vruVDV9l-eewY&J^2t#rdxc;i3KI4MXB-mr6mQWCAU~gQY%V| zKyLG!{F&)9Bmd;*%oRf1x44Q+lM0IRlT(X}Z!s5@=1pGAGL=zavL&mG5eG1m8W=vX z2(nt<6_lPLcZ0{{0vO$pmcJn=`hlHAi0dN*koW>3I@msNO}@fPzf_gi73M|pzvy#Bm-E61+4iqWBKGdCMi~yq|}_8$5I{kb6JN$2`>UTI_;ZeH5!+)Je<`R$0 zjNmJ>#us@^uJD*#0HYsw`6WLvFmXyCh{^TL3W}^ip&vhfd|+b`5bY=}<_DVJY+%B$ TMbS}``Je=&qvYlT%yx_bbZtV6 diff --git a/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc b/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc index 7b174acb6187efba931ed5a8f7aa72e8e71bb762..a54f63e2dd8447f146d0585861b81183fb2712ad 100644 GIT binary patch delta 946 zcmZ`%Pe>F|82{e9nf){FD!$UvO4D_-J=noZENn_BbcjkYDhnO9or&FCcb1v4jhszE zi4Go$cQ7bnPev#rbP2jdUK4g@pk?S9bX@S(srPo7HZi~X=9};D@B6;@=3CpB*4Fpo zundeoMo*^a@axve$TG7I0s_EwK(Gcuu;DS!sFSg6LkZCzs6V|7Dq6sIU=6jya;=qaoAw$re}hRx5N$BV3WNKDk2+=!}Lf|umvmVd4Z!ybP0D3&(?j$fGyad z{B6Z%@imj1npZ|kNV<$BVRe*c`kV2lrb?=B%YS$n#M+hue85kN~*uzZjGN+L@aZPNK;?+=_6FOIeP;dfONym3G zxK9QvB<7GX2wTbS%?7pMH1c>gX7OuhKA=@l#Ch5_ufq-Lu_e3A~$-m>v@h z{_o$639f9;Kg=6?N;6G^FZ%(E5c?SXntlfobGka8)kcgyy6W%kYe(@K93$#we*l1- B;iUio delta 1035 zcmZ`&O-vI(6n?XRE!%AiMW9fy5)`Qs12Hi{5fkkJH4+0xLLwoxvyqn4ZDzNCW~;<- z;NV5qM4}QKqYwiJ zI477w6efpqqA42wy^UZ((rx)rxIr{w%I)|~$k(x=q7!fSAwYn|F}Qbz24xlu0e0iM zP5IA4h*I(%7PsCZzg#0Zl;eOsSoxd65_f?Sbm{{f1ac@RIlNPVy{7a8R=Iz%C+NX) z(rt-lsiAo^4-5PR%%h0UGPuc+h|6|QCuU=**v**jR?~B8O3SFWkV@#rEv{Fyg`}>f zDbj6WfXbB!XZzx@jFBbkl$JFzS;H0#b-^I4qgIjAL?SVCfnOSA?bMG^^_7lAD6|gK z`MPJdYh^lbJWUtddP{A+HSh0wmomWe2s49TTh_DJGenE4x^D9%o3>@5(%R{`hE>8| zi!G+K7}jk`H!wX9wHh&0OnlZG;nE$K&BrnsTOz8SO&K*popVc?F+rIgXFZj`Hd1vL z&3d3Fq@Gpq$KpmpOHXBpI+xJ0I_aTafj+uQzvD%y2>Zl=orciE{bgmiP@UyaxZKcG zx#kiA@^_%>2UE*ZMFu5M^0epCwx_e;=`1TP%NN-w2iu>?kLCP{7pIHC{!*}i`TR~G zSnJB$1hvK1;S%lN=+#1CycifS1;(wHVyZ>)e^xrSm5!nkE-B%HGp&=7cKPnv(7A5- z5I#!vmbZoJZaLb-Z}qiB>-mrMBE=Dous?2u+7@c7S9=8{z%aY~Pxs)ghO;Skh#Yy~ P3U@Z6feQRbRaw6P20jH? diff --git a/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc b/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc index 845fe339269275003439aaca4273ca94a90cabaf..302da93a583860f551dbaa46b6b393e5af7beb31 100644 GIT binary patch delta 336 zcmaFFc8;BQIWI340}${X6wa7Bk@rc`1R!TRLk(jMLp(?v3{rr+1&lBrGM&P-3@E-D zE*H-PlPzI~u^1S#SYYfF<`VA7@{IbdEWr$#tdoNobp%4Hc$IS#GxOq;^Ycnl^GYVy zGfHT(-4aSJEh++X<8xDsixbmR<12QA#4iq;-29Z%oK(9a$;qdfGWA&)trsYNU;t7pRKOHY0&EBXXs}Do delta 247 zcmX@d{)mltIWI340}w>_3uh!x0+her^#|lD7mz#C^fGn9x4%^ znKGG$sY6@J83USfnOzXVPNip8SZ(LI$K_B}0)QkSgK_62CZX ja`RJ4b5iY!#3!3EXX-OES}#!kzyPFHsDLS)1lSM&8TmL} diff --git a/maildir_gtd/actions/archive.py b/maildir_gtd/actions/archive.py index 23ab991..d84b75c 100644 --- a/maildir_gtd/actions/archive.py +++ b/maildir_gtd/actions/archive.py @@ -1,19 +1,36 @@ -from textual.widgets import Static -import subprocess +import asyncio +import logging -from maildir_gtd.actions.next import action_next -def action_archive(app) -> None: +from textual import work +from textual.logging import TextualHandler +from textual.widgets import ListView + +logging.basicConfig( + level="NOTSET", + handlers=[TextualHandler()], +) + + +@work(exclusive=False) +async def archive_current(app) -> None: """Archive the current email message.""" - app.show_status(f"Archiving message {app.current_message_id}...") try: - result = subprocess.run( - ["himalaya", "message", "move", "Archives", str(app.current_message_id)], - capture_output=True, - text=True + index = app.current_message_index + logging.info("Archiving message ID: " + str(app.current_message_id)) + process = await asyncio.create_subprocess_shell( + f"himalaya message move Archives {app.current_message_id}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE ) - if result.returncode == 0: - action_next(app) # Automatically show the next message + stdout, stderr = await process.communicate() + # app.reload_needed = True + app.show_status(f"{stdout.decode()}", "info") + logging.info(stdout.decode()) + if process.returncode == 0: + await app.query_one(ListView).pop(index) + app.query_one(ListView).index = index + 1 + app.action_next() # Automatically show the next message else: - app.show_status(f"Error archiving message: {result.stderr}", "error") + app.show_status(f"Error archiving message: {stderr.decode()}", "error") except Exception as e: app.show_status(f"Error: {e}", "error") diff --git a/maildir_gtd/actions/delete.py b/maildir_gtd/actions/delete.py index 7991bb4..8eae076 100644 --- a/maildir_gtd/actions/delete.py +++ b/maildir_gtd/actions/delete.py @@ -1,22 +1,25 @@ -import subprocess -from textual.widgets import Static -from maildir_gtd.actions.next import action_next +import asyncio +from textual import work +from textual.widgets import ListView - -def action_delete(app) -> None: - """Delete the current email message.""" +@work(exclusive=False) +async def delete_current(app) -> None: app.show_status(f"Deleting message {app.current_message_id}...") - app.query_one("#main_content", Static).loading = True try: - result = subprocess.run( - ["himalaya", "message", "delete", str(app.current_message_id)], - capture_output=True, - text=True + index = app.current_message_index + process = await asyncio.create_subprocess_shell( + f"himalaya message delete {app.current_message_id}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE ) - if result.returncode == 0: - app.query_one("#main_content").loading = False - action_next(app) # Automatically show the next message + stdout, stderr = await process.communicate() + # app.reload_needed = True + app.show_status(f"{stdout.decode()}", "info") + if process.returncode == 0: + await app.query_one(ListView).pop(index) + app.query_one(ListView).index = index + 1 + app.action_next() # Automatically show the next message else: - app.show_status(f"Failed to delete message {app.current_message_id}.", "error") + app.show_status(f"Failed to delete message {app.current_message_id}. {stderr.decode()}", "error") except Exception as e: app.show_status(f"Error: {e}", "error") diff --git a/maildir_gtd/actions/newest.py b/maildir_gtd/actions/newest.py index d6da131..8d61cde 100644 --- a/maildir_gtd/actions/newest.py +++ b/maildir_gtd/actions/newest.py @@ -1,18 +1,13 @@ -import subprocess +import asyncio -def action_newest(app) -> None: +async def action_newest(app) -> None: """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" try: - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], - capture_output=True, - text=True - ) - if result.returncode == 0: - import json - envelopes = json.loads(result.stdout) - ids = sorted((int(envelope['id']) for envelope in envelopes), reverse=True) + if (app.reload_needed): + await app.action_fetch_list() + + ids = sorted((int(envelope['id']) for envelope in app.all_envelopes), reverse=True) app.current_message_id = ids[0] app.show_message(app.current_message_id) return diff --git a/maildir_gtd/actions/next.py b/maildir_gtd/actions/next.py index 4cef2d8..a6f8c0b 100644 --- a/maildir_gtd/actions/next.py +++ b/maildir_gtd/actions/next.py @@ -9,20 +9,14 @@ from textual.reactive import Reactive from textual.binding import Binding from textual.timer import Timer from textual.containers import ScrollableContainer, Horizontal, Vertical, Grid -import subprocess +import asyncio -def action_next(app) -> None: +async def action_next(app) -> None: """Show the next email message by finding the next higher ID from the list of envelope IDs.""" try: - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], - capture_output=True, - text=True - ) - if result.returncode == 0: - import json - envelopes = json.loads(result.stdout) - ids = sorted(int(envelope['id']) for envelope in envelopes) + if (app.reload_needed): + app.action_fetch_list() + ids = sorted(int(envelope['id']) for envelope in app.all_envelopes) for envelope_id in ids: if envelope_id > int(app.current_message_id): app.show_message(envelope_id) diff --git a/maildir_gtd/actions/oldest.py b/maildir_gtd/actions/oldest.py index 419aef5..467393b 100644 --- a/maildir_gtd/actions/oldest.py +++ b/maildir_gtd/actions/oldest.py @@ -1,18 +1,13 @@ -import subprocess +import asyncio def action_oldest(app) -> None: """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" try: - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], - capture_output=True, - text=True - ) - if result.returncode == 0: - import json - envelopes = json.loads(result.stdout) - ids = sorted((int(envelope['id']) for envelope in envelopes)) + if (app.reload_needed): + app.action_fetch_list() + + ids = sorted((int(envelope['id']) for envelope in app.all_envelopes)) app.current_message_id = ids[0] app.show_message(app.current_message_id) return diff --git a/maildir_gtd/actions/open.py b/maildir_gtd/actions/open.py index ff81268..b9b3632 100644 --- a/maildir_gtd/actions/open.py +++ b/maildir_gtd/actions/open.py @@ -6,9 +6,9 @@ def action_open(app) -> None: def check_id(message_id: str) -> bool: try: int(message_id) - app.current_message_id = message_id - app.show_message(app.current_message_id) + app.show_message(message_id) except ValueError: + app.bell() app.show_status("Invalid message ID. Please enter an integer.", severity="error") return True return False diff --git a/maildir_gtd/actions/previous.py b/maildir_gtd/actions/previous.py index f475d2c..fa70133 100644 --- a/maildir_gtd/actions/previous.py +++ b/maildir_gtd/actions/previous.py @@ -4,15 +4,10 @@ import subprocess def action_previous(app) -> None: """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" try: - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], - capture_output=True, - text=True - ) - if result.returncode == 0: - import json - envelopes = json.loads(result.stdout) - ids = sorted((int(envelope['id']) for envelope in envelopes), reverse=True) + if (app.reload_needed): + app.action_fetch_list() + + ids = sorted((int(envelope['id']) for envelope in app.all_envelopes), reverse=True) for envelope_id in ids: if envelope_id < int(app.current_message_id): app.current_message_id = envelope_id diff --git a/maildir_gtd/actions/show_message.py b/maildir_gtd/actions/show_message.py index 10b7e72..8a2221d 100644 --- a/maildir_gtd/actions/show_message.py +++ b/maildir_gtd/actions/show_message.py @@ -10,5 +10,5 @@ logging.basicConfig( def show_message(app, message_id: int) -> None: """Fetch and display the email message by ID.""" - app.current_message_id = message_id logging.info("Showing message ID: " + str(message_id)) + app.current_message_id = message_id diff --git a/maildir_gtd/app.py b/maildir_gtd/app.py index e7550cc..4063ff1 100644 --- a/maildir_gtd/app.py +++ b/maildir_gtd/app.py @@ -1,33 +1,31 @@ import re import sys import os -from datetime import datetime # Add this import at the top of the file - -from actions.newest import action_newest -from actions.oldest import action_oldest -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - +from datetime import datetime +import asyncio import logging from typing import Iterable -from textual import on -from textual.widget import Widget + + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + + +from textual import work +from textual.worker import Worker from textual.app import App, ComposeResult, SystemCommand, RenderResult from textual.logging import TextualHandler from textual.screen import Screen -from textual.widgets import Header, Footer, Static, Label, Input, Button, Markdown +from textual.widgets import Footer, Static, Label, Markdown, ListView, ListItem from textual.reactive import reactive, Reactive from textual.binding import Binding from textual.timer import Timer -from textual.containers import ScrollableContainer, Horizontal, Vertical, Grid -import subprocess -from maildir_gtd.actions.archive import action_archive -from maildir_gtd.actions.delete import action_delete -from maildir_gtd.actions.open import action_open -from maildir_gtd.actions.show_message import show_message -from maildir_gtd.actions.next import action_next -from maildir_gtd.actions.previous import action_previous -from maildir_gtd.actions.task import action_create_task -from maildir_gtd.widgets.EnvelopeHeader import EnvelopeHeader +from textual.containers import ScrollableContainer, Grid + +from actions.archive import archive_current +from actions.delete import delete_current +from actions.open import action_open +from actions.task import action_create_task +from widgets.EnvelopeHeader import EnvelopeHeader logging.basicConfig( level="NOTSET", @@ -46,17 +44,23 @@ class StatusTitle(Static): return f"{self.folder} | ID: {self.current_message_id} | [b]{self.current_message_index}[/b]/{self.total_messages}" - - - class EmailViewerApp(App): """A simple email viewer app using the Himalaya CLI.""" - title = "Maildir GTD Reader" - current_message_id: Reactive[int] = reactive(1) CSS_PATH = "email_viewer.tcss" + title = "Maildir GTD Reader" + + current_message_id: Reactive[int] = reactive(0) + current_message_index: Reactive[int] = reactive(0) folder = reactive("INBOX") - markdown: Reactive[str] = reactive("Loading...") - header_expanded = False + header_expanded = reactive(False) + reload_needed = reactive(True) + all_envelopes = reactive([]) + next_id: Reactive[int] = reactive(0) + previous_id: Reactive[int] = reactive(0) + oldest_id: Reactive[int] = reactive(0) + newest_id: Reactive[int] = reactive(0) + msg_worker: Worker | None = None + message_body_cache: dict[int, str] = {} def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: yield from super().get_system_commands(screen) @@ -68,6 +72,7 @@ class EmailViewerApp(App): yield SystemCommand("Create Task", "Create a task using the task CLI", self.action_create_task) yield SystemCommand("Oldest Message", "Show the oldest message", self.action_oldest) yield SystemCommand("Newest Message", "Show the newest message", self.action_newest) + yield SystemCommand("Reload", "Reload the message list", self.action_fetch_list) BINDINGS = [ Binding("j", "next", "Next message"), @@ -77,7 +82,8 @@ class EmailViewerApp(App): Binding("o", "open", "Open message", show=False), Binding("q", "quit", "Quit application"), Binding("h", "toggle_header", "Toggle Envelope Header"), - Binding("t", "create_task", "Create Task") + Binding("t", "create_task", "Create Task"), + Binding("ctrl-r", "reload", "Reload message list") ] BINDINGS.extend([ @@ -89,80 +95,162 @@ class EmailViewerApp(App): def compose(self) -> ComposeResult: """Create child widgets for the app.""" - # yield Header(show_clock=True) - yield StatusTitle().data_bind(EmailViewerApp.current_message_id) - yield EnvelopeHeader() - yield Markdown(id="main_content", markdown=self.markdown) + yield Grid( + ListView(ListItem(Label("All emails...")), id="list_view", initial_index=0), + ScrollableContainer( + StatusTitle().data_bind(EmailViewerApp.current_message_id), + EnvelopeHeader(), + Markdown(), + id="main_content", + ) + ) yield Footer() + async def on_mount(self) -> None: + self.alert_timer: Timer | None = None # Timer to throttle alerts + self.theme = "monokai" + self.title = "MaildirGTD" + # self.query_one(ListView).data_bind(index=EmailViewerApp.current_message_index) + # self.watch(self.query_one(StatusTitle), "current_message_id", update_progress) + # Fetch the ID of the most recent message using the Himalaya CLI + worker = self.action_fetch_list() + await worker.wait() + self.action_oldest() + + def compute_newest_id(self) -> None: + if not self.all_envelopes: + return 0 + return sorted((int(envelope['id']) for envelope in self.all_envelopes))[-1] + + def compute_oldest_id(self) -> None: + if not self.all_envelopes: + return 0 + return sorted((int(envelope['id']) for envelope in self.all_envelopes))[0] + + def compute_next_id(self) -> None: + if not self.all_envelopes: + return 0 + for envelope_id in sorted(int(envelope['id']) for envelope in self.all_envelopes): + if envelope_id > int(self.current_message_id): + return envelope_id + return self.newest_id + + def compute_previous_id(self) -> None: + if not self.all_envelopes: + return 0 + for envelope_id in sorted((int(envelope['id']) for envelope in self.all_envelopes), reverse=True): + if envelope_id < int(self.current_message_id): + return envelope_id + return self.oldest_id + + def watch_reload_needed(self, old_reload_needed: bool, new_reload_needed: bool) -> None: + logging.info(f"Reload needed: {new_reload_needed}") + if (old_reload_needed == False and new_reload_needed == True): + self.action_fetch_list() + + def watch_current_message_id(self, old_message_id: int, new_message_id: int) -> None: """Called when the current message ID changes.""" logging.info(f"Current message ID changed from {old_message_id} to {new_message_id}") - self.query_one("#main_content").loading = True - self.markdown = "" if (new_message_id == old_message_id): return + self.msg_worker.cancel() if self.msg_worker else None + headers = self.query_one(EnvelopeHeader) + + for index, envelope in enumerate(self.all_envelopes): + if int(envelope['id']) == new_message_id: + self.current_message_index = index + headers.subject = str(envelope['subject']).strip() + headers.from_ = envelope['from']['addr'] + headers.to = envelope['to']['addr'] + headers.date = datetime.strptime(envelope['date'].replace("+00:00", ""), "%Y-%m-%d %H:%M").strftime("%a %b %d %H:%M") + headers.cc = envelope['cc']['addr'] if 'cc' in envelope else "" + self.query_one(StatusTitle).current_message_index = index + self.query_one(ListView).index = index + break + + if (self.message_body_cache.get(new_message_id)): + # If the message body is already cached, use it + msg = self.query_one(Markdown) + msg.update(self.message_body_cache[new_message_id]) + return + else: + self.query_one("#main_content").loading = True + self.msg_worker = self.fetch_one_message(new_message_id) + + def on_list_view_selected(self, event: ListView.Selected) -> None: + """Called when an item in the list view is selected.""" + logging.info(f"Selected item: {self.all_envelopes[event.list_view.index]}") + self.current_message_id = int(self.all_envelopes[event.list_view.index]['id']) + + @work(exclusive=False) + async def fetch_one_message(self, new_message_id:int) -> None: + + msg = self.query_one(Markdown) try: - rawText = subprocess.run( - ["himalaya", "message", "read", str(new_message_id)], - capture_output=True, - text=True + process = await asyncio.create_subprocess_shell( + f"himalaya message read {str(new_message_id)}", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE ) - if rawText.returncode == 0: + stdout, stderr = await process.communicate() + logging.info(f"stdout: {stdout.decode()[0:50]}...") + if process.returncode == 0: # Render the email content as Markdown - fixedText = rawText.stdout.replace("(https://urldefense.com/v3/", "(") + fixedText = stdout.decode().replace("(https://urldefense.com/v3/", "(") fixedText = re.sub(r"atlOrigin.+?\)", ")", fixedText) + logging.info(f"rendering fixedText: {fixedText[0:50]}") + self.message_body_cache[new_message_id] = fixedText + await msg.update(fixedText) self.query_one("#main_content").loading = False - self.query_one("#main_content").update(markdown = str(fixedText)) logging.info(fixedText) - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], - capture_output=True, - text=True + except Exception as e: + self.show_status(f"Error fetching message content: {e}", "error") + logging.error(f"Error fetching message content: {e}") + + @work(exclusive=False) + async def action_fetch_list(self) -> None: + msglist = self.query_one(ListView) + try: + msglist.loading = True + process = await asyncio.create_subprocess_shell( + "himalaya envelope list -o json -s 9999", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE ) - if result.returncode == 0: + stdout, stderr = await process.communicate() + logging.info(f"stdout: {stdout.decode()[0:50]}") + if process.returncode == 0: import json - envelopes = json.loads(result.stdout) + envelopes = json.loads(stdout.decode()) if envelopes: + self.reload_needed = False status = self.query_one(StatusTitle) status.total_messages = len(envelopes) - - headers = self.query_one(EnvelopeHeader) - # Find the index of the envelope that matches the current_message_id - for index, envelope in enumerate(sorted(envelopes, key=lambda x: int(x['id']))): - if int(envelope['id']) == new_message_id: - headers.subject = envelope['subject'] - headers.from_ = envelope['from']['addr'] - headers.to = envelope['to']['addr'] - headers.date = datetime.strptime(envelope['date'].replace("+00:00", ""), "%Y-%m-%d %H:%M").strftime("%a %b %d %H:%M") - status.current_message_index = index + 1 # 1-based index - break - status.update() - headers.update() - - else: - self.query_one("#main_content").update("Failed to fetch the most recent message ID.") + msglist.clear() + envelopes = sorted(envelopes, key=lambda x: int(x['id'])) + self.all_envelopes = envelopes + for envelope in envelopes: + item = ListItem(Label(str(envelope['subject']).strip(), classes="email_subject", markup=False)) + msglist.append(item) + msglist.index = self.current_message_index + else: + self.show_status("Failed to fetch the most recent message ID.", "error") except Exception as e: - self.query_one("#main_content").update(f"Error: {e}") + self.show_status(f"Error fetching message list: {e}", "error") + finally: + msglist.loading = False - def on_mount(self) -> None: - self.alert_timer: Timer | None = None # Timer to throttle alerts - self.theme = "monokai" - self.title = "MaildirGTD" - - # self.watch(self.query_one(StatusTitle), "current_message_id", update_progress) - # Fetch the ID of the most recent message using the Himalaya CLI - self.action_oldest() def show_message(self, message_id: int) -> None: - show_message(self, message_id) + self.current_message_id = message_id def show_status(self, message: str, severity: str = "information") -> None: """Display a status message using the built-in notify function.""" - self.notify(message, title="Status", severity=severity, timeout=1, markup=True) + self.notify(message, title="Status", severity=severity, timeout=1.6, markup=True) def action_toggle_header(self) -> None: """Toggle the visibility of the EnvelopeHeader panel.""" @@ -170,18 +258,23 @@ class EmailViewerApp(App): header.styles.height = "1" if self.header_expanded else "auto" self.header_expanded = not self.header_expanded - def action_next(self) -> None: - action_next(self) + self.show_message(self.next_id) + self.action_fetch_list() if self.reload_needed else None def action_previous(self) -> None: - action_previous(self) + self.action_fetch_list() if self.reload_needed else None + self.show_message(self.previous_id) def action_delete(self) -> None: - action_delete(self) + self.all_envelopes.remove(self.all_envelopes[self.current_message_index]) + self.message_body_cache.pop(self.current_message_id, None) + delete_current(self) def action_archive(self) -> None: - action_archive(self) + self.all_envelopes.remove(self.all_envelopes[self.current_message_index]) + self.message_body_cache.pop(self.current_message_id, None) + archive_current(self) def action_open(self) -> None: action_open(self) @@ -211,10 +304,12 @@ class EmailViewerApp(App): self.exit() def action_oldest(self) -> None: - action_oldest(self) + self.action_fetch_list() if self.reload_needed else None + self.show_message(self.oldest_id) def action_newest(self) -> None: - action_newest(self) + self.action_fetch_list() if self.reload_needed else None + self.show_message(self.newest_id) if __name__ == "__main__": app = EmailViewerApp() diff --git a/maildir_gtd/email_viewer.tcss b/maildir_gtd/email_viewer.tcss index 768406f..a4333e0 100644 --- a/maildir_gtd/email_viewer.tcss +++ b/maildir_gtd/email_viewer.tcss @@ -29,10 +29,28 @@ EnvelopeHeader { width: 100%; height: 1; tint: $primary 10%; - } -#main_content { +Markdown { padding: 1 2; } +ListView { + dock: left; + width: 30%; + height: 100%; + padding: 0; +} + +.email_subject { + width: 100%; + padding: 0 +} + +.header_key { + tint: gray 20%; +} + +.header_value { + padding:0 1 0 0; +} diff --git a/maildir_gtd/widgets/EnvelopeHeader.py b/maildir_gtd/widgets/EnvelopeHeader.py index 5dfd7cc..2277b49 100644 --- a/maildir_gtd/widgets/EnvelopeHeader.py +++ b/maildir_gtd/widgets/EnvelopeHeader.py @@ -1,8 +1,9 @@ from textual.reactive import Reactive -from textual.app import RenderResult +from textual.app import RenderResult, ComposeResult from textual.widgets import Static, Label +from textual.containers import Vertical, Horizontal, Container, ScrollableContainer -class EnvelopeHeader(Static): +class EnvelopeHeader(ScrollableContainer): subject = Reactive("") from_ = Reactive("") @@ -15,12 +16,51 @@ class EnvelopeHeader(Static): def on_mount(self) -> None: """Mount the header.""" - def render(self) -> RenderResult: - return f"[b]{self.subject}[/b] [dim]({self.date})[/] \r\n" \ - f"[dim]From:[/dim] {self.from_} [dim]To:[/dim] {self.to} \r\n" \ - f"[dim]Date:[/dim] {self.date} \r\n" \ - f"[dim]CC:[/dim] {self.cc} \r\n" \ - f"[dim]BCC:[/dim] {self.bcc} \r\n" \ + + def compose(self) -> ComposeResult: + yield Horizontal( + Label("Subject:", classes="header_key"), + Label(self.subject, classes="header_value", markup=False, id="subject"), + Label("Date:", classes="header_key"), + Label(self.date, classes="header_value", markup=False, id="date"), + ) + # yield Horizontal( + # Label("From:", classes="header_key"), + # Label(self.from_, classes="header_value", markup=False, id="from"), + # ) + # yield Horizontal( + # Label("To:", classes="header_key"), + # Label(self.to, classes="header_value", markup=False, id="to"), + # ) + # yield Horizontal( + + # ) + # yield Horizontal( + # Label("CC:", classes="header_key"), + # Label(self.cc, classes="header_value", markup=False, id="cc"), + # ) + + + def watch_subject(self, subject: str) -> None: + """Watch the subject for changes.""" + self.query_one("#subject").update(subject) + + # def watch_to(self, to: str) -> None: + # """Watch the to field for changes.""" + # self.query_one("#to").update(to) + + # def watch_from(self, from_: str) -> None: + # """Watch the from field for changes.""" + # self.query_one("#from").update(from_) + + def watch_date(self, date: str) -> None: + """Watch the date for changes.""" + self.query_one("#date").update(date) + + # def watch_cc(self, cc: str) -> None: + # """Watch the cc field for changes.""" + # self.query_one("#cc").update(cc) + diff --git a/maildir_gtd/widgets/__pycache__/EnvelopeHeader.cpython-311.pyc b/maildir_gtd/widgets/__pycache__/EnvelopeHeader.cpython-311.pyc index c725b67d8a5fcb558827382a0cdc79b5d3bb8a05..d2815e0059fc6dccd43ef0c88688a039ff46c7f8 100644 GIT binary patch literal 2533 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 M8`*#6n39VB1w#jhxBvhE