From 47d1573bbadb534dbc36c0ec14bce1e6efca9099 Mon Sep 17 00:00:00 2001 From: kidev Date: Fri, 30 Jan 2026 23:54:19 +0100 Subject: [PATCH] Add new features: sort, date column, dry-run, mouse, config, auto-refresh - Sort toggle (s key): switch between Size/Date/Name sorting - Date column: shows creation date for datasets and snapshots - Dry-run mode (D key): preview deletions without actually deleting - Mouse support: click to select, scroll, right-click to mark - Config file: ~/.config/zfsdu/config.toml for customizable colors - Auto-refresh (a key): toggle automatic refresh every 30 seconds - Help overlay: accessible with ? key (in addition to F1) - Updated function key bar with new shortcuts - Updated README with all new features and configuration section --- Cargo.toml | 2 + README.md | 68 ++++++++++--- releases/zfsdu-linux-amd64 | Bin 767672 -> 767672 bytes src/app.rs | 150 +++++++++++++++++++++++++++- src/config.rs | 119 ++++++++++++++++++++++ src/main.rs | 178 +++++++++++++++++++++++---------- src/ui.rs | 197 ++++++++++++++++++++++++++----------- 7 files changed, 588 insertions(+), 126 deletions(-) create mode 100644 src/config.rs diff --git a/Cargo.toml b/Cargo.toml index 40ab21b..62ff8e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ crossterm = "0.28" anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +toml = "0.8" +dirs = "5.0" [profile.release] lto = true diff --git a/README.md b/README.md index 55d3315..a59b09f 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,34 @@ curl -sSL https://gitlab.datazone.de/kidev/zfsdu/-/raw/main/install-binary.sh | - **Expand/Collapse**: Navigate the tree structure interactively - **Snapshot Management**: Mark and delete multiple snapshots at once - **Size Visualization**: Bar graphs showing relative sizes +- **Date Column**: Shows creation date of datasets and snapshots - **Search**: Find datasets or snapshots by name - **Filter**: Filter the view to show only matching items - **Bulk Selection**: Select/unselect all visible snapshots (like Midnight Commander) -- **Scrolling**: Handle large ZFS hierarchies with scrollbar indicator +- **Sorting**: Toggle between Size/Date/Name sorting +- **Dry-Run Mode**: Preview deletions without actually deleting +- **Auto-Refresh**: Automatically refresh the view periodically +- **Mouse Support**: Click to select, scroll wheel to navigate, right-click to mark +- **Configurable Colors**: Customize the UI via config file - **MC-Style UI**: Classic Midnight Commander look and feel ## Screenshot ``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Name Size │ -│─────────────────────────────────────────────────────────────────────────│ -│ ▾ rpool 500.0G █████ │ -│ ├──▾ ROOT 120.0G ██ │ -│ │ ├── pve-1 80.0G █ │ -│ │ └──○ @autosnap_2024-01-15 12.0G │ -│ └──▸ data 380.0G ████ │ -│ ▸ tank 2.0T █████ │ -├─────────────────────────────────────────────────────────────────────────┤ -│ 8 items │ -└─────────────────────────────────────────────────────────────────────────┘ - F1 Help F5 Refr F8 Del / Search f Filter e Expand c Collapse + zfsdu [Sort: Size] +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Name Date Size │ +│─────────────────────────────────────────────────────────────────────────────│ +│ ▾ rpool 2024-01-01 500.0G █████ │ +│ ├──▾ ROOT 2024-01-01 120.0G ██ │ +│ │ ├── pve-1 2024-01-15 80.0G █ │ +│ │ └──○ @autosnap_2024-01-15 2024-01-15 12.0G │ +│ └──▸ data 2024-01-01 380.0G ████ │ +│ ▸ tank 2023-12-01 2.0T █████ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ 2 marked (45.5G) │ +└─────────────────────────────────────────────────────────────────────────────┘ + ? Help s Sort D Dry a Auto / Find f Filt d Del q Quit ``` ## Installation @@ -79,6 +85,9 @@ zfsdu | Enter/→/l | Expand/collapse dataset | | Home/End | First/last item | | PgUp/PgDn | Page up/down | +| Mouse scroll | Scroll list | +| Left-click | Select item | +| Right-click | Toggle mark | | **Tree** | | | e | Expand all | | c | Collapse all | @@ -91,11 +100,40 @@ zfsdu | / | Search (n=next, N=prev) | | f | Filter visible items | | Esc | Clear filter | +| **Sort & View** | | +| s | Toggle sort: Size/Date/Name | +| D | Toggle dry-run mode | +| a | Toggle auto-refresh (30s) | | **Actions** | | | d / F8 | Delete marked snapshots (2x to confirm) | | r / F5 | Refresh | +| ? / F1 | Help | | q / F10 | Quit | -| F1 | Help | + +## Configuration + +Create `~/.config/zfsdu/config.toml` to customize colors: + +```toml +# Auto-refresh interval in seconds (0 to disable) +auto_refresh_interval = 30 + +[colors] +# Available: black, red, green, yellow, blue, magenta, cyan, white +# Light: lightred, lightgreen, lightyellow, lightblue, lightmagenta, lightcyan +# Also: gray, darkgray + +background = "blue" +foreground = "white" +header = "yellow" +selected_bg = "cyan" +selected_fg = "black" +marked = "yellow" +snapshot = "lightcyan" +size = "lightgreen" +tree = "darkgray" +search_match = "lightred" +``` ## Requirements diff --git a/releases/zfsdu-linux-amd64 b/releases/zfsdu-linux-amd64 index 442c548dff52adbc3edf70bf0026e5f3645e311e..c3d58821ec58f17225cf63925179fc74a955f6c7 100644 GIT binary patch delta 41996 zcmb@u2UrzX_b)uNXND?Oj&wwdq9TG7J77C1D#nH#-`KkdYGMHe6$FfmabeeJj6GQ5 z5H*S&ON>Ti?7a+PARx;8wfmkqr{Gao!K1MJP|2+@ z0FC6&t2)`Y@4c?OD@gb>g^u$@-zK1sq^el zLzNI^4wc7e{J5B0SKqMQxqddeM&A(8KXWepeDP~ICogU zcu~l`8Q^Z4aKs$rjNJZ#?ztX;A-OjK5FkM3K%4Co1H;AtLGB-v)&P6}@v$ocF1vxFKeK>Sz3X3qK<_ zI>deZ>5$Ws?aCmFYVQ6}_uRtJ)3r{YuozwVsnHw1S~Nf7uRM`^phj3qz9rkb&%{xe z%-IS*;jAsDGl zFZw;6@_|_<{QnEPzBl^8W=-&#W7!*mGy;j^XwY^Ax%Z%`kdxzDAH)8J{EceU=Gr3 z3tgs{0D39X3Zza*-y?N-CZ`C~fRrh90N&j~Hvn9S^reL^)A@f3(fh#YXyGpcOt#?MQQll-4)+B9orP{}fh_^&TJSQxG8hBC>2LI*r?L=bNhyb3 zd?3o0|2Oa`3|v9Vel80kNNbULBNbX;3}CXP2<2o2%ZoW#D`uDu`XywEvXUayurTXb zU?;$K7Q8Iud?<5#G7>||u+WV@(<{^GFb1*-NWUQsuvUa9q*$bSNMn#vkh)o?fC*!g zl_K1Q;uEB+NDiPqkzN45;DTDXDuM?14Cxw@2k0tD?|_f+fWuyj@X%8cz6ackxs2qAv>Egcq$Joo4Y<>5( znO8$03MmG%ZJ_u1qWu9^fj1s-I#M~{9U%V!_&-Q%L9ammE0%QhBfO0F-_Y$thBDy- zpY6Y|e%3BGwA3Yc&l9`c^wRo(ez{FbJ#%-L`sHqYV#rM>^~!zwq^4unQ3D1hju<*> zVDr%~C;kHEZi`_q9(_ci z%RE7t*jE%502Zk;Zi`N$V(iX@E41j2*vlcZ7Zcv(3BpUjC=&0AY0P63&AcmCPAQnm zgwxdp!6td25iL^8tmLf)MgO6M_Ka z*D@gza2;T5%@kz#fe91Ahy=_A%mXaq3hR(D9G(KY0mg4)!T>;z&4>tKB;XoABVYkw z#un5(0yPH=0<`%N4gwAV90T|ga4Dy^BT|4Dc0fO+77XuXLO7tY02u(+{frs`hU|f1 z4)-$Qth3xMm+!eQu7#Azy|4)~8? zAJF45ngFoiDHBrGamK$)cnLW16$(U#B2f}r)I|*>Ntg_{&PEbW0_NFDLTEk6?Ia-^ zu)rQ$H^9hpl3=p}6>*S+I1Zg8!KXg-0F8h~XGvHCzK4q>Oay%aa1Ef1tCS)X010tL z0bCLA24Df;8^D+4B_S#j1sEhD9dHO>9-vJHNhk!o;4TTz0OP$RA*2E16(wN^U=d&* z;LA#qum^Bm5bSao41F#KbZ!Vcl~Yh7AjZn@6fgsD0ALYdD&SMVrGSxDB%uH>9`GEX zO?63-8=*#k{(vuQNJ3*yhf6{NV0;}(SitGJa1<~S@CsmFeMxX@4E+X(FyMuhMv^cF zNJtbs2Amip2}OWg0Pg^1G?xUS2|NdM11tax;jo1y!~%M>MuC7sj3^M$rmZ9#0*vgB zngSXpOF~dn$frP`LsWPo;KWokAz+HfG)dUR8GxrboGuBafJK1L%}_v^B!mOz0mgAU z9S(CEa4z5lz%_tFW+6Xd5ugx-&IafPI3yEI2sm*zng%c=3yuOt&V{2<82?5fH@E`e z8^An3=V;UzFck0tpb_vTU~fQ=d8h&45Woe18G!kK6XzphfO)ydAA@{=p@5MKke|bE zksmN*Au0f91k8>}fgvDwRtUlpz;zI81Uv*-0C)pX1AGHGF%O}{c_(kNBy<7105}Bj zCE#ShqUDGrV8IGRg2R<)0?uD02_7wwZw>m)N;J`0RDi>E$e#j129O1S7uKWgA-J#= z1@jEsU;wa4MccK6qXifRfEj>!fG>Z>Fk6L4?Lub-wD|>{6EJ=sI^%b6`~Vz*Ui?8+ zfagC3M_NPwxFpm8%sU|oDP1^n5;aGLb-zhM5@?*Zg#~~afN#JrID-&_wmFAhyBhlE z(e{Aru1ms8@UIPWFe?6YUGUL6=0-`EHwTB1-aq01Q_ol3q^n# zezNce(4!(wTkGIJkSwfRr`~!fK6Vx>(3!Vl`SJ$A{0RZ*jkM{QKqbyBSoL`)Mk>{v z!8)V}!W~Tz%9%9rk&ScFUtP;vu+eDYM;^kpoMim8NBy@I>OHbwzI!1gi<{BEL zFS;-R%ZC2(!2dVkRp`nDSE^)W<-N^Kljpl=JH>Dbq}T2#3U@!%aCh*gosI_wR{s?XnwRdK!9{ocN)xvCtd=-X>b)%-Sh?fIg&RXI7<#1~hf@2TktB z8}PEhv(Zbza0GNl*UO+LF08dxZ=jQ>&}+nS=7h6gYUj#)t)egnCQhSKuB?L68~91ebY)dMGl2SIUwGFN z9NsOLg7=1wxw7h3CxM2}qF1i0sg-jDGqHyvF@mN63{vq=sR~- z*=i;5i5KXUJ1Tw}cpklVXI1*V0UCOV@eQybv~cw2?dj}|YJvChzzG7c%SSx8C`5vn z0Nx0m7qdejN1IlTFc1Z@3uHTJng?rUn+G)iG83B7yjsja*F4ZQuYkAdie+Mnb#x9w zmQwJF$k!92$pceU)K%QR&Gd&UEGA8gw@g#K!jaG^e87Z%f%6ZqTr7B-N|@k5 zXS`7Q5RmyG>*%DC4)Ppr^tr*;ps7 zoc+)m52>j)G(&+;hG-*2N8zzlvNxuV-r&7?!US7gz0yRGr63W-50wDwkF)@?9kkz@ zRkSMrn*WRm4P-jyh!}}Jn4}(pxraP`Se0OpikK0xC+GEhKb-@u!xLm(AnR!k?_?k5 zVLJl6$*-Bvo%1~CCm-gdECp{99fLWmBA^K-dhUZsxD`TDZzw*LND?-WU>XRdi; zkOx4X*Jt}0@gP&JBmpBz|M2*a@W%uK9>@8|A1M|8GHA0etLV7~=m2X;z#^l6!tuWV zynMRr%W5hQfu1B!KQz2gB|&Ihj@tUMYF4ov52UX-J{h=AQ`+Fi0<6{mcWXw!`|%kP zxE6(iMcuG~ek0rg4`aO|^G=ZiP%t*k-MF-D`D>jf&yiSLbRS(k1Uf6B^WtM2_BR*n zawFua%_RXFwhuO1rb%BxgBLdh@_fi!d?x?8oOOz>kO74oP?+$s0!w^fz&gmITKv!Q zBFI-lp8QGvF?xO;a1C;yr6knia(ws%_6EGrmXajQ(OceaGSk)hyc!aSg{QS7^rK3S z7|o;nnX7L+c!l6$ZP7p6#0c9$RvaM&qKgV{g5NwqN3+;^8l6c zS%w|pEufo~F#Dedng?+!^Jser@+3&!Jnx$wDDQVR!Gds_>IR_SH3phZLj$mY4FQ_k zNfK)729$J=*pUP<_lT81HAu>)79Q~(;GO$I5{iNI4|l%^y!_7CN1H9&0l9-*0^yuc zSrAGfaW)SpACT8Unr8;vNRX#NRxCnE;FZxC~;CRf3@=_LubxmMX=Ku2a5^}ZqagB=2$ z8@*_YFY}@?LAVHw1!E6=6~rnkV}P!sWkFcg3W4U)qaZ}?A<&0kNJ+FXw1-!CeQ3qK{!PCgM zGOKMJQWY!P6p2P#vq}yg81P-d=z`;`Lems%&-$e@XWCeqJ+c~84eQ@@T3!Vk&1~Q! z(r8x|=4G`Bc;j@s#PO5BLo?_V@Tw1iXJ<%4FYYKJGt|)r|FN8cw87PvFdOGA21qy4 znIH8EVZKTn`0Hp&2&-+I0W=?HKohMBLa^V?Vvcm3>zspN2Z>dgx9>Bcca};*B&_gH z0+J0{s&FNrXQvskA?RC`)vGZAyrno7+HfU&cqmiB%LC6-=a`r01>mir{Z(1BJ{N$_ z{ZSGkxEOw?G5Fuo(K*`DJi)z$JRkC!@8yHv%ei?E+@&StWAJc!vZHe>0KcGH3Ldsk z^;s24ug3gsCxe%LSQ2iT_bO+rF;825A6MsyB;-*4>dc#*tFs_=Wpy^B35)Ne+6`sz ztgLRroP3r#jA6AXY7CR@PEHeqi=3&FBWI?LVMg2A<-i#I{H=-lTd_b|I+po3#&Jh% zF|@wNrrnYJ#-fC)Q<)Pz8^io*$XJ%`%3H|U4!f=6g77uEg!~P)N@NWkNgKHB%n`DhO@qSRzbSN@V5LH;F7=F+@RWDWan76$KTzJnJQV ziipK{xIU2?ToFs*2IK?wiTbJ{52pDOSpdD9$ac8$4qJrH`kwuw@B`0$pK5*0D!WDj z=W}bF1ER2%^Z%sBU$I)cF@9{w2Y)x`tF-qkbg=lZ*$t~C1&m%aXA+BX%)x+p zAOyKHU7N(3y7Fe?oB1o?ujBmk)D(R3naqlq&po;~nbl&(V)B~8!kAV}Z8@}gL|;z< zzsoyy%M|7)vM&C#cPjgW6;>wK6c)o8*QIVLpbL7@h7{I;g^Z+EDXf))Fbe%2fAalj zh{q~tw9%MU7Q~j0p@pfeDVzM9&ZjazmMzosRF=px6dE=S`E6`z*EHBW=RuCs*?;W) zJtd(Nx6}D4{Wb<$ocYt?PN+8>na&0=H(v@%W5bwMl@_NVS9%S4oW@$QJ>gU@owaok zYDZI;9F~ir^Bj(e zA=jCJ3u36lOco|SqbW04BQ~Tt?c(f*t?2no6dlr;+Rj473cAtkS)hXk(6w2t1Jr9| zu=*@^Bn`>{J0pp9WuOM>U(HBHO*vS*hk@dZ5D5pC8hLaDDx!eObm?+nXG4J z@n5xeKI_TE(KKfp^I$XA(7J6boOM~FUfRZXh|FU-&E3J`!^YYevpP8&GmjSAVfH!s z{$tE6HD);(vmzk2;o_2Z5UUk_Vr@n8SJ@Bj?g~2j6YJhE>4l9Dn`tr`h65?t)+Y0? zF>Q*CkYpR1dEaE{Rwiw)BjaphGoNV3w%G8ZsOL^rMYN~sJJD;dY@uyCS%kQr?tt|B zdW$IZI9TS6G4qTu^KWBL-M~XOLh;lv8dHFN&^L_ca@bU-J*{&OLg*K!o-Sa=Me!eX z$u4GO>{vs(yN9)4gEmn0Usy=F#v5>fU7!D}wL$Io3#-e-*XpG8{Gi%1`)TFPEvh%Fj3iXR3Hczx_}QQ}p^@TTf!gSwGto z9$@^TRavGEJI*FZ&KpCR;AC&iY0~q8ji4w+|;!Xbvr@mKMNc@qFZ%s}v){q-BUphG}_cnY=ciZ$SUHLQmbNNh*J(_cs?V$%( zSiai!8Y^Ywb&Ex?GfC9`7S`idzw&PIA~y40o6IA|8HLG)eSCe1&3vcL9!D#0vam3F zj7g{%(++v`itZiVH@crG_{m8d!I*i_n0a68{n%tOW?t6@q$5`IXVCRqtU5Kk$=a|U zCus&?)l;zexdHaUz&92H6Vm9#O*Wdk-Ns}-?H0QI5=UBnn^j{6($wE>vtk<-@M!L)FcBngSB`1;nyNwRpNtJBRZ$q`Rl;5_|6FW&`Gj7mDd#MilaVt5M zlYV6H@~N<#)Pfb{la+%M$)@I0l!MfR1?JNd2WbPF@FTT$lEEpd`Mvs0U?)Jgh^b>2+FouxR(PNgPO@l+W*k}ThUHmS#)r2vtg*+`FEq*#`= zQElWZ`G{=b2I^5>ieiBqXi0f^k+_~>-J~M6ZXH!HNYQNzn!GcibNQij?Zu=oXl{Rr zg7zb>w|F0R;Czf}W9E77^%GIx&73tk2y@3ZV^)`-IFliGo!$ksoPM;`M)FbL{>$uS zcK3{G`;JWz+2rrk=_a;tmNjmQ1Ug*NP5Yf zkCJ0EAID}s(5l?zg&*Y|A@f>n<}H(<>mn{Usml#gRY^Ro7I{c>ZN0nv0UJ-6S#0#3 z_TKwO=V)92qMX=5tr#d>#cN{q)ptSCKUU(u^sKs6yG{NHQ9vN9Ch)%bn=$ij$@;5%xn=9i5cxwM--A{XtVw;Vm~>^CN>8;v6+8qhFzk-=hDeR9zv2m54FF^ zprIDVtk@vusPsok&5bjTCAWn=>+Q%4d*Q~TFv|Dnyd)1uqOd>b68{RAgvPDpWf-Q! z@e(1|#vP}uFsW0DQIE|Flc5=ql1&(oZBVRv4#&h^wM^{KOUDRZ8Oh;6i*L>r-x$v< zS@_OEk{z#)vzANdVLcp~v_%cd7MbYo(cPlEbT+0vG}6X!^lkqjx)v^Z4p2I9_Y8Yi zSps6ri46+TQVP%{yyEGFAL{-6R&%{Qc)k7k)-^VBa!?5BU5GP{IU+eto-Ws`{Z_Y| z1Y1vws8@s(XMZe(Hy^59m_pNPNN#mElrdHT;T5e^|9wSKA*@|5w6KNd>R3Uf)rn*R zb*w2xMjDzky+D67>Cdrx4F;_Js0JQdZS{XsgFCdZrsP>Msf(qV&`Lin)7{E$Yx+@Uw@*j2o%jH`O%r5VONYm=UDZlLkpR z5%PF(mxr{|T^=^~6PIuO^ugt5-r0-@?XO$tuTUwVeEd?~&;RI^<$lH=F^4q_tG^!B z;3;}o8#IA9Hgpy#yj{QC&+fQB4ujI>yrd)%A6xZc_VDUr{I-ifu6; zKaVwEPWIGEzRoyuvYmyzrSB#~KpQ6g^i?E zRJk$uS3IawV`&r{;X(5nLT=Rr@;7RD6Y03<5>bK26r;UU96 zn8bRQr{9}P-I!cnt=2+%B{S=CYUMUkvB=1pT-r(N*;re3PdmxMhK(cjL}%%w%)Xzm zE{d1VFm~WIjp&7Hxxnv2~N8OKUH*_N9N6QuxVcUKKclv;^mA`KrW1+wM0)w$!O zG?{r0R}B-Tn@+6muk>;j!uQ)LwO@wRUuyMmxF{4Gu;T06fW4)BJ^B5JAfO&)yl_5X z5ruGLR_~xlZ3ma$M%(H+c!Pd-gsifq*px0@k_HLpg)rINhsJXIqrq!qIu3S4g?G9? z%|Y$TWu3VtB=nSP8Wq7p^v9eToKv%m!%G{-Ip{*>Tzp>d&pGyD2)FTD$r|0-UK?H$ zBbWRQY#$a~VK1+VR;P&m&6c{bYe%VLj^rLcb{M)`B7D1BZ2iHLUYy(8Y$_k2(3T&? z)lEC7`8|M|1WQIZQ-`b7)gw_nWVgxmz9yrO(%u}&hjlutUdfTJSg}KgY1aa21nYKK z4f$4j#n|lK6qF~0up4{T4tWwWw&A3Df3Z|sW-dEbpB2&_Ni0X=YH2AmEmT*mmKw-n z9lH2~v?XQDCb(i5Uc6njT+WEs%jOYE=VTiT`CLopWW)E;L7c2^arCj)os&NANh2p6 zEXwz^1|Xg6v5XsY_5_6^=vqN_J8WVKcqFIoCFk{$7aO>jLf1>Jna^HIUN22%%YRXy zu9v(;=DUHMH%eVuwLLU=qtu=4T~CD@B{vqokokC_*!YUp#FG35=Ay?9&I}) zRbw6Jsn-rlKgcO-v(X*-QP2|K$JzpLN+z14kEZn3(l}%Adq$*|$Qj+*^2ell?5sPrJBID1yqt25 zNkQz|GTMGjn$5mgM$L{(Et&l?b>?yD3&zyNbm^p2)jE4IHf*yNt9HLi1DJKCJWxJ) zYVvPVx|Ntlul|s3){dKs0xRhr@BtdlNmssz*_ge;qEG`o>>9Hg2VuXc3HIdpr?iF* zv!fk>TK*Dgz*Y@tm5UdApy#apd;MT!>Lrj?X%I>I99*NR`>DXu8Vj$5Bk-RzrtH2NQ` zR^GX4?mzH_{W6dKdM>qM!3Wg_FQitI_?A*%NxN7~KWh9M$FBHj8vj~qVEwijT`l7; zb?0korX(fA32HNwG)rOkI#7`;`^d&Ga4j3>DSMKr$miJ~?dhT-`>^_*sJe^nrMg(j z*F@&fj{dTi!1P|+mv!r)Ua*mWV3KbOL7i?dj}@i$ zF@pNMoE$1iUpEsd*jc{ER>hEm3#?-|0qdPuY7|Z7^!%3UQWyCz#zr@xZ`@=rCN-mt zZgK$Y7el|f$<>(DgkE#nDMqbmkb^}jzJZ{&t{`6!r786V3h+?MKj_A^XQGm;Qyi~{KZ4e^79c#$^y$=)n_KOOgz zYp~#p^sk@%Z_3?2G50LdM@*CRW2^0>SI#iW3{PN-p#d(*p&vDwU! z$~5>Cx5&(!@Lnmy%y2AI(k6hR5B0scx)iiMb!e==>=yKeSuf#z#%MER@Owu1pS0dz z4o-R89Nz6iUb7Yc!i&}hm#UWehO&d@spV&2vs7X%(NFm}#wBRc3-E+Q zuE;t^Q`5@mmLo6Gyvnj4Yk!HhR+giJ?C0PJZ&`BHPH;2F(0}n8^O=?(;heRL`^Zp5 z?(M-3eqH&|FY~oA?Wm3RdLwdXH8)am6*)Y`dzQ8N^xz5v@!>Z`eav@70yx+)wE5_I z@zG3+_8lP5HW@DK9Fx|osjh6ynPk*PFXO&JxYr_V3ZXWlnfT$tGrC*nIPOqK?okx? zC~ZygvGZfLFwzkASf7Qr01n_g!3tVOMJLuWWTtq~^5$@s@FW~q9p93$hUkd2I<=k^A+ZW*}C^yVSQwI z97(IghSRtu|`}bB)fC)U>9Y>2p2cBMlFWjnsP3*ixTv*OZ5|==u~FE)QZA zLZ~2Ie&cTwBns)+sYv=9UDsDKpWCB$>2vLaKspy8&o(SSg=f^Ce2TLgsN-tMwVC(A zgJm-Ps(%8`quurA08LJ+`|HSS7_+WUgX_y(#ZvmEzPymxR;R9!@@Cev8p#dhQC1Cq z;RYzAAv)~x(G=fMo)j=G*IF3lR52Yf$26Dr(&Ut$D)D(gUMw{_ZT0P+OaC;KyLs*p z!L*gr$77x7l>QS`+hm?cr_`!-r2&oPP?r`-Xit9k_0p8=W->HMqE(Hs0)>RoJr2L@ zLe(3~^;n~E^kri?Mm$Sv8_T2F=tOdFBG+QICB(^#?!*D&jcBK&TFm<3}%eJAUe9Si=0 z)eAjZ(C~-_)pD4GzG2LKg4sDX^Ocs2(@Vr7oqi6Q-&c8P31B+ex99V>a&8z`$GN2B zQpBS+nb#HY)(<1!X0m5WhcZ`M>aO6p5t}&#H~ZJMJym!v+{xkk#PV3BQkk*`SB6d= zbV_tBQ%hugq8)8xr3+HbnK^5Rg*5`Kk9$Q|eiuhSHItpCKi#eA*PmFo$U&odU@$WK z0Qs+{I>s_-GaWHM)!AK{&v)Y+$N^H2#Z3L}qbz1N0QoO7-XwD0z{@ zc5hdoHkW^s*&SQ;kWro?v$Nr9yY_OJ$hLdXqz>qLUwY8S4(JP8Jm_`@c{h#vNAZ@* z_p_$#f0T++B_Lb=QJP9xUu$~uj}j#P45Y$yrFz(;zT7iBq3Wq4FU@ni{#4>%S7YYg zpZOXdjax@F_E#GyzazR*BAw_cj}&EU5GN0k9p9Nuv^ow>g!ZAcahMCO=w%#yQF~K( zCwY-_C(sH3`L&Zgk4^1G317&=Sgl@k@(Xzy`>`i=>@45$+1kV0d=W0z0x#7#!<4LO z5?{lIP{%IvcIMiHejA2KZ*M&Lca`hg*1yUZRYTiOf5XCiRy0us)F`A ztaWp9sGi2~P8azVoYHLK;5zK9!>F@}- zkhwe4tdVk{^RCO-1ee=|H$ow;OP6WaNI4SC{&b{VorSrR|0ryPeomz5QF1@_a29Mq3kcX4ur8P9(|!jb31RE-~<%c3eQ~PXRnkhFf4pJj&FJ z(+(A(tGwSL^}$_5lLsU94${<~LdMB8t2orqTY7mYujzsy0s46D0R1bwf|XEQ@`~^A z9mdqAspI5I&g(|#y`!m}o~NQ6{WwmJ_I`#XxS03$&mRn2Zb0@)a-{t#DCo0jUIXfo zgf<#@mxdwIF_bk$PQlfe{ZzRl!@;5VRJoh6R!!YQW17j7 zWMwpUv`J#U4)!tydx3xQqYnuN$pTs`fzDaobOXliGZY=_!G`(YTpnu@h? zWSHIqCC@NxvL9^c#k*in_IC}najNWO%fjEN?K9;nqEoo(wJFMpj{J?svja@8DQ&i_ zIt+XF+T=w0Id8=~wRM(UjhE zU4XX!G?otwzU;JmW`W#TWTT#t(?U6cy*))^7s^rPYP;*fLjbC}tG_IipGz#FJpHs- z4rRL^((T3A(hhn^kxOtmJ>aSiSRy|XZC`(@d-L!C^;{+wR;y(u1G#ezxO4O31p&v; zBrj|L+N7DB#(V0^&|)WQu^i`b%vmzv$?VT02#35i|xNH)-m=HFBxw z`p+%B8F0{uC`4O01m9NouSJP$>J>7rlTWhz%j&P|<#A%OLsws8vI~n2cbXZGA=M_Q z6*Fc|=T~R^$yWkz$gxnt5fa~#LDfR9@xf-19KA{@8|AOr>MQhmBh3GLg&J(a?&aoX z8o5bs%W|&J&zr#Sewkiul9#jAf=ZiZMPhT`((-)yjqR+OdSx2@N~^cZ%Nz#$$=%jH zUi2rm-zINiH($})ZSpL~gjakkZ~@I~GIV)G^S8^*@Ycoe+vO{c&(FO!MLW?^UeRiQ zsC#yxciM|DtuUOQm<%tmY5GZC=$P>0BLgqg`8(yNBHQ>Y{jSRU>^uLXZ!}DXw*S!5 z0=Y7CJxzxS3J zXNXK4zDLdz+07@avRB^EnBOBR_(QHC9eHgcZL!=@Ji@z3b@8E^v>!J#w!25`LEUsn zU3^gfnMw2RThY$Na&<@V{b=h!VV1ourL2;TVj&f+Lf-qTe3bX>Ugk+Gb|1Yel1KX9 z9jco;zqhPsx4?UBPTdP*bLwu`P3gxlD8lYgK8LmL(3NAdAD-~MK8F282O4@@Ztps+ z!W+|5r>PDd_VR8P57+aF zu%&FyXf&ViVqCC0;=;i)4(%wWck)GuWzK9@^Ua8?afv5&&XCX3%fu?g>tA| zu+<#x4s6g?%WUwcI^aBy_lcf*IeUJj*%#zHZ2UPIb5TxV|D2`2E@IuDd6p6`$x*Du zS^Dl0?!G$x&gI^ee_1v#_uuL0Wo#)Hk>3@ZPeRVn&MR_naSfHfDhJwa!34$+d0`o9 z)KwYJ^M3w~T3(auI1K;IO4t{LPGmB~|3)*f$+Ie~Pc)fuWz4tO&Ga2Mo(Ug3MS<7l zN_HzxnQP@glR8|NXNlHy{<_><%%rdzatD_1D^0y25A;1dhFieFoKIc0h=R%R_@vo^ zQRxeBVl7*BlA7Ga_F>IQ8g^5zSf>a}09M`+%b}H{_yX{U6xMt73#rP}^;vnFS;uJB zaam5k+>~oqnsJTiOu}*ffEz!8eY#cagXP5Kmh3AH1rl*fuGuL41h?Jf0hSjOIA@t& zU^9S^3N%jB3p{7m8D`e0y^NOJk_WR_$4S18b!_T!O1O={&pJ*MZp$$~x0hh->HC75 zCfRRw>)}gv>n=;_kK1xOTYZdr-I2R`h90xR8SfCky=bn#$bsY1-zG!dV|46}+`z|+ z8#;`Y9Bwv$qZ_)vST|I(nEdX_o*v&9SqX8Fx|*ds%~D#dcDyTROKkIM^{OWSEZQAe z#yjS0s_1~Ccb9}o1 zL=<8#_^nPWbLvy$zvVG3GLp9ZEf4aHs;#Rv0?^g6by}HGn`%6jhqA}DDeoz+)tb$t ziqB+!yBZ6)AA^+STx#=74y>kqV8`&}owsrWW-6i4Z_z&j zFVf7na#PQBCAvDIs;d{7vo`VGMOWXVB_d{1`*&DLM9O|Aw`V@sXurc`_+t&SHp%^M z+%nM-GT+Umgm-eNID>Maq&_gocns2X7Ws>c2kSCRZ73==7(0`x4waOxB70m}^-+{* zSe<67E36dkpuBF;W*a4xJ-kVmZIpjm;dnZ4t7Nm|tJMK^N-Z&L&2`@43{J_I>hS#= zL7P#+JEGxT5*U21#hWM8%zKbThwPQctjk?u<&jje-|rXO~cQSEZ6i z!l?H;{MDoWw{^9)qiBGu66`Pu7L1wYg3y63jHQLHN?o?ylYT`;HuXG}x+)=}LZ0P8 z);dp(%PaNS^_7%dURl7#C6T?G63#k}r6z7lEE`dgvfPwW?4NV$Yc~ZCFKe8ogB6q@ zOmSKTWf+_1OI_TRD0b{DEp%7oT$6X1p)Z?rku>2v2PbD6(=lZ^;Nr0K3I)qwz zD0dNwP*3D8TtK5dm9Ew14Yoq1jhUy6nftYemwBC(fgkfOn0XBbb#t<<(X;?5TmWx**5@==P|g;Vs{ zM_IzI8`Lacv@$#TAKK@qTw;e#@ch11P*HJVE6S@UDk?n~yO2ed1C(nl?v!d3sH~P) z=UM8;%E|yM<~vPwsiB+|nK6a#g()?edkVSNR1Tz^jeFlp+MkDcbsP#mu=vyR{W`bF zvfX;PN+8^eYe+%!Jj%73>?+jx?K*+azeJ*mI^Ut%7d%jSx``=Ln_{*+=wr)Aw84i> zDcr;Yvx#HeL_Ka|-x1yPyj{BK2wo(MaLqPubAOyx=}}Qqxf1%SDQMb-hNE|sg`0Ting?;wo+t^#f%5G`cBHJ zqtx^Lo-a?CGfHvNGaj9Rm*!2FHH8xRqDrV2 ze>78;C@g!3n%Gh)mf4vvsZU#_DqH&{Wwcdh8kg7o(3anA;30I#`oQAh$~JS8KXCcb z9ItbmoL!-ZJ07pKw^IUAlIp`6-q6>(`!%huS*g|cx>Ch@h?(BpIUDu1YWopiZ^L9W zf5=CCY#arxkIpwVg@JwC`daj|K$a2=GIqek#W3h{AO-Ch!fsL_la!3Py*QjqOdQNzgh3P6wp~|!)D*0k)4%#tT=+c@2s@KCCS6iN*&g_ z4h3{kTC?oy>hLbgcE;vUC;#qBKNc~argc}0Y)J^6=&mH%&brK_R`-X?)G8kN#$Tq% z@fd{8^J#Ou(#~r{72eBQ=4&Tjn@s#Zyp-Qkdtfr^ zd4;a^fcd7CDWoT=F>Ee%?5X6rTVH%_(yz`-${VvHap`3PBZn{0yPnD-Hu4K~Q7-uHfCPZ&VQ@-lL_GAfXX?CbdJFn9KN{T;4cc|qRv8|F_Sd|sbYc>94#N%SZd0ojjtop09go^rxv`Dm5JT<2P^+D-H9;zTQ;urP8S4S7ug`F$e3o zzOE|^e?+FtmKUI9N3H{uM(o8e)NugXXys&*2P$oFa_lfr`HtbcPk#?oBH4?>6gmhi zTuVP{Ge~LX_RL1XK|w<&G+$`uG`V+}mO{gJ^!m}B(s5@twL3a|bDFjFQC$xs%6Or8bLpqxNI5{`EdUOU5eo z91iuc5*mxiu1-6ZT|Ma9Sf#pCbG(y4NLejn&soOb*`x7|hI|v*Y0)BMauCT$*o@@#C;ueH-B^?Jt)Wm0h<=N~&o*7~_F~#0 z<0toybx#ejxKnZk`myhQIR(m&Qj*YDf*a7!NrjJ8(mQBIjuhR z8LtdsRmaiM@k%>(C6U}FV4(k$NR1|-?dQ~^F%y)@b}uoJ!;m)dk@|ds;wnmVT`N2% zlB$(Eipfzw&aSOvMPgGaxET-u-)F#mINlNB-)!(Gsihxco|_EwA>%Biq+)F5OJinp zn*wJvM$3ptCGPr-XoEWR{Z|++bvsa-udx|xT!*HAt<+*g?P=%NaC~?N_4e0_jmR$6 zqDPaI-fUbgwcTXp7OpO8Qq8H#Bj#L_{8E%dtY;YANl}`xUPg1ycSed!RdQH&nXacQ z0~)Q{s80k;#^in&K4?Rd`S@?L9XA)4hhi#$&tH+&{BP-}U3@8Ji!MpEqN&p`g4>1C zl4**MU5!w4Y&PGf{nOAF9@*2?X^J1aYEN&cDL7=2I$g=|>S4s?*$fH2Gt0LV@Q7pi zVyRQUL$r~4q$x95jdJSEG$n+&yjq8j!AsT`DDBP>)oZ4rh`sOpVBzDryIFMNFn&4^ zG338RrGHY5X+?MhCYAI;|7g>K`}G>Tie19-#f`Mhd(rDNuNUJuj0!5%AZ~-FY}kuD z-A$8bDcjhKV5*s+RPkOLj0Z9LN5AKFz};;}n`A3faB?t>&A?XouXi{zE525auv5Wp zVW#5AY9Ao)Or@nqmA;*Tj?6tljxAiHBcF&c?LecwmYz?pm1)cPo-ON2yZpWJN0>Yf>yOWq_nj zij$JhWlid0GVIXFcq!>bS>W|IMfhV7hXPybJ4Xqz?+YE>gbCCGX`PIhk}qXV>TNRY2g%hV zpsw}%ldtFI55<0i@juc}{puT>OV~Vb>N;OZaIfjj{cSSD6;~pc|6>`cvt8-reB~rt z@P-!WV(a|N8!{|VQrPIl>cR!e4Usi`rA}O^lo#D5l4Vd~D3}LTzn|0TML z$mKC%Ds*?^pdanDvxRXTSfS%m+BofvHLj1%aX6DnhQ&(9YIBR>0pIQNu|2*wcfzoM z^Z2B~dH*r<@``EBV&$UC(%erboS9o`;u7Vm%a-|{u=DcOK}(hOqQf6fywmFI&2cC4 zS*`>)WLJ4(a@y(Uf~9K#_F2o7CN7iaev*6O7D`=*!^`I7ii=CjPh~YNvfIm*+TN!e zVV&Ox9tKh}3r)LojvnuS?o-4HrAEz}T)|xYB(tve`Y;AL-YCI~yB_+>V_J`{`dp{o zJWM$&l%ACmzR_)LxnI^cb-L?q>08Dpo0UqdO8=URa(}P(YqyWp-0#!qmDqcoD@S&# zlxpRR%3-EDb61}Wbe~t0qt>gG;Hap(_&H50-Zxrx(G9%I*2{MWe{U_$U*6SZ!u97T zxnp3Ccl`qgY11mDrNK4llMueyAPXgf$n>Mqje38le90mntNXuG9K}jb=qPC8@wZ@I z@2Rn!^-^n;@u_sR@`YhP$d4A5jq5(x+q_1ZU|(pXmv1ttHfqi9x#3l_bi?^K$_!tN z(+#I(SoqX;t>RsNxV0tYe}Etm+O556_FCn(=(1quCq+(O!+XIv`@gX0VnW3Cvt)X= z4tvyP_toU}${xnPyrR_+;~=l3Kp^JAJk{89D>u zMW6kNmi(y1I{T!2QnD4j_)*zeDPoGA+ZmIK+3KiPx>c==ub#@sGb2{}HJ#mt+pp56 zbZWcujmz4vKe0A9k4EiK67A)G^+HXC*DtALhZ55N%u5TS2ne6@jrq#^cSkyJ6(b7y zlHad_m5}U_<=YFeY#Y#y^V~Bp^y1H?n}RSe+Uw@xz|L3D>R@TFawzF1rCId|SbzUz z-?CTn(vCW7E6d~1k~Rt7fa7oC;R|rqbnPeHt37x@Z+=p|B2T@5fBbo@v-x?fd6_E! zmv_9#A3r$zu~q-_)M2Mmfu)+Q_zke=-`Qf;x9Dc2gyR{(3tGNYX~iQGR9b-R&!|ll_Onvam!9$B^mif;V({xzMjCiLr$obkRu;1xPswVR(wr@M zO6_(jUF)}aiXYv*RJI15Jek$Z6dVO!87He5=G=-$ajD_5uo6uhom@xov z%YBg61>d zP!1rDi5vFdCgj<8`fU#aFs_t_|Dv>Ysfa0pf45+THNS}uj&r0VzbIAMpN{n87o{0n z?o8o(mDBZOz0rSW6t>w1XZR<@X-1e0=6>wE@XgVrpg8SoSKh;V2Sue$4l)YK=zhvK z4pe_1PPEYuG-IEV7O)gc{EWib%n?D(N!T?7gKUu%0{wDk{iWroTA>og9zLR;g*X9^ zz|y~O9gl<|`VlQF#3?uY5uGYj8hBmB>aS-s{%m&0l%f~;y**XhucWf93bcB^GQ}+& zdMFa>n1@q3-zl36wp8bU(xmzcXa2?4`z81IK`uex0Q1+%O3+Q9ZK;J9YEjLOmL5dggHsr zRjAj$YBBwBNU3CdVX!{2KG{d6LrS=9{kO09igaopMIKfviv1}5uoBq6#~VJH9KQG+ zrQ|4CXE?P$w-)hS8*m7}VekJ^pN?Zk>()9!!+fRs=Q4cP=_ta)=Txt%yhWAQR#UkXN*NY+a9q97e|EsL;42U9W8g^@$ncZ0tP;v%QF(N2tjEIYZIiRPQ z69x>J5JW|}L&SWRdge1=PH3xV&Ji#Wj2Lkc6>~s@uX~wXmuCA`GuCA^? z{7h9Tk&cT-z~6rqYOf0I!R{b-x=P#qN)>R_RiP#sBj8oioKzoyC$5rO?)Y%jU!x6+ z(Ht9GqbXK+0H<6NJmG11{OcNN2Wkz+f36A6?H{>(FIC?DPK5WJ){dhBRYLfrdOndZ zOR=j*5l{G{lI;JC04iTo6Dh@hCKc`oQ|t;C^tdjx1=o)_;yRtzlXAZwfS_Pc7EQ*)6dM%*L9N(%7(8P$fOT*WVQU2Hl`VqFfXSGy!~rSZV{5)53d>*LteR!h60K-=JGqng2zR z7FNO>`Y~PR`aKCIkmnAf5^KhTav|rFbpP)6goDw^qsP9j;f`HSe$Sc1h z2k^+L6lvmrd`(a$W21 z)pAQ~tdk>D(+`|Rmj}Blmev@aBeb&lL=5SqGjCjTSCe+Yrsz1P=Fk@P!)=nSv}9*N z+{~=h=HqRIJ3^O$#ROK#>JR#$l~~cu=enG87Kfs|+&M3@SAxFKeW8D-Py+ulGbc>; zyp4c?zePVZ>I(h1Q(_wRd=n&(e)u1a`U?(87TifbrBQ!OAztOIBz%2` zxXAK1*yyg%%+G!Yk1kZ!eV=V8Bl#LaB-=*lM@Ghcs>rzt$773HT+Z6^kwzlwJ-$z?77UZ8&a&!g}5$!23H zL=HfdU?PR&W*3a7A8U=t*1@GVcWJ>d7)ndn;GXIdwtpLuqIz!EnK&ew93=;|c#iGv z3ocIX>KbC>OFtoG*{e3NdIr^J_Z;Ky3&UZ{GgR)AUe)y(j(Z?%>-$=>ggP}gMxB`6m~#$*BO z>8nifraJSvcn#ei3ss!EzZA+%szdAR%zzO3EEm$uTGb{{O_%%jxbNRm(}BuwC>N!`>3ay{qj%L1>Ml-7Y3djtF^&SK72 zV$(DxRVbi*q>I7%F9^WZo|LPCnrj&4;skiHtViF7q9zfNOO>-TsrcjoB~LITlf-aj z&jiw*BtFqjU&f@Nug1c;6mfIY2UJ8C8cWGCXpHH>^PsTF@S8F*!A6(LG9L>4& z`7-)yLaH5$IhQCSVW&1%BHM>y&jO*rkTA+n5b>|1xta7RO&lf5q7UVCFVeJ(|5x-W z9zB4f8IRt`qq{0;(VW14)wY_aX+~+5@-#Dg;oAa|?Z#%J&eztB85@GL_Ue>A_;ll;WJpLR1a6h zD$?snoOQbwbP1%>HRnKn!eBGtA+~-kwCph96p2AUPnG0{91Yt{X!t==m2!<#qWLRK zfL8=CQ9#cy-jG}h)77&5BuW+xNfI7B!0WF`aNh6$HE#q5w;2@8|M;|~V}`3IIVbpR z;spK!A7JA*WQNt85}D+0-4wOkwF#gUcgG2Dgs`TLl!q48|FA!iU^HF-&vz0L(|cOn z)x|fTukyqtMovbdP^e-vfv+rcZ${OlY0{B@Ckef7Vv9mLaj$q2V+w`t6$)tepiY0F zmMZIW@L{3g?@EGVxem$Y<3bX>F3|>`pN{ptWEosaMm+&@Z=mm6!A3us(ok_zZeXXk zLXfQ+1w)z!=;+5{ZiH#Be$T)iZ-uH>51P|l@SqAzbHTYUK6xwnR@lb(RZ0Ha=|}08 zjUMmFmf}-3_I@Y4bRD10i}UY8Ka;r4br1m}KS^HHH|R8ZJ#UYTg+w+K|DS`R_bI z=^109&0IBy&DiVc{!s`HcA}utk^3H^yi}6G6zxnu`DD_|7lSrU)unsnQd+tzQ!|vp zvAnfpD;!6&pPWq4DAzwi9tZ?a+42b0L%Y01A=tJXg87* z9r-3irlaTr8!iJirPlHNamQ{Y*rk|O{BdFIc(HIzg39kP$xL)`*1e}(G(7{tTdCa) zTTk8e=G_ecfTzqv7mxIJ+<8SPzq1~&bRD5FLR8>4@=6(ID+TVo! z#I)~hJh6stnqHRe-O{1Y6*uJCLDz06enx9&&nGjja5 zyn+vFi%xzAE~)Whf*L=D$JgZXe_iHqVP`lmnvj=AIN-LG@t3e>9dVbY6z|j#n}S_c zwDl3gA*?!^>?2+#8?S1tm9I$Vp69DDeLYduv|8#~qH0fmy3|v)Inw(gM1;P6G`{_5 z$6ltK<-h7JMUu#^c^lm5xm%@nJ=VRx7*4m<&tR&*I7FN3S&I8&MH{T!Ks*a~Jn&Nk zad(|>jrg<)^a|ttbm)#zAeZWd4-HLFvQM!=TUxI-AP)xc@(86ye!MVkAV| z$NG)M$}n^f_HHchhkd)5c@y!X7N%NaR)E;wXUY+dZJUgagp%d-ZDA;IDR3e-uon`9 zdJ6=4rYg#fKdePh$|<$0vmwpJYFY?6#FATyRW+dZ#DlHHo?vy5nYR&LHPyPfbJ7o3 zx=P)mjtZtxb>|qpbD+eefa8uQdRAav14V=iO9>BDyK@lL{$wxv5+pVeVas+_FH~F% zP$>m3bQ05HtH7|cxLo5ptsG|s>LmwG#)Uv-wxrT|;0dq}UBwHabu3rHtb2&*A{f8n z^WVg&R%v8W$@halU6Y;s%EtB;muaB-7iJbNw$Z?|Pb|2sHa@!!66_jEjm4VKeya)JZKHA8Si<+>X{>gn z7^r~`Q*q)raRa1GVYcH%(n3w2jBO?mAZHThO%MmcraxHwiQ-ogqF-WQwCD*5vv5ST z=n7&C{t+$Kfvo9xI9hb6YB#Hd8xA+B%jd@zV^V!D-r+- zUXZ`-A(G|!3Pn@Kn0O~NQBj)T!$q;wB-eXvZ>;Dfz~d-(eYUtk0}EofG6X9q+t zsJsE?RIwjaY0XBaiiZHM2Qm*P#sIOSwMWE`aH<);IU)vn&D%~GJzuTFw7Vife=Z67 z)W-I=sf}a2DKe!R_B<+fwzZ{{#w0BnWq!>kTO8u7FO&)1c`k(8lE&F(hnokSLPtT$@mw7i|@D7D3w@gVKRM=kwrHn;@iFXJj#!|9Iniv} zU2zdWw~N^Hfp`v*FQDe3xYjbZgyxxQUC5V+-1E5Sp%`QN?#I8O-OgjvT(OfyMGhsF zM-20!GWQ3z`Ycw0N9S;NuDA^D7MsHU=dkx9@gmGG#ahqAPBvGId8T7tk5sB8bJa77 z?kpxe78`J94v&e@+n1Vh{&@z!Jr*0f#_%T5Hp`N75Z?N7VVx=+M)%c}Mlu-c-_H)kon7lRKb@MI4fVY z#vRYaPO!ho)cJQ`{=Zu4oWKEjG|de~_;(%^cSmgqN2*#}pD(83mK?|8JX$g*eZ$5t z#Hv=I|4o+m*_2E?&TF;7H7}@z=Z@hOI!A)#Urn`Ve)|7vuXqe&6tMvmeKB>i`&Sb_ z*ZqTqir5emzL+}MU2XWUR%L!PRcHPWukMt76gjRBNgnOh66-t@?deu*A&~_4I?<9M z)Y0Ms)Y%VeF3opb@=_cQl_%r(m!coUeL|lCvAWG|-aOj2B&T}uo8z1^JdWU)0%BGk zpD?*V^tE_H&ewky_gNYL-qeU~hmj00J3{Zn=>JM=4i1OO?U&dMo_#Qt+n6$O{`aqP z6Bz1Vi!Si{2dws5tY^{n!_RU#C1<|<**S)@UW>U9@*W4hp?0o#Z^F&fgSg`jp*QOt z$?24N!@GZF_9`@G?t9QQ5bX-Z4&eBXEh`icYG807c6dic?I~1?axdEdqcndo$)6#K zbJVjrwR6U#RMlgW@}2E}M~0p-_%(L;K!d;KwVI7QaIgMASb6^n%|DBdnDs%lfOe)h zSh5$3KhV;%;FYO$yX(I3SYnh$vNqc{o7e}dcY!QfA#H#9B4iJ!z# zns6NWQM9vLzMES4bj@Xb zH4@#8aec@Zf#jrdTzr@JX3}F=bPrtxsjAh8du9Dr z=TUx+splT+aEu`J)a0X!AUR>KAX!+PP>Uww=5kG|p9!a}MH^A-U>g)d&E;EpQ*#IK z=K6);WKn9ad5;H0$p~)mSg9!02Z!DnraD_HEpC`v`-mqWbd!bXr38T6L3rCBdBgS~ zEHOy7pnJt^%S(zGoEUq^N{R(&F&g74Nj+f2D9oxP`NGgqsJE8x)ai4XiiCN z?Dch}k6=82%YCJFnlISEPim$=x|bxdcZ#h)PWO{Wh`~Hq=83+Z)L(yf4*|#%eSM6m zC#{6)dr+t^t$}&FaZ`P%k-jsLTS1nx(+{85r_v0j;BY@y!(XzI;rK>u)k5-tV}G*= zEuE+Z^X^Lr9hyongzY3ahBCOD0I_Hb>&Ee z`_rdtLxPnFnx{C5cf4e?BcxeoR^EBVKSyLehdbnDu(=r`oJD=)DW6F;tJm%y{V`mvs-$k?Md$4BD-cYA}nrQkn)VCSh)Z z)Xerq6klzGi1Hi_eBVwYlu#Zn)b6N%A_rD)Ja;^)QEKhR+;JFrCZ(SYL^e6~~?0Eb7jpkH8nkR2v%P$c|i;E zTeMnQ4t7KE#cE3aVlXG3UEm##7uFD_Dx5g9R+2R^yBC&UC#6)#>7h0vCXgE7+>M=C zN9122yqgM)1xG=kv3iLyzQo-k{!mN&W4km5TJFILJEUG!vS~3hCbdWkva~q*$YFv7 z{dcs8f4rHNxZ)IgdE-!v{HPQ*Z-+$BMUW@6zjjJZ%&K)K0orGkWQ&D63EKtTu+lCX zgrnUsa2G8W5#4avF6!dBuGnNZVSQOwjMy!Wfht|`&TeTd#B{;gdx##Jb-^=xhz`xW z;JZCUU^$(!>RzceEbELBdnF{H5cWxR;Yug0zmG7K)Cni;(h+;^C-U~`i0S*K3Gh!SwmCoysdFgqK0pKcBm|vPrB1LogpEm+Y%~zv z0p}c~`Wke=O9!RXFtI%*9g;?XzCC_AB=v`sVC>1Ha4-hrAtt$kcQ9r#$peI7{Kkm7 za*gPAm^i>zBlbE>6K9|i*Bq9*g1Hgj9i|56wnMig#PJ#jV2>lx-w>68MMos2AsOzd zRG0Q(8`-my(jYU=`p!uUy=)t*BW*!12rJvku`@BWQ8%^aRMSGf8>CkjK48%~$=i9= zx*w%&eeV@yQj)9;?iat1UtJ}px3>W{KTmB;Zh!;NOLh>|0He=SUn(`gd*`Jp4zK*x za$i%XpXCy_Od~h^=x{-52GXC{>w;7Y%iUPmMX6W>@8#GtO&U&<{zRJ8)$gJ=&E1Mb zhtn_fT2*ar68Xd-{nE4oD6l3?F_#DjD0He_uArgPX$7`Um)e7N1zV6VJ=ORejpvC~ z!HLWNJ19t6DI=-SiGcaeIGPJ{#1GEUQ`8Cx6E~!1pgd&>Hzj}ajx+<0-=ZOmUy2oO zOO7ynDb~0x-G!tjC}v7K;lW~-nklu@#4UE?-6Y5PC;6m2QJ2q5oKB0gyGdqC;eZ4V z_^B4^%mI@Kpq_G{3Rfr6y;C&V=n=?r$_;lui_~{t@+e&3rT8r}L8#H4)#nBaEEQK1 zEF{tDi$b52BUi~;K>ZLGVN$lV54tVH<~huUbY&R!ze9|Evy%tSaT~4o3 zE3cgK^rPnyzT^f&lCbuD;)N}eu!zcg^o9G~P%!QeX*YriD?Z7#;(mr@m2 zV2k5k(puNF7!U9;S=irwDJ=!h7>q2CUV&yf>-S2Ut+5ChPUDpn`-rGbQCbXVZ(d8` zW-wqNc6={&fzXe*;k`5;;s!9E4@sm>aVM!fv&E2uYsx4 zqZI*^W?tB%jjk1m%h*PD1{zex-vV{#A<`2)+v-Mx#uFE{)uq9L^*A6%=Lml07!#y( zfL`m#0f?>x$m{W0kgf#eb@;5EZas`xiwQ>EIG15E5q#pLKh*oc<4DnABTxKF)rZbh z?ybQ}!MYZ(Y7KS|*1dpPYcZg`t|Oe*Vsd+34QP7^kF?j_fI)xZ{0_Pv;B*k5cF@&= zHQUiDMCSqX>{tBCD7>!P7xGoJ0Ni{ui27hN)p(&Cvex}#e6AKzJASDgUb zt@KLq@7;B7)$df~D2QF8{P;lrNUu_eU)G>^x?(fPY^)Cr3?rDbzamTTu1l36BV|1gnR#9_la+f*qFHy{%vFhV>^)!(F1j8p#F;m{N>q1x9BnqPer4-y)JzUHf8a~c2@;C)h{?WV$EN6(f^|PKR;8$_Rrzx$5BZ@^hZr_E zTDL%7>7R*2aC3>)i2}(FTRCOk1T>qY8)$zagRZDrtJh?>FvKWG&NcRCu-G}e6&~8` zZQs#;i|({TrER=-XH;X0hg1&H9p8&K-Hq?J=pt+u(JjTFop+!pUevK&Z2VT;P0?xa zCc1U|iD%SuQ(m8}O%19Asc|__Ve6Uwn85SO*#aY)I>L0>>`*dC5(Lvm|Pj^i6JXl<+xE#c& z{W?D>Ka~Q{QgPRQ-DDV@id7Ei-eBbex{9nys&0W9z514VSXX3bS-lC36&=pnj3JLL zic=Fjaa`9H=5Iscgl-%-Zo?@jbaAjU1;3uqMZo1h*^rYub2FIUh)q1JyW#?oHmt>C zqFksL$(BFWt<+XZvHTy_UNDBWexaic;o@n_-{@+?plRsyM&}PTr?FvgbV-6$o|ccL z&9y{s6n9a{rk5x_>h{Bw0qoF6U4#Y<{h8-yorjHfTi|yVWY7;3oZ8aWXKszl?O_OW zCUpQGiBW;YX!dp4>4x3r)9bFG~rG7ao7Ici5znzTqf)gM9%5kJ6i?Y^7n?c6_ zd~2_t4M&=?CTsQ=;GmaMEvAs3qX zg6!)4g{`DXr<4%_p6{e@2kWgfhQV|}R1nT0hb>1V={N4Rqmb-a8o^O&qZZ3$N**|P-wcT1=1{i%Z^u8>bL{$^M< znLDeR$}A-{oE4!yMF)wgY}0x@J(-@p0f(pRTfp4)n3Af$0!P-dsDt{~6xvl?O19jb5c$WSZKx-Ekaz`(kd)ygY^smEBezaNkEY;l4(*)&eBu>oJ z$AkY^_BBsWhwb_?=%wh-!PXhgtXn4GPYIQ=)hqpD(2qc`*ZM#RlriG9 zzB|0p3X_Xj%~$;_*wz>4eAVxPzGIpHH+=v-g)#=Ciu7Y4xDR_)q`#zxr-RsT zGr7B^>-Hx_rAph-pIA&?z_D0HY6~7ScN=-0l@r{cL~F>zK^?C0v1%fIr6NC_ov$jd z)yV?a^P$P4v z{5#V30*kFH7YOjI4vT0YkEGl3Cs<}fne2kIj=n$p3S+STrSUs80CHf6r|wUQ29{P+Ga(+WL5PD)0qGyNE6#3Jmo1%L}~~LsLB5C zdU2$bDjpO=wyF>8S!zdl6dj~%(WA562$n9#-#W`HA?yeH+*xj`0jpvb&{d|Rpu;-W zvzz=;gpC_8q>t=Zx%V3NxOWe6e^M@S#Zx;I9i8X?O|7+Td&P1r!WzXn2=v4Ml+DVplHm#R3v-jKHl#*l{ws04u^>w(K=^oEG)Z0r^Jk*xWGY2@OQk%0i)$y#Q(^gi zw4Wl^hXMCl$0_n$4dh2+=2STaT12wAX|jg~YCU8dqj@>nJNQqG>>|}@-~zW>AwL;oIBt&9nZ4=8m=cqXt!Ky+LCQupLw1E1S(rUT zZVp?s&@N7%0b8dc#>w5_W+uLhqxk)qq|>AL37ITpCT~glWQ?38`+7LvHdWL>H8%41 zJ@7vj6_XDJ+1>NE$);qVOhycB| zBy!@w+kF$WXUidOD{q?eo#mUXWdGg!c)r-ZCOeCmo7g&DUTAS|;;#wyPzk+(kK*N- z@aigFyeNC)ygBj$*fba&=gObq{s4OaR^9^V9?*lga&0(24)YV_7^v|GhyPBz!)qKp zcq#FqE6p z18RNQpMS|C^^me_`DWs#OS_;G{n}3LQdFA1x@=`XF_Y*}3Eze7H_J|8tShmuvBW|T zmD0U6e!o$*H&T2k554H5YR}zy2K!y#N(bzFD?+Z%3v7qYy*AnFajZ=`I_QjhuHyru6WZ83ANJm2!4J=8GXjv8d@ zsJOJn@%v;$`{ZembdZVrp3YRrt?Bw8sXxTlx1Qg zw&SEc8=%l1ea^_IeQWuvtSvHL%`o-}WvIq8SexL*vsL)##{~E0@M7k1R(=U!<%i}M zmw!Gw#%rGNi}PlnS_-<{37#Fv8C z^!EH^xuIPu+4&Hm|0I^I%A};QQYNfHw=2YrdauDoSBT0duVlKbGKns~d9!Y5)E{V7 z6~|=ARqY(A@phO$xz1HNbmLLo8ktqa?HO_dm;1!d%gR2ilsKCRaoC^KpJmJOM+VIs z=jCkD4Y{KRE>~vfZ^~me(AyLJZ_|2`xd^A-me*LF_ADd)iX0KMVzF8#G2(*@ab+ei zAP%jv=&kjAGgwR(Uw5ld$GB|Sg-$ouWXo58yg!Z1Aq1GY;r1Njr)jS2Q4YyOP|=00 zzAL{KK&;0WJd%%?k>e;9{9In9g_E@~SCOxQRVmw*FV_xQAFvQv-LuSDFV2x$j4|C%I< z4`ul84Cz)jLpD)GZ$DG$KQp8&0n`~ng(x>Z}+{MI0>nF-%Om&aXFRLFOT_J8ro@L(=&{&WVqmW z|0!4K1i1OBgyy=KictymoC&_uSA}m`L~xZ*cRpnktPOr-pGLRc>&^6QYjN)Cj6c9i_CY^xlS{F5It* zPx$Zls%YKZa1{F4Vn%aAZm9QR8Z5g1ViNket}?=gAV`qV#Jpeo_Z}QLyDS;!Ky5gL z>|x6?<|)25thj}N9{B2h2>Y}$RD+s_SbQtPKU(Ox4_mf1xPZ?-?BCWf6s~&Ug|>#f zQ0UG!1{r2*oW{5n(H`#{E#{B-KK!4}#iSDaQedZacf$Z9<@9&MzD7eAc;bqCjD|rl z!M=nvuF+1sv$zqQVZvN~;PKl)0=q`p02Qhi$_-5mGyG<<aUpI>2zP(tb-m`(qEQGDk?_H|53`{4v0= z80vh)MFR~^mEKvX2~vI~u>8VK3^X(ZE4D;chyD@7v6HUcTFS&BhFFblcN4rd!G9Sc cwxp+0hZ=fm@zF@bRD3(a@Ro%|7zTv=KjAD#jsO4v delta 41882 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 diff --git a/src/app.rs b/src/app.rs index 190e43b..f85d8c8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::zfs::{self, ZfsEntry, ZfsSnapshot}; use anyhow::Result; use std::collections::HashSet; @@ -37,6 +38,13 @@ impl ListItem { pub fn is_dataset(&self) -> bool { matches!(self, ListItem::Dataset(_)) } + + pub fn creation(&self) -> &str { + match self { + ListItem::Dataset(d) => &d.creation, + ListItem::Snapshot(s) => &s.creation, + } + } } #[derive(Debug, Clone)] @@ -56,6 +64,31 @@ pub enum InputMode { Filter, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SortMode { + Size, + Date, + Name, +} + +impl SortMode { + pub fn next(self) -> Self { + match self { + SortMode::Size => SortMode::Date, + SortMode::Date => SortMode::Name, + SortMode::Name => SortMode::Size, + } + } + + pub fn label(&self) -> &'static str { + match self { + SortMode::Size => "Size", + SortMode::Date => "Date", + SortMode::Name => "Name", + } + } +} + pub struct App { pub root_entries: Vec, pub tree_items: Vec, @@ -73,13 +106,25 @@ pub struct App { pub filter_query: String, pub search_matches: Vec, pub current_match: usize, + // Sort mode + pub sort_mode: SortMode, + // Dry-run mode + pub dry_run_mode: bool, + // Auto-refresh + pub auto_refresh: bool, + pub last_refresh: std::time::Instant, + pub auto_refresh_interval: u64, + // Config + pub config: Config, } impl App { pub fn new() -> Result { + let config = Config::load(); let root_entries = zfs::get_zfs_tree()?; let expanded = HashSet::new(); let tree_items = Self::build_tree_view(&root_entries, 0, &[], &expanded, ""); + let auto_refresh_interval = config.auto_refresh_interval; Ok(Self { root_entries, @@ -97,6 +142,12 @@ impl App { filter_query: String::new(), search_matches: Vec::new(), current_match: 0, + sort_mode: SortMode::Size, + dry_run_mode: false, + auto_refresh: false, + last_refresh: std::time::Instant::now(), + auto_refresh_interval, + config, }) } @@ -166,7 +217,9 @@ impl App { pub fn refresh(&mut self) -> Result<()> { self.root_entries = zfs::get_zfs_tree()?; + self.resort_entries(); self.rebuild_tree(); + self.last_refresh = std::time::Instant::now(); self.message = Some("Refreshed".to_string()); Ok(()) } @@ -306,11 +359,19 @@ impl App { return Ok(()); } + // In dry-run mode, just show what would happen + if self.dry_run_mode { + self.show_dry_run_preview(); + return Ok(()); + } + if !self.confirm_delete { self.confirm_delete = true; + let total_size = self.total_marked_size(); self.message = Some(format!( - "Delete {} snapshot(s)? Press 'd' again to confirm", - self.marked.len() + "Delete {} snapshot(s) (~{})? Press 'd' to confirm", + self.marked.len(), + zfs::format_size(total_size) )); return Ok(()); } @@ -561,4 +622,89 @@ impl App { self.rebuild_tree(); self.message = Some("Collapsed all".to_string()); } + + // Toggle sort mode + pub fn toggle_sort(&mut self) { + self.sort_mode = self.sort_mode.next(); + self.resort_entries(); + self.rebuild_tree(); + self.message = Some(format!("Sort by: {}", self.sort_mode.label())); + } + + fn resort_entries(&mut self) { + Self::sort_entries_recursive(&mut self.root_entries, self.sort_mode); + } + + fn sort_entries_recursive(entries: &mut [ZfsEntry], mode: SortMode) { + match mode { + SortMode::Size => entries.sort_by(|a, b| b.used.cmp(&a.used)), + SortMode::Date => entries.sort_by(|a, b| b.creation.cmp(&a.creation)), + SortMode::Name => entries.sort_by(|a, b| a.full_name.cmp(&b.full_name)), + } + + for entry in entries.iter_mut() { + Self::sort_entries_recursive(&mut entry.children, mode); + // Also sort snapshots + match mode { + SortMode::Size => entry.snapshots.sort_by(|a, b| b.used.cmp(&a.used)), + SortMode::Date => entry.snapshots.sort_by(|a, b| b.creation.cmp(&a.creation)), + SortMode::Name => entry.snapshots.sort_by(|a, b| a.full_name.cmp(&b.full_name)), + } + } + } + + // Toggle dry-run mode + pub fn toggle_dry_run(&mut self) { + self.dry_run_mode = !self.dry_run_mode; + if self.dry_run_mode { + self.message = Some("Dry-run mode ON".to_string()); + } else { + self.message = Some("Dry-run mode OFF".to_string()); + } + } + + // Show dry-run preview + pub fn show_dry_run_preview(&mut self) { + if self.marked.is_empty() { + self.message = Some("No snapshots marked".to_string()); + return; + } + + let total_size = self.total_marked_size(); + self.message = Some(format!( + "Would delete {} snapshot(s), ~{} freed", + self.marked.len(), + zfs::format_size(total_size) + )); + } + + // Toggle auto-refresh + pub fn toggle_auto_refresh(&mut self) { + self.auto_refresh = !self.auto_refresh; + if self.auto_refresh { + self.message = Some("Auto-refresh ON (30s)".to_string()); + } else { + self.message = Some("Auto-refresh OFF".to_string()); + } + } + + // Check if auto-refresh is due + pub fn check_auto_refresh(&mut self) -> Result<()> { + if self.auto_refresh && self.last_refresh.elapsed() >= std::time::Duration::from_secs(self.auto_refresh_interval) { + self.refresh()?; + } + Ok(()) + } + + // Get snapshot by name + pub fn get_snapshot_date(&self, name: &str) -> Option<&str> { + for ti in &self.tree_items { + if let ListItem::Snapshot(s) = &ti.item { + if s.full_name == name { + return Some(&s.creation); + } + } + } + None + } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a7236d7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,119 @@ +use ratatui::style::Color; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct Config { + pub colors: ColorConfig, + pub auto_refresh_interval: u64, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct ColorConfig { + pub background: String, + pub foreground: String, + pub header: String, + pub selected_bg: String, + pub selected_fg: String, + pub marked: String, + pub snapshot: String, + pub size: String, + pub tree: String, + pub search_match: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + colors: ColorConfig::default(), + auto_refresh_interval: 30, + } + } +} + +impl Default for ColorConfig { + fn default() -> Self { + Self { + background: "blue".to_string(), + foreground: "white".to_string(), + header: "yellow".to_string(), + selected_bg: "cyan".to_string(), + selected_fg: "black".to_string(), + marked: "yellow".to_string(), + snapshot: "lightcyan".to_string(), + size: "lightgreen".to_string(), + tree: "darkgray".to_string(), + search_match: "lightred".to_string(), + } + } +} + +impl Config { + pub fn load() -> Self { + // Try to load from ~/.config/zfsdu/config.toml + if let Some(config_path) = Self::config_path() { + if config_path.exists() { + if let Ok(contents) = fs::read_to_string(&config_path) { + if let Ok(config) = toml::from_str(&contents) { + return config; + } + } + } + } + Self::default() + } + + fn config_path() -> Option { + dirs::config_dir().map(|p| p.join("zfsdu").join("config.toml")) + } + + pub fn example_config() -> String { + r#"# zfsdu configuration file +# Place this file at ~/.config/zfsdu/config.toml + +# Auto-refresh interval in seconds (0 to disable) +auto_refresh_interval = 30 + +[colors] +# Available colors: black, red, green, yellow, blue, magenta, cyan, white +# Light variants: lightred, lightgreen, lightyellow, lightblue, lightmagenta, lightcyan, gray +# Also: darkgray + +background = "blue" +foreground = "white" +header = "yellow" +selected_bg = "cyan" +selected_fg = "black" +marked = "yellow" +snapshot = "lightcyan" +size = "lightgreen" +tree = "darkgray" +search_match = "lightred" +"#.to_string() + } +} + +pub fn parse_color(name: &str) -> Color { + match name.to_lowercase().as_str() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "magenta" => Color::Magenta, + "cyan" => Color::Cyan, + "white" => Color::White, + "gray" | "grey" => Color::Gray, + "darkgray" | "darkgrey" => Color::DarkGray, + "lightred" => Color::LightRed, + "lightgreen" => Color::LightGreen, + "lightyellow" => Color::LightYellow, + "lightblue" => Color::LightBlue, + "lightmagenta" => Color::LightMagenta, + "lightcyan" => Color::LightCyan, + _ => Color::White, + } +} diff --git a/src/main.rs b/src/main.rs index 59c08f9..aeddd75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,16 @@ mod app; +mod config; mod ui; mod zfs; use anyhow::Result; use app::{App, InputMode}; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind, MouseButton}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use std::time::Duration; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; @@ -41,64 +43,132 @@ fn run_app(terminal: &mut Terminal, app: &mut A loop { terminal.draw(|f| ui::draw(f, app))?; - if let Event::Key(key) = event::read()? { - // Handle input mode - if app.input_mode != InputMode::Normal { + // Check auto-refresh + app.check_auto_refresh()?; + + // Poll with timeout for auto-refresh + if !event::poll(Duration::from_millis(100))? { + continue; + } + + match event::read()? { + Event::Key(key) => { + // Handle input mode + if app.input_mode != InputMode::Normal { + match key.code { + KeyCode::Esc => app.cancel_input(), + KeyCode::Enter => app.confirm_input(), + KeyCode::Backspace => app.input_backspace(), + KeyCode::Char(c) => app.input_char(c), + _ => {} + } + continue; + } + + // Close help on any key + if app.show_help { + app.show_help = false; + continue; + } + + // Quit on q or Ctrl+C + if key.code == KeyCode::Char('q') || key.code == KeyCode::F(10) { + return Ok(()); + } + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(()); + } + match key.code { - KeyCode::Esc => app.cancel_input(), - KeyCode::Enter => app.confirm_input(), - KeyCode::Backspace => app.input_backspace(), - KeyCode::Char(c) => app.input_char(c), + // Navigation + KeyCode::Up | KeyCode::Char('k') => app.previous(), + KeyCode::Down | KeyCode::Char('j') => app.next(), + KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => app.toggle_expand(), + KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') => app.toggle_expand(), + KeyCode::Home => app.first(), + KeyCode::End => app.last(), + KeyCode::PageUp => app.page_up(), + KeyCode::PageDown => app.page_down(), + + // Expand/Collapse all + KeyCode::Char('e') => app.expand_all(), + KeyCode::Char('c') => app.collapse_all(), + + // Selection + KeyCode::Char(' ') => app.toggle_mark(), + KeyCode::Char('+') => app.select_group(), + KeyCode::Char('-') => app.unselect_group(), + KeyCode::Char('*') => app.invert_selection(), + + // Search & Filter + KeyCode::Char('/') => app.start_search(), + KeyCode::Char('f') => app.start_filter(), + KeyCode::Char('n') => app.next_match(), + KeyCode::Char('N') => app.prev_match(), + KeyCode::Esc => app.clear_filter(), + + // Sort + KeyCode::Char('s') => app.toggle_sort(), + + // Dry-run mode + KeyCode::Char('D') => app.toggle_dry_run(), + + // Auto-refresh + KeyCode::Char('a') => app.toggle_auto_refresh(), + + // Actions + KeyCode::Char('d') | KeyCode::F(8) => app.delete_marked()?, + KeyCode::Char('r') | KeyCode::F(5) => app.refresh()?, + + // Help + KeyCode::F(1) | KeyCode::Char('?') => app.toggle_help(), + _ => {} } - continue; } - - // Quit on q or Ctrl+C - if key.code == KeyCode::Char('q') || key.code == KeyCode::F(10) { - return Ok(()); - } - if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { - return Ok(()); - } - - match key.code { - // Navigation - KeyCode::Up | KeyCode::Char('k') => app.previous(), - KeyCode::Down | KeyCode::Char('j') => app.next(), - KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => app.toggle_expand(), - KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') => app.toggle_expand(), // Same as Enter for tree - KeyCode::Home => app.first(), - KeyCode::End => app.last(), - KeyCode::PageUp => app.page_up(), - KeyCode::PageDown => app.page_down(), - - // Expand/Collapse all - KeyCode::Char('e') => app.expand_all(), - KeyCode::Char('c') => app.collapse_all(), - - // Selection - KeyCode::Char(' ') => app.toggle_mark(), - KeyCode::Char('+') => app.select_group(), - KeyCode::Char('-') => app.unselect_group(), - KeyCode::Char('*') => app.invert_selection(), - - // Search & Filter - KeyCode::Char('/') => app.start_search(), - KeyCode::Char('f') => app.start_filter(), - KeyCode::Char('n') => app.next_match(), - KeyCode::Char('N') => app.prev_match(), - KeyCode::Esc => app.clear_filter(), - - // Actions - KeyCode::Char('d') | KeyCode::F(8) => app.delete_marked()?, - KeyCode::Char('r') | KeyCode::F(5) => app.refresh()?, - - // Help - KeyCode::F(1) => app.toggle_help(), - - _ => {} + Event::Mouse(mouse) => { + match mouse.kind { + MouseEventKind::Down(MouseButton::Left) => { + // Calculate which item was clicked (accounting for header and borders) + let row = mouse.row as usize; + if row >= 4 { // Header area is 4 rows (title + border + header + separator) + let clicked_index = row - 4 + app.scroll_offset; + let visible_count = app.visible_count(); + if clicked_index < visible_count { + app.selected = clicked_index; + } + } + } + MouseEventKind::Down(MouseButton::Right) => { + // Right-click toggles mark (like spacebar) + let row = mouse.row as usize; + if row >= 4 { + let clicked_index = row - 4 + app.scroll_offset; + let visible_count = app.visible_count(); + if clicked_index < visible_count { + app.selected = clicked_index; + app.toggle_mark(); + } + } + } + MouseEventKind::ScrollUp => { + for _ in 0..3 { + app.previous(); + } + } + MouseEventKind::ScrollDown => { + for _ in 0..3 { + app.next(); + } + } + MouseEventKind::Down(MouseButton::Middle) => { + // Middle-click toggles expand + app.toggle_expand(); + } + _ => {} + } } + _ => {} } } } diff --git a/src/ui.rs b/src/ui.rs index 97bbaf6..786e243 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,5 @@ -use crate::app::{App, InputMode, ListItem, TreeItem}; +use crate::app::{App, InputMode, ListItem, TreeItem, SortMode}; +use crate::config::parse_color; use crate::zfs::format_size; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, @@ -8,17 +9,22 @@ use ratatui::{ Frame, }; -// MC-style colors -const BG_COLOR: Color = Color::Blue; -const FG_COLOR: Color = Color::White; -const HEADER_COLOR: Color = Color::Yellow; -const SELECTED_BG: Color = Color::Cyan; -const SELECTED_FG: Color = Color::Black; -const MARKED_COLOR: Color = Color::Yellow; -const SNAPSHOT_COLOR: Color = Color::LightCyan; -const SIZE_COLOR: Color = Color::LightGreen; -const TREE_COLOR: Color = Color::DarkGray; -const SEARCH_MATCH_COLOR: Color = Color::LightRed; +// Helper to get colors from app config +fn get_colors(app: &App) -> (Color, Color, Color, Color, Color, Color, Color, Color, Color, Color) { + let c = &app.config.colors; + ( + parse_color(&c.background), + parse_color(&c.foreground), + parse_color(&c.header), + parse_color(&c.selected_bg), + parse_color(&c.selected_fg), + parse_color(&c.marked), + parse_color(&c.snapshot), + parse_color(&c.size), + parse_color(&c.tree), + parse_color(&c.search_match), + ) +} pub fn draw(f: &mut Frame, app: &mut App) { let chunks = Layout::default() @@ -38,63 +44,85 @@ pub fn draw(f: &mut Frame, app: &mut App) { if app.input_mode != InputMode::Normal { draw_input(f, chunks[3], app); } else { - draw_function_keys(f, chunks[3]); + draw_function_keys(f, chunks[3], app); } if app.show_help { - draw_help(f); + draw_help(f, app); } } fn draw_title(f: &mut Frame, area: Rect, app: &App) { + let (bg, _fg, header, _, _, _, _, _, _, _) = get_colors(app); + let mut title = " zfsdu ".to_string(); + // Show sort mode + title.push_str(&format!("[Sort: {}] ", app.sort_mode.label())); + if !app.filter_query.is_empty() { title.push_str(&format!("[Filter: {}] ", app.filter_query)); } + if app.dry_run_mode { + title.push_str("[DRY-RUN] "); + } + + if app.auto_refresh { + title.push_str("[AUTO] "); + } + let title_bar = Paragraph::new(title).style( Style::default() - .bg(BG_COLOR) - .fg(HEADER_COLOR) + .bg(bg) + .fg(header) .add_modifier(Modifier::BOLD), ); f.render_widget(title_bar, area); } fn draw_main(f: &mut Frame, area: Rect, app: &mut App) { + let (bg, fg, header_color, _, _, _, _, _, _, _) = get_colors(app); + let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(FG_COLOR).bg(BG_COLOR)) - .style(Style::default().bg(BG_COLOR)); + .border_style(Style::default().fg(fg).bg(bg)) + .style(Style::default().bg(bg)); let inner = block.inner(area); f.render_widget(block, area); - // Header - dynamic width + // Header - dynamic width with date column let header_area = Rect { x: inner.x, y: inner.y, width: inner.width, height: 1, }; - let header_name_width = (inner.width as usize).saturating_sub(22); // 10 size + 10 bar area + 2 padding + // Layout: name + date(12) + size(10) + bar(8) + spacing + let header_name_width = (inner.width as usize).saturating_sub(34); let header = Line::from(vec![ Span::styled( format!(" {:12}", "Date"), + Style::default() + .fg(header_color) .add_modifier(Modifier::BOLD), ), Span::styled( format!("{:>10}", "Size"), Style::default() - .fg(HEADER_COLOR) + .fg(header_color) .add_modifier(Modifier::BOLD), ), ]); f.render_widget( - Paragraph::new(header).style(Style::default().bg(BG_COLOR)), + Paragraph::new(header).style(Style::default().bg(bg)), header_area, ); @@ -107,7 +135,7 @@ fn draw_main(f: &mut Frame, area: Rect, app: &mut App) { }; let separator = "─".repeat(inner.width as usize); f.render_widget( - Paragraph::new(separator).style(Style::default().fg(FG_COLOR).bg(BG_COLOR)), + Paragraph::new(separator).style(Style::default().fg(fg).bg(bg)), sep_area, ); @@ -143,16 +171,18 @@ fn draw_main(f: &mut Frame, area: Rect, app: &mut App) { }) .collect(); - let list = List::new(items).style(Style::default().bg(BG_COLOR)); + let list = List::new(items).style(Style::default().bg(bg)); f.render_widget(list, list_area); // Draw scrollbar indicator if needed if total_visible > list_area.height as usize { - draw_scrollbar(f, list_area, app.scroll_offset, total_visible, list_area.height as usize); + draw_scrollbar(f, list_area, app.scroll_offset, total_visible, list_area.height as usize, bg); } } fn render_tree_item(app: &App, visible_index: usize, real_index: usize, tree_item: &TreeItem, available_width: usize) -> RatatuiListItem<'static> { + let (bg, fg, _, selected_bg, selected_fg, marked_color, snapshot_color, size_color, tree_color, search_match_color) = get_colors(app); + let is_selected = visible_index == app.selected; let is_marked = app.is_marked(tree_item.item.full_name()); let is_search_match = app.is_search_match(real_index); @@ -186,61 +216,68 @@ fn render_tree_item(app: &App, visible_index: usize, real_index: usize, tree_ite } else { " " }; - (icon, FG_COLOR) + (icon, fg) } ListItem::Snapshot(_) => { let icon = if is_marked { "● " } else { "○ " }; - (icon, SNAPSHOT_COLOR) + (icon, snapshot_color) } }; // Calculate available width for name dynamically - // Layout: prefix + icon(2) + name + size(10) + space(1) + bar(8) + scrollbar(2) + // Layout: prefix + icon(2) + name + date(12) + size(10) + space(1) + bar(8) + scrollbar(2) let prefix_len = tree_prefix.chars().count(); - let fixed_width = prefix_len + 2 + 10 + 1 + 8 + 2; // prefix + icon + size + space + bar + scrollbar + let fixed_width = prefix_len + 2 + 12 + 10 + 1 + 8 + 2; // prefix + icon + date + size + space + bar + scrollbar let max_name_len = available_width.saturating_sub(fixed_width).max(20); let name = truncate_str(tree_item.item.name(), max_name_len); let name_padded = format!("{:10}", format_size(tree_item.item.used())); let bar = create_size_bar(tree_item.item.used(), app.max_size()); // Determine styles - let tree_style = Style::default().fg(TREE_COLOR).bg(BG_COLOR); + let tree_style = Style::default().fg(tree_color).bg(bg); let mut item_style = if is_marked { Style::default() - .fg(MARKED_COLOR) - .bg(BG_COLOR) + .fg(marked_color) + .bg(bg) .add_modifier(Modifier::BOLD) } else if is_search_match { Style::default() - .fg(SEARCH_MATCH_COLOR) - .bg(BG_COLOR) + .fg(search_match_color) + .bg(bg) } else { - Style::default().fg(name_color).bg(BG_COLOR) + Style::default().fg(name_color).bg(bg) }; - let mut size_style = Style::default().fg(SIZE_COLOR).bg(BG_COLOR); + let mut size_style = Style::default().fg(size_color).bg(bg); + let mut date_style = Style::default().fg(Color::Gray).bg(bg); if is_selected { - item_style = item_style.bg(SELECTED_BG).fg(SELECTED_FG); - size_style = size_style.bg(SELECTED_BG).fg(SELECTED_FG); + item_style = item_style.bg(selected_bg).fg(selected_fg); + size_style = size_style.bg(selected_bg).fg(selected_fg); + date_style = date_style.bg(selected_bg).fg(selected_fg); } let line = Line::from(vec![ Span::styled(tree_prefix, if is_selected { item_style } else { tree_style }), Span::styled(icon.to_string(), item_style), Span::styled(name_padded, item_style), + Span::styled(format!("{:>12}", date_str), date_style), Span::styled(size, size_style), Span::raw(" "), Span::styled(bar, size_style), ]); - RatatuiListItem::new(line).style(Style::default().bg(BG_COLOR)) + RatatuiListItem::new(line).style(Style::default().bg(bg)) } -fn draw_scrollbar(f: &mut Frame, area: Rect, offset: usize, total: usize, visible: usize) { +fn draw_scrollbar(f: &mut Frame, area: Rect, offset: usize, total: usize, visible: usize, bg: Color) { if total == 0 || visible >= total { return; } @@ -262,7 +299,7 @@ fn draw_scrollbar(f: &mut Frame, area: Rect, offset: usize, total: usize, visibl height: 1, }; f.render_widget( - Paragraph::new(char).style(Style::default().fg(Color::DarkGray).bg(BG_COLOR)), + Paragraph::new(char).style(Style::default().fg(Color::DarkGray).bg(bg)), scroll_area, ); } @@ -313,15 +350,16 @@ fn draw_input(f: &mut Frame, area: Rect, app: &App) { f.render_widget(input, area); } -fn draw_function_keys(f: &mut Frame, area: Rect) { +fn draw_function_keys(f: &mut Frame, area: Rect, app: &App) { let keys = vec![ - ("F1", "Help"), - ("F5", "Refr"), - ("F8", "Del"), - ("/", "Search"), - ("f", "Filter"), - ("e", "Expand"), - ("c", "Collapse"), + ("?", "Help"), + ("s", "Sort"), + ("D", if app.dry_run_mode { "DRY!" } else { "Dry" }), + ("a", if app.auto_refresh { "Auto!" } else { "Auto" }), + ("/", "Find"), + ("f", "Filt"), + ("d", "Del"), + ("q", "Quit"), ]; let spans: Vec = keys @@ -345,8 +383,10 @@ fn draw_function_keys(f: &mut Frame, area: Rect) { f.render_widget(keys_bar, area); } -fn draw_help(f: &mut Frame) { - let area = centered_rect(65, 85, f.area()); +fn draw_help(f: &mut Frame, app: &App) { + let (bg, fg, header, _, _, _, _, _, _, _) = get_colors(app); + + let area = centered_rect(70, 90, f.area()); f.render_widget(Clear, area); @@ -357,6 +397,9 @@ fn draw_help(f: &mut Frame) { " Enter/→/l Expand/collapse dataset", " Home/End First/last item", " PgUp/PgDn Page up/down", + " Mouse scroll Scroll list", + " Left-click Select item", + " Right-click Toggle mark", "", " Tree:", " e Expand all", @@ -373,22 +416,30 @@ fn draw_help(f: &mut Frame) { " f Filter visible items", " Esc Clear filter", "", + " Sort & View:", + " s Toggle sort: Size/Date/Name", + " D Toggle dry-run mode", + " a Toggle auto-refresh (30s)", + "", " Actions:", " d / F8 Delete marked snapshots", " r / F5 Refresh", + " ? / F1 This help", " q / F10 Quit", "", + " Config: ~/.config/zfsdu/config.toml", + "", " Press any key to close", ]; let help = Paragraph::new(help_text.join("\n")) .block( Block::default() - .title(" Help ") + .title(" Help - zfsdu ") .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)), + .border_style(Style::default().fg(header)), ) - .style(Style::default().bg(BG_COLOR).fg(FG_COLOR)); + .style(Style::default().bg(bg).fg(fg)); f.render_widget(help, area); } @@ -433,3 +484,39 @@ fn create_size_bar(size: u64, max_size: u64) -> String { format!("{}{}", "█".repeat(filled), " ".repeat(bar_width - filled)) } + +fn format_date(timestamp: &str) -> String { + // ZFS returns timestamp as Unix epoch seconds + if let Ok(ts) = timestamp.parse::() { + use std::time::{UNIX_EPOCH, Duration}; + if let Some(datetime) = UNIX_EPOCH.checked_add(Duration::from_secs(ts as u64)) { + // Format as YYYY-MM-DD + let secs = datetime.duration_since(UNIX_EPOCH).unwrap().as_secs(); + let days = secs / 86400; + let years_since_1970 = days / 365; + let year = 1970 + years_since_1970; + + // Simplified date calculation + let remaining_days = days - (years_since_1970 * 365) - (years_since_1970 / 4); + let month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut month = 1; + let mut day = remaining_days; + + for (i, &m_days) in month_days.iter().enumerate() { + if day < m_days as u64 { + month = i + 1; + break; + } + day -= m_days as u64; + } + + return format!("{:04}-{:02}-{:02}", year, month, day + 1); + } + } + // Return original if parsing fails + if timestamp.len() > 10 { + timestamp[..10].to_string() + } else { + timestamp.to_string() + } +}