From e08af727a7939ca9effea9722cf382226f318057 Mon Sep 17 00:00:00 2001 From: John Osumi <931193+sumitake@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:53:08 -0700 Subject: [PATCH] display: refine calendar and CI layouts and Nyan animation --- README.md | 9 + assets/nyan/frames/frame_0.png | Bin 381 -> 604 bytes assets/nyan/frames/frame_1.png | Bin 368 -> 617 bytes assets/nyan/frames/frame_10.png | Bin 344 -> 596 bytes assets/nyan/frames/frame_11.png | Bin 358 -> 591 bytes assets/nyan/frames/frame_12.png | Bin 375 -> 630 bytes assets/nyan/frames/frame_13.png | Bin 364 -> 612 bytes assets/nyan/frames/frame_14.png | Bin 374 -> 606 bytes assets/nyan/frames/frame_15.png | Bin 335 -> 565 bytes assets/nyan/frames/frame_16.png | Bin 342 -> 567 bytes assets/nyan/frames/frame_17.png | Bin 339 -> 575 bytes assets/nyan/frames/frame_18.png | Bin 368 -> 613 bytes assets/nyan/frames/frame_19.png | Bin 380 -> 594 bytes assets/nyan/frames/frame_2.png | Bin 374 -> 592 bytes assets/nyan/frames/frame_20.png | Bin 372 -> 597 bytes assets/nyan/frames/frame_21.png | Bin 329 -> 581 bytes assets/nyan/frames/frame_22.png | Bin 329 -> 591 bytes assets/nyan/frames/frame_23.png | Bin 329 -> 579 bytes assets/nyan/frames/frame_3.png | Bin 349 -> 576 bytes assets/nyan/frames/frame_4.png | Bin 350 -> 584 bytes assets/nyan/frames/frame_5.png | Bin 346 -> 586 bytes assets/nyan/frames/frame_6.png | Bin 383 -> 602 bytes assets/nyan/frames/frame_7.png | Bin 380 -> 591 bytes assets/nyan/frames/frame_8.png | Bin 372 -> 597 bytes assets/nyan/frames/frame_9.png | Bin 333 -> 605 bytes assets/nyan/nyan_72x16.anim | Bin 76177 -> 83097 bytes docs/display-design.md | 84 ++++++ docs/images/signal-calendar.png | Bin 0 -> 22194 bytes docs/images/signal-ci.png | Bin 0 -> 18750 bytes docs/images/signal-nyan.gif | Bin 0 -> 89120 bytes docs/images/signal-overview.png | Bin 0 -> 23534 bytes integrations/calendar_countdown/README.md | 10 +- integrations/calendar_countdown/logic.py | 318 ++++++---------------- integrations/ci_status/README.md | 30 +- integrations/ci_status/logic.py | 129 +++++---- integrations/ci_status/main.py | 5 +- src/busybar/presentation.py | 8 +- tests/test_calendar_logic.py | 82 +++--- tests/test_calendar_loop.py | 16 +- tests/test_ci_logic.py | 78 ++++-- tests/test_ci_loop.py | 41 +-- tests/test_nyan_asset.py | 23 +- tests/test_presentation.py | 17 +- tools/build_nyan_anim.py | 63 +++-- 44 files changed, 490 insertions(+), 423 deletions(-) create mode 100644 docs/display-design.md create mode 100644 docs/images/signal-calendar.png create mode 100644 docs/images/signal-ci.png create mode 100644 docs/images/signal-nyan.gif create mode 100644 docs/images/signal-overview.png diff --git a/README.md b/README.md index 2efb7de..d836857 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,15 @@ Local-API integrations for the BUSY Bar — a 72×16 LED status display on USB or LAN. +## Display design + +The **Signal** display style pairs dark backgrounds with bright time readouts, +consistent status cards, and native animation. Urgent calendar screens retain +the event title; CI status headings stay fixed while details scroll. Nyan has +a softer rainbow trail and a seamless star loop. See the +[design and device captures](docs/display-design.md) for the before/after +comparison and unchanged priority/handoff behavior. + ## Requirements - **BUSY Bar** on USB (default address `10.0.4.20`) or LAN diff --git a/assets/nyan/frames/frame_0.png b/assets/nyan/frames/frame_0.png index 4878723a4d78201fca33da0797f38405cfb03327..db6750aaf024dc1ec1ca29efa63d9a0432ffff52 100644 GIT binary patch delta 580 zcmV-K0=xbF0^9_UBYy%TNkl(@$rdD*;~`QF_7?!DO` zjJLGI|A1Goiq?*#c1ywkfLE`oYa(D-0MRh6Ih#Z!H{NPGO@GBs5d8bMT4ZPGVB};g z#wcNC>PfB|!o2b_m7-e#dq_GWv#2JEh2^>Y$It`^3n1-Vs^J87LPX>U{yM>5)i^37 zZ>Xx+bds*r2DVoYwG|8Gzn}@DFw&g2N{hn0AvK#Vq2WP?#Xj(~l4-o#QyAQi&R(;HLl# zvuPq=h2U=y{Bew|Y87cMsv9FRN}s8UtJn^fjdD!Pev^*^?M54bX(e)e-ifS`` zSTr6I)IAyFPp{|bFZH+0pz~+XOX&Pq&n`N@dou`s4`IAyvSdG7l6{f=4c-B4Osr@+ SF@FC50000Gt;{$X)$$AjI8X<&dyq4*0Br#hC!)UwEM@;x7IfP&{rf#I2?;J!29Gp`}0bZQ;TBBLG*!P$r%+er-*{w_AwiAd>{)YC3_1dh$% zfr$+G%g-s8;)`d)&9OEc|LoWYTA09P#qXk2b=6B6xLSbR#)tXATXrmK^IUaKo~|(@z>2?v?%CKas(_n2J5Xc_%yuB>Pw_Pu*?kSV3pr@ebWVU3 zzZ-puwd@3&sjruIFtnSu28x_C;{OkX=lps+f^TGfoR7E;;}HM=002ovPDHLkV1gu` Bu<`%^ diff --git a/assets/nyan/frames/frame_1.png b/assets/nyan/frames/frame_1.png index 607966f2a42bda41fdbffed08e7fd22fbccdfc5d..f561fdafcb068d348ecb1caf538b9578147d0fcd 100644 GIT binary patch delta 593 zcmV-X0-7J>mupTeojk7}q9rNSz;c>U^d$jDmp)kZ{c?JD!~rJJJMy zli;r_9JwV^S?8@up{i)nK{`SkSfzrmEte(Vf+CE>TKK2=kwH13w8m3I-4OjtP%Vjfk(ujC?g!aT(jwMI)Uk=IZ2Y zp{+&>0F@Nku5XDdjRS(2f&>?4a7ZxUPq5B`EOI~w;(zHHxc>9ij!z5ldEfMy&tw7q z?cuX`pt^-+0vA{4tElEp_Lyzko};GgH$C=ty;upzSMK?G{=@N^8@IA0mLG#!sc=JS&&rWKan7pth8Y#`sgN!E2iYcXHtf+k?zfPsK0 zp1y&2Qc6veI;xMajb8nt$hg&EE^UCt}LZPwm`s100000NkvXXu0mjfXg(vh delta 342 zcmV-c0jd7!1n>foBYy!mNklv9-^Io{5m|dBWwQzHfny_>FpvR1eAQQq zZ@L?1$Lef++Oan@Fo4gB-^5zgU2o~fl>%h`Kgk~-0@MVMBYy%LNkl>OBY1V{WPIuL?*cbJpHUrq|L>0k1Rzt3{eKSx|48%Al-PMZ*F;Rl zmvXU=8-V$x(w)vN@6u|mUd=6!k-}aki3wR@Vk|SO-_4MiO~qb zGcB^&&4Pn|uhq^U%MfS)o;IF3u~Ar?k-uSZvXpj7)hM8kh?F)2lhZ`opijO9Ll}hy zF;CwJ{%d0Fc7IV3Kcmg%V4d2H5MfkFQViz%kTNkLNavP!7i-fymWfn)Xtd~QBs;bP zu%fBg08l5rBlriH2UhFCFdPnt)T0yPq*RbuSs9;ZgBKdr-*A?zyC zLi1pO?@Gz}c8X}%<6gy=#JXc<-?BRvhGFcO?V9I~H7>vz7*lIvQ0vzhZoa?g=6`;J zSy5#guLWCr;4>z9V#0D*9+4dRZkl53YM+_GoNu0^oUM!cOdn_`TBUZGKVoFZzW?2J z#-Bg@=rxQV{#td$SD&1r0%Wsm4yWl`kAvb?;Sl5hi~I@x-*E*8eVmST;azwD0000< KMNUMnLSTaC2^kpx delta 318 zcmV-E0m1&%1lR(QBYy!ONklx;cbO541|3WlXL}`$sVCIbOdK6vO-sJMe{^M zh{KDN7FF;2703~MTpWmA95bVT1-uFCe4tnV+u)WU{=n_z&Zu)=2I-I3>OD^(63^QJ z%gDpFtC%m7SoK^{Z&a&i^fUm|SYh;4m%Q`PoaU}k<|-hYe2ApMEY@bSj)-fW;i zv1KU%ASH%dPCMWtvEAh6$48vq*y^#xxxWt5w<)3%kvuCoURIgBf;`6Ri~79XSjhCs zRV%R=>U(r%4AED~UF{~B2w6n(J?5+C1?e44CP;;*CMkL>&u#Cqy`A*9C%Y44=Ys?u zqi^ucZoC9M-#3t0i0Jx3R}nw1R$}LOC%g~Qt1rOv=%Q3^J}xfxIQ4RT0c4Jn6)W9; QkN^Mx07*qoM6N<$f?&U!3IG5A diff --git a/assets/nyan/frames/frame_11.png b/assets/nyan/frames/frame_11.png index dd9631f76bcf13edf3aeca09f7bbe7251afee552..588e834619bd78c9e7854171ff6a8b3fe7f205dc 100644 GIT binary patch delta 567 zcmV-70?7U50?!1HBYy%GNkl+)ZDAn<#oC}LtU|=*5ELv@1lwRBwO%8$vp=)5^PPKgBKY_i-p;() z``&)vuzO6r8OLh`uc+(a-9B?BBmNaeRqV_CKM4pxrlI*Np@07o>&-2iOubM?Fu#<# zlZ<$^Wf=kuz>9;YL~a@7myTSUqmg8o_Jt+PWu1;ZGH2GLLFgnp4| zyF*mOpJ;nBSbwKhBSh#`vZTR$A5tbE1W87`S{ol+vCO3ML!(J|BU!O;04s)i3;=bf z6+(ZEr@$&*7>3<$m%4O9{46D8W>&`Lkou1Ul~!v}-Duh9KIUpr#r|Hd!ISVrJjTHl zpI9g!{1m^xpMuN6mRLWtJc-IMU1cT%fEgtUV;-n39e)IdhM(qP+7Nb@X`=aLLGRj< z>+LO~RoAuBhr~K#X5F$g7KWiFX1Shn#u``P42-EU38?Ycyj$-dx%K;>F)XTdqY38= zoxL0L8IwE-VG)+kNRE6p-D0f9oSBok-n@?@yI1w0UeHc7b+yyaOUoNsvFrbP&iVIm zzdGknZZMXd^Gnkgr~o*mItaRpRbv?40`|o002ovPDHLk FV1oQ85kCL` delta 332 zcmV-S0ki(k1m*&eBYy!cNkla`HKx zNn**T=+yUx<7dLlgBV~JIcIoJd>pEJQ*9V+&Dkvl`$zV!cE8->pysCh2i|J^15#)*?W3)nTNZgFo zzLgm(<0RXj>_v8rf)AoJhQEU|-FOJ3=>o?zVb=xS1Rq?`r{8UeK146y0q6S^S-ExW ema^UYclZSp7MmByh6q#u0000%uBYy%tNkl&3pb$SE!`qo} z_TI<)W^Wf+M-R|w-Cgs}UU-M_9BZw+YdyX@U=e_~gBi)MxPO!RjxFEcm^8h2g6RC& z{Y~c1GI84EZH4h%>94t(7A8#*g;a@Q1Y%Ssd~88a)~c1+^222w419n>99}y~Vwc!a zBKS)L|A)cRTgqup6hb8EB15SSM602+)v6VTq_!{$pVD>fvM9{uq`LVzoQ6LK#5}bc zLI}~5Bqd<*k$-T^5wT;OaN9P)FWJ0URwbHtGF54jQV)Te(~We;eBz)`m>|y6i;!tu z5Crvlow~Gw?E#=Iz7ZR~6Z{o||J6p;y~*mUFBh%zU-~z! s^Bcz_@Y>+968Dz}x<6a~XSjv^1A7>!%1oDd@Bjb+07*qoM6N<$f|XG#X8-^I delta 349 zcmV-j0iyo)1or}vBYy!tNkl3n2u0b8uf0^O5i_U}OyvWWwiv6!xd6g@suW*)QbAb=4$k zP*p`@H-ykktAWslznhdW;Y+1zabSIv7PXNJ^R7)*9~R31cYlK?XvD8@^Ve^8L9?Db zErq1qtJ*h)_n!yj&VbLnhq$Lx)%Qh==Ohxx&zZOca2zHJt7!Iy%3 zsIlC)43D vev37K2@YK?r#cww!`lNzP8#t)px1B*$`qcFA^Ald00000NkvXXu0mjfK_IBj diff --git a/assets/nyan/frames/frame_13.png b/assets/nyan/frames/frame_13.png index 46bac676fcf756deaec0e49a8d728d1bcf6e26a6..9b62e38402031bf6d4ce2f7f8324e8d53c6dc785 100644 GIT binary patch delta 588 zcmV-S0<-<>0^|gcBYy%bNklC(kPS4Bsu#WsVRgU%{A=ps~?E-Hu)iin`JyVNXlNjxvP_mV0V^y3lU-F+{-ukXEU z7;ozY4Q``mw0FUO28#%D8#Q(Lf90_ZfbnTuakjBa0>IXLU4Ny?*lB|Q@LrF+SvVf~ zSNnod!t~TzdD_+~Tf^7pR#vGLohn!(9&>bAZWarxi@C3*4h|+j+OyQB z1bzz!FwNpF-U24 zZQ=oAXW~>3Ihw)w*Qi&tAPz zWZrV=HbrWV<48Y2*z?;3dN9~AkM5tptf2doeS7Hs-t9272u6FCyhRf~GNWJje;-YL aI)4C10<6bVo)Q%R0000Vz1ndHkBYy!iNklU7Sj zeqELfgl>ptb(v2(c_+e80)HTg z(WffP!qS1RRPBgDL~=v73}J4O7+{GMSZ*ni5FJ_}RH>-!6=b0D`C^>&dl!hvl26v< zo&7!Q`|_Ue62`|SVHA5r383y}Pk#CO1d$f8E_A zc2>dg+k{cZT~#L#oj7q@>{TsQCRNPZDdi{bgZWH9sCSF&&l`H zY5)O*kFun};eQ3uG3P|a647mk1iygwVkwqrx0CJ4m{fiUR8qT3rX>)oRo&3f=@@*jOhLJ`(&Y!GDX9O|25GMRjAsMy;Q#w2IX+H70QJ?umR9Xg5j# zW|hc~VyU_`4Gg?IWQ7_T0a+f$Sw6i`W&#)ob&Uo@@qg86HKz6O#mlvon8|zi?fomg zp`F4@X-`GFZsv^nzOMtjT!Wmk>+hQnf@0@7#`!E(#UtxgM1S1lLfOT|7fXae4uBM8)$IyCvOPva+`)!R=L zqg&rfDL=*dl>3q(js5<2iZ0AgY@qWee?Fk|Z)O|leBg#R-hl)sQzvF59%jbGuR_kUl$p{kOSBXW91Rg5Ca zqJnwuz&9pjDL5D7R6CYL20a*p(}*40iAERxE(+^JBsWU7!zP9a6lO5ri!UN|n?Y$T zWY`mF{6U2Y{Ce?YAj{h^SM|{Q^1$23Nqj!chauZOWFxnmjnfW%&#S87o74P^<-Vme zmbBSzCNB$nFIB{xyzZaAJPeO*VG7J<@JE;U>Vh!u{p#CpgKk0&8Z=!K(2HN3v1Rwm uUh_^+4DGhGgJJo2jg2BFJMq7u=kNzV5|?0xvm|@~0000$n+ zd-C{?FaWgPo`2v;Kl79TjH=ku`-=cLAbj)uWrBa|^3@}rGk^R`9l^{*>RRWEqu;NV zlaVPP7d69O_X+-rPl)l?GUd^_tWqJT32RI`q7>d$uT{&Hw5vIa(L4g9FxMr2Ga)qx zR1&J17JY>ywZUp#wzbu3N_xNsIG zoRiKMM|<<>6@LpvNhr&bIAFqe1=Df}bZ6QZq|p zqf7pDDl^tvR5!+CG&-qDtJu@c3UUHD?yjyG(jSV72E({?vR8@gU z3t&o#Y}dDBm!^TCX!?1mGI%^oKhbbA!q*)M`SByXQh)GjBf#=gMB7r!e|?hIV{`}2EwR+M2WgSQ#V7t zdXIFrZwp<0ptUG>_&^i5d)?9QkN2-@_fH;YwfnOp*T?`-T2q@d@LO-sx!QO e@=x$z#}x-+xswdb#TeB900008vRZUvah9^q!V5$u`B3SA`?O+`cC z6{4m>Rqy)+r^%Gl`d~L=}wJKUddSS?HTQb zo9H*mUCkzih_e>y?OOK2%VcMSOgR;rLQ-Tb@7tWQIZn2m$?A>?_#iHA^iMc4jk~b) zCv?mMSN)(j(Jh@H^x?A;=tI=%3wXGvD3zOE7q_-t`Z}&CiH8^ib77bO0000X^(WM0^g$_E|#YqQoaMJ7@6vqT9 z73|{BRYm90p&1+mQJfWZa1x745fmI8#8niWYZkd&(_Ak1)3jERK0e3K{ddQI`Tvs; zI+B6mg4=p$=`0yCVQ%YPIH<6VuMY-50hn*X-$eLV9$!8poPWtbe{rs;m>Ef3>kK&h z`)Vl~Sp-Oh&G6O}gkSav3cr#l54W*{g`6f>Gu&Zw!CkfLPO%hsHHCaQkB(tj>5;z| zk(vT1MN~B{{0fI_1Iul+wbiQVTabid$Rqu9hwzt?wmXG{cyab~))p3Zz=38WNdm+) zfw8txiaG<1rhhZ>73+xPdTj?h>`hik9+qUcVI`*gqy@@0w zXBNe7kNl-rrqWtiH!3pf-BiU@?CqfpDF!L(t)3a&A72O$zK`egYcN>oh{vB^%Ryqu zsyZSq03wyh_I!(XX&eaTO+OD=235246Ad;)eBF_NuYVq)rL3rx7!qa1nR~~EO5QcyH gcQ1Zqe;0DYH++Vbu~~BX5dZ)H07*qoM6N<$f+{5fp#T5? delta 315 zcmV-B0mS~d1l9tOB!3-AL_t(|ob8q2je{@@g>8vRZUvah9^q!V5uBOG3SGq~nu>;m z*MO80s(RlqATPuZ`w60d-U~(xesaTE{a?d=$*IR=?#|4;jTv~Z9rSgvKBN5r^%Gm2Piq+ zus@@ta2Ne9xvSNr5OLNbyru#6XUEHp$u^@H9;8$am7XD85yIIAz<>5-yTZhl?d+IH#V_yrXViWmf1O;Z2> N002ovPDHLkV1hAHmqh>o diff --git a/assets/nyan/frames/frame_17.png b/assets/nyan/frames/frame_17.png index 2a754621176e7842c306df5172eb60f26e88b0e9..61f6518c9cccea730a4f3862aba082a3e6cca4ef 100644 GIT binary patch delta 551 zcmV+?0@(f20>1>1BYy%0NklCg-gf+)_4Iyi~Nr3e-r9K=;BIM*z4Nt4`N?#EQCh>y?l@BX{vzx)44 z2p!77zku8Auxvh@l7Aq~?RLbeFAo*A_WsHMC;$u0_tz2rrGLj4xA@M)Z(W?LRLqQ} zu5|_+{Q`o+}gyV z4mi+CCCLXdO@CmlXOyDOfTP)5a>YWCTn~=2+>NO!b_hUZ>jn7miRqW+{12POU)EhlBxIa24Kl}iXXIEe_ zX^Y37S{aAb5LJaDEdU~v$o71TcWDv`6wDwGQ3h4B41W@h7A3xJOT<^V&`RE~mH$YT zHzw9Cd}Fq4`;J(yue`BB8w>)3@UdsW$D3I_-#pdxkCvfdr0K>_LB@tnLYUDrsyHNC zVFpCeV2cp-d?GGJeDNIa?6>oMeW0}{f5Hi4N`e^_2%of(#BtVUW^Xu#3V9>`3m*RUA%CA}*U2dyk`!ZPW{!7+ zjCsegEOOF=p?DghV*_|UHsWuivQAY?uXOoXd6m{3Y6zh%zv>T7V?{GCkXhB1W5L&F zv=?r|Uqz`ZixeU*EYhE~;PB2FBfBGH%Bj&5k}6}RZ#iQ*PPUv$Q~^3B;D@-h;cwu` zH12|>zrisNd@|~SUWM*l(1&X$pdWFTUx9}?MXB9#T|L@z>(AjExQC1$WPwry00000 LNkvXXu0mjfM;Tx!8!cXwD=SS%iPLb`)H&gdVA zSneW{CXGdG6)hzfG+SI};njD7g*JhhCXEWBg(5*vVt>SKWOg@`*`4<`QF4tRkLB&m z_h#O=@69gD_=jsK*PLCVl7By+pLCjvo%i`(w<9uH zoqzlM6kAz3|G!NbCCp5Hl(U8~udzX;=(fO~m5!JztHEkzW3^aDA7uaz7C_p!RL=?Q zgxIG*@OKFQmc~&bMMG7~rk}nmwSnz5LT%Mb|0$rVd|?#6M)Ou_Q79Qwllca2-0eww zjhc1Ag;qpTGJiOHBs%7jh{zG$c0}+CST7dC6oYmOpsP|Mr5*y+UN;iLC|JZnp)gOJ zr#B&1I>&KpwHkGcg4Y2UW=llCCc)n$_&+eRs#UDDsBVnOs0~t;RIz<58|9c-c_kkO z+Kn~<(@Ny{zAd{n2@I?}q=mGUfHV*NG@qEMFaeB%ynp%zzTa-{g|rA?Of7y3nLNV3 z_}vZ`)I)eNoA6fD>!!|_<2Zq%m+LKO?D4GBPbgF#_&ERi@3Eh^7Lv$Mz%bo75d6~} zFa{~ZE-XACSC_M9$l(IcFYF`yUr~;jD|-z*T((>bpIW8Ml~gh7+(-!Il{z%}Kdbw*=v6qUDrTQxosq_F#cO>|;foBYy!mNklx5>g%n zE-z`_N|oLzKlnIDKF%1T7ycJqL>pk;gpbI7A?#Pt3X8KxWDUgQ8Z}8?^xlg+rY#0S zKSWU`d}&k#Mkx~MqK!Dtn>O#^DP#)=0J-jVZi_@% diff --git a/assets/nyan/frames/frame_19.png b/assets/nyan/frames/frame_19.png index bbdf379070b2e422254e54071303c842018eee48..94a130113896dd2f7eedaa6bfac2b38c1792a5ad 100644 GIT binary patch delta 570 zcmV-A0>%CO0@4JKBYy%JNkl6ImWWd>PlAO`?+mNl~OcR`8Hjo zC$)hcRAX&!iToBcVH8Gd>sDn^m^Y-R^9|g3YrXaY74^Ua|3{J{aOff?=8DJ|A*St! z;1{r2EXF139QX-`u0&eOw(jBvigF+!koTrA6Xr1FY<#L(2MZqfo46|t>VTIst z5&Q{^ta_CgEovGQGRpN-rFCovt428{W`D^?fli|hz_b=Qp>NA6O#=ff4_P5CBOuFT zKg&A@N=yLbAg`f8=s%oo#k3l}=$jggnY@O7x%UulsDG#MVn@nTQSX~NV~*oQj^3_D z&e-$${C7dI^2o>eHwVXI-C9a2KLtOT#=79$_rMrr47;@OgprDjnkk2qIKQxi@VCQI z@ctrNW$VQ7Vo=4?<;eMXXs*r+nt*j-6a?~09h&_9@A%P=J*6$GUVoyfy7gPvcuA1L z9^cN;Kr?^a47z^$w1lpY_w1tUyVry0x@hfCs{R7(zy59c1XeMl1&S)TlmGw#07*qo IM6N<$f=u5WF8}}l delta 354 zcmV-o0iFKR1pES!BYy!yNkl=8P{jQ}%+6>b$O0!QKC zahH}>s&r5J2;rId@r)sQ;eUZWngHLY@Dcehg#9W?VcvUY_Par3>qyokYSaAD7^C_% zY_Skp5JgSl*W{qe2}+U325lsMPuh%uyO6QKna8m|;~!p?!+)#2yvqk7Te9Syk`%%9 z;?+uh$OJxfN>NpVJZJA5%OZmw48_%m9UGL!6#fu*)~Ragl@`NzMB5d31&;0Dmm5vs zyIUqPzt9^F$J%NfJ0_C0v(@&7Vm9#1;&)N1y4p+1zAiaE-lfpfcZ^KmvQ_$*jk7cO z%7d<0`j#Eb+FMLrlc&!Z5-^KDK({wO-wz{Lz#+4s83?HPQ+%5b*$v|hx(j`9LDM+_ zv-sWYW3Kroh*Raz)Kdd2?dGLKk&~17FVOe!2{k8|llLG+8UO$Q07*qoM6N<$f(x;) AZ2$lO diff --git a/assets/nyan/frames/frame_2.png b/assets/nyan/frames/frame_2.png index 73115728edf187edaba7d2fdd70b66fa433de91a..36686a92c59d7e6c4be5604735e4163955b64a27 100644 GIT binary patch delta 568 zcmV-80>}OK0?-7IBYy%HNkl%y-z50! z3P*;dA*`xw(MdW&8(3~B(3a1Uzk(u+!bE-EGA#Roo(BNLVw_o%g=z>-*N#N} zR8;FG_n2+lzN41wJA3T)a=DgJpxpOy{@u~3m$woU$&WxY-PjPk{}C92m}b`|9*`)C zSv}-<7U!q-5dLmG#~9DJB|KcTTp6EQYVd(3U|wknfw)qeCVAKzKLkH}^+b_*tB-l@ zBS8v#elfp5Cx*Kg(DCz^Rdjr6U>_adza53Y0Q;|hTRs6hYMljzZuM#a0000!1fBXUdblbGki9$a|4N@j!j3VE| z8Xchzq9_->RH{-yDH7?Vje?Dfwiw|q6m)RqIQD1!3U|G|hkp;Ws-%=oNy=c@%nIg% z1D|<(IG(AoKfSeK5tgWqoL0Hc37BvLQe8*xI+^SkV@@HF>$m zkN_|K4w`4<`Bn2Ug9B(X1!f0O^OyKqn(W*K-Gm%m&~#0J7r&W(&UJkgtcPZ%s)v#? ux_a}LM3Iw;&5W7P^T`R!e6=9|AU**o;FnwNVOYNa0000b_D0@VbNBYy%MNkll-v=$UQ6$=ZCL$M3k35rI4AYyrk zNSZVj+A3N~44NzKQfRG$g*JhhCXEWBg(5*vl3wC9vUittlbyN5C_)~?GQ0ccn3tKo z+c4hK58VT|Rxx@9+F?eZd*Ie8a%>3}2Qcc!73YAcrGLrT1%m(lS#5fM8Gz|KP33`7+Kb;skNwXY?6^}r7EmqIV>CL=ve(B9|hWt764O9WP83Px-<+7 z%s501VG#qOJn*A@a3akNU>wBg8F+rVco@(c_{P}MY=6My4g9;4vj0FegeM0=-im77 zyX delta 346 zcmV-g0j2)c1oQ%sBYy!qNklWUn#$J zHblpc)!288naf@+@rGh1@L2JiC{^9{wq;+JoW%E3A4pXkyuNMO$nBU#E%;KAcRg16 zmL1F5d|i{5ds7Svu;O>noQ>z_VS;1OWD14>gPOm@*W6_1F6buY$puZ<1X%H#*{58~ sn_#!}^-%}I@#k%cA}5XbFVOe!3E(l3mS&V6{Qv*}07*qoM6N<$f>1W1=l}o! diff --git a/assets/nyan/frames/frame_21.png b/assets/nyan/frames/frame_21.png index a7f5cf755389911562961fb79aee71c90cffd8a6..2c97ee19edfa419b9958f50c9cf278024d2b9cdc 100644 GIT binary patch delta 557 zcmV+|0@D4-0>uQ7BYy%6Nkl)#*B%~Rt=mYF&1N#0pH1Q-`}hoh=D!*K zoBxM|@u>o@?}yKqw*=1o zI4-V8#mq$NT4&7BoN5aQ<78wFD8K5Lt z2DJ(viSowex((l$ZQFq(m#fGdtMyibMyjQH|7o^#Tx=KDbF&=~Q7Ch+|KRJ*=3y{}!r{ZrSj*B2g< v0b+SiZYECISElWFh{EUn-52=Z`v~3vh&-O=*GwXy00000NkvXXu0mjf|0EyS delta 303 zcmV+~0nq-%1jz!BBYy!9NkluhDEquz2B#!so%`6) diff --git a/assets/nyan/frames/frame_22.png b/assets/nyan/frames/frame_22.png index a7f5cf755389911562961fb79aee71c90cffd8a6..b42ceb53e4e69b5f48417e499c657e539f12db36 100644 GIT binary patch delta 567 zcmV-70?7Tz0?!1HBYy%GNkl_{;Rubl4?olOr131gdkt^LK z*gM#3chbuu1e$;q7>}LMC~Qv1|EO`2lukj_z@cy9DQ$54%Sc=AfP4#@FbYj#o_-Mg z*TmR;Mn(Mg_J14BF172y!=RF+5X`nAWn@B-&Wv|!bLAaNL@GTr+VnJ%9XkeC*3@eN zs5@;F{4-30TwKFREDl95$OO-YmpoHj(?icGB7mlxDH*0$X6K`ng%m` zQ%cIur-)8H>{WP3a%W8MTXM!+*9{%LUDO%#M?&1-N5#?%@a)cSqZ%(uUo`Jc8h zDXL6kIb^NQ7N6UUNgkQVIV_Dxj&wIp(RKr$>A{q*-$yxz*Y$}$&_=Y!e4q*3+#MO? zujbzy<1pucTE=*5=_e{cu5|U`)cxFLFS|v7{0V;QcmSQ9nQdt?VCDb-002ovPDHLk FV1m-P8$|#B delta 303 zcmV+~0nq->1jz!BBYy!9NkluhDEquz2B#!so%cE5BYy%4NklBEc%Q`SHd8C;;=!^|uiImBSa8xPQ*{FLeabBB{qZBaVK* zT8c)N0MxK4&Uk|Gs~$n(*HYzSx9V8PYJ)z<9aaqXYBuWSO48O83gJ9_!>~Fee=j0c z1*k++wRL<8i)#bjZuPY_8|YgQgkdNk{d9-$SCFbZsMHx9Q@B657{2%(p3bj9VWA}se|9|& zsUfQJMd|=VD3Rg#I`7gX5GZI~9-<5?XXzyxOosSzOMe2syo6SAZmrx)BHb~uZvGuJ z48wKAa*20L`d&dRR_K62fDk_R4fuGw5YN}n;`zsGFf7t^V-#)&`mIQ&#x+8i(l;tO zB$#0e1W|8^5OsVaP6m8&A<_}S_Ipws^(+5_*{uhDEquz2B#!so%z_& diff --git a/assets/nyan/frames/frame_3.png b/assets/nyan/frames/frame_3.png index 7135822b08e43e0fb3ecbbf335ce5c71ecba0942..27d25e43dc1c061f8de401cff40219cd8f1075de 100644 GIT binary patch delta 552 zcmV+@0@wZB0>A{2BYy%1Nklfj_6mm*kja1d9i;9R@NU6STo${_Fog zDWPL2_>VAt_`YtZ{WG@^EMr@r?+kzfFwY!+8{uCIzP!b8W`BOEB8V9ZUF(c^_1n~T zJ8hOsgSZI6GC(3|h8Rx~e$gW+{8GGt8f9#$-UMr&J1py;Rjris1MqRfD(8Cc?jjVW6bW zw(VN2hCOOQZ$kmek%h50Ab%y2sgxF0jf#v~KUGl~dwVQGf#s0G{lCiswdI{AX18K;aj{)qd*{IdU41ysDB!z7pOlO;On;de02*gq@7$j zO``OeoVQ+&*|zOCa=M1;u}ljL0)+6-i_^Feu_wV_%S|agz{cxJDI+ z_%qA^f7RO}tqPyWlRjTQhdYNY_cI#v1Fc0KPghjNo<5GW^9vKZ+WD*3oOV7p`v5V( qvL(4VWg%aAtOl1bpnrn@I=%o+e3g`&4sNFa0000m80#0{+9eXB{yszlXAIoeLi{;G zkDe#zN!nq+-FGcBQ{7s+fe9fL;p;+`7rcZUw2_4Vqs`1NIDdu$xkCH}Uha992by){ z$RCmv!PVcxV^rR8Y!W%>!BAX{(6J8O?+fv_ky)pzsaHC`Tt$NtLMY46@anSvY+jS69TTu2PRj5% zaHJY{!P5T0F+At_)B(K;-8rC-#~#1;&_*oz5eGMpyH6LzbklKmY4fQ9sQ>G61#kQw Vl@V#?1}Xpm002ovPDHLkV1k;3oDcv2 diff --git a/assets/nyan/frames/frame_4.png b/assets/nyan/frames/frame_4.png index 9dde8f3a0059a55d8065f4154724c68067491dac..6763065a48032caccb65330780d6370b3498624c 100644 GIT binary patch delta 560 zcmV-00?+;40>}iABYy%9Nkl*Wf^A$4fZLl1``lvYf_xe2%aC z@6P|-|A&w9XB8|T)5%zrt0)jw0lzVwjH9tIFsfp^_qPJzfPV-r@b?J*FQ2b237nTc zzxawVJC(Z8dE)4D`hTh%A17IFfl|^efBZu5+aW>Yck<XJf1^I_5sU@J5QZ;nwEj+0W&h&hyt=}V0K@mowM)cDkg8zeij`o1H(Gz$zhJAceWmBIBa!$jlB1mCnI=Icv% zs~XfQxFp&gQ|lJpG1qkiM=jT5?pSREi-9pY&J1!qdu`_1-^~2aAFwF$bmLsG7b`Yn z!kQV^92Uh8q&VA)*#XypI=*D%gE y)-R9)qAA`{n^X0*&wesQfqVp~QT#*pfA9dz9G;XNHhSX#0000v4oI41}c_DoG1alT<=#NC{F+xP`Rh4Dk&y zhUIY1oWEZNFSZs*Jc#Zj#z>vu_gG@SYa;~QFIHYqtG^rWQY&w8JHQzO=e3c3OPta5 z{UkFD)w+nTJ-n1CDN z(nf#6m1;bPnLSy?4jJ>nRWIm6^vMhQ^4pJUzN7g`1CyL)%XMpN_vCukZuf WER_&i4>$1u0000%TLB#eKU#@Nt#eh~QrCIL1kVV&m(ef$3M7$t1uG0Qgc8xp{niCPc)@Ac%4>PTm7KccYx`Nm56@2B=~DY z+npdIZkzi>?|(Zf4o$R8Nhx5i3n9A3Iq8h}Hb0$Sv7Sh&hekzpqZMSu4gnT7`7zKV z^Nrx2VHg;t^L@Y3Xi!@#_@6{VdS*%N4#;0kWg4wTb)zAp(N9%c#qR%YAt#XIYE|Y) ze|9-O_#tk7UBD5s5XGNgFCsH^RXvd&z_b$i%J*cKrhkE<<42mq>*ca^M zl*^cKy2cHMMJK{RbQ^|fx5{VwVZ_(pkG<5<5v{=|GJ;xUyrxo6{lvL6dDD6VE_OC07*qoM6N<$f`UgF AsQ>@~ delta 320 zcmV-G0l)sr1lj_SBYy!QNklO;QP|AtgvPnHJKDb{sYY zSYJP8$@(KXU&35KLXzze{yBoXuiCGN(1f2{a901@&?UF}f@lXgqu{x8(%1Nm)(4A$ z$Hk!OXD?M1+1h%54k5be~t#9nD&>5D$fv zpi5p|L^~PFK`!xN1Xm->*aV)>o%Gu@sS}YhS5gl%?oQu#HXw%(ef5ohs2UrYKGS2- z)@yWR>Y`tzj4B7oAyAPb`Lelg-uyF=3|bPWS2USWax^tb(_>}ca*yS9(z+&LDWanp zG_7$T{RT&>aXA;_@q&z5q*EX0Rb=M_efaDIx`?y-2Ab|&l-w=tW~lYlpW_DVL6s6* SDJ_2h0000IPAsm0;M7bN0 zQ(U8+M{^Alp&)XR5PD%7dAobh+kG?7Nr=Rk$!2!vn|Jfw%Uw2)S~vB_~5 zSjS1oO&;0t!@v@2SxJQGgPRB;(A$7fEDYwm@DcexguN8CurNDBHcv9HyCzMO&NEvk-nY!GI)my00000NkvXXu0mjf De1ocH diff --git a/assets/nyan/frames/frame_7.png b/assets/nyan/frames/frame_7.png index cb566ebbce2f2ee4ea03a42b2618b8e39046fa55..13cc26ad66eda02c9b87fdf4f6e983002a182b72 100644 GIT binary patch delta 567 zcmV-70?7UR0?!1HBYy%GNklEierLE z7Z(Rz6&*#3Z3edvbymSamqK;PqJrq4hzLsErFN0z((5(%URrBK@Z%BQ<-T`)U*5Yr z%6L~V{71w(xK_1N?%--a{1ui2FlxpXXP>C#-sihYld%bc|9|-T9$MLRe!oo^B}`Af zle3zzYH5Q?(Q(0=@VyDk=9q)9987??Z?T#q?3{>55&Rv3zol?wNLo|n zT6B_*rwuFsN2-dDR1-#Fsxfbw7KIf}YBXQNgR?zstx~fNIN-JDb>_)m`Cb3Z{B#8ag7DA=7Z9A9CQ9~5G2*5B~AObcC zewN_RVq{sXP-{`$7?P1|rYfppc`O_0=vewD9|hWt764O9Wcz;oBq6}SOhVl7EM`EQ z2Y#IQkL@x87zgj^8~Fb7Wj3HS@QsoA>43=__&0~oVt+w3gr|d38$A`(y2(9e+qQ7j za<#L^-mX^a2?ff9kMr-3&-}cVkVt-ARizAW!`E-68=Ha;J^^D8)9l8?115{!tR8YQ zhx1c;gnytA#GP9$;o)A(x%kv_lM9-Fd95V`-jmui$)oS_LlD`kCyLBl9cHvkf)w`h zajA~0l`yjSgrhx+Xu!qmI{JQQsDQpN+>OIufc@9MEnf%Ro6(5jS`)D@N;;Y=}-Hgh1EB%|z7&wK2+w!Mq6{k^e*3pQ02NdXLCDkZY%?PST|J zUgURJq9OD_6lKDfLe=0@~ diff --git a/assets/nyan/frames/frame_8.png b/assets/nyan/frames/frame_8.png index 5b5175573ab42d4f0b5c37ed2bad7cd038881b5b..b99c6d747ea4becdb732891580022aee9f3ea397 100644 GIT binary patch delta 573 zcmV-D0>b_D0@VbNBYy%MNkl_~L2)a%35r(#K*cdZ zq>GC~T@@Xr7TXMN4mzvgpo>slx~L#JC?bMlchM|zNqW8J-b-z((2qxW$$hVVU*C60 z!+2LOv%#Qb|8i9m=9bo}7M(I!<03JVS$Q#^TVKx9PIYiF0pec7s)--xL`9n5Zxj4Yg(FKc znyRuzH|Yp%V7aA0TRunr3W_iall6VewkWJCXbPdMg| zsF)z!c1rNmn12^DL5r%LR8>->&_f^}=|*~DCb3Z{%o6*lC4@$2+jg;7q)t)r3jl`2 z0#UF*@CyWg1|!Q^g{(!nF(jkdNLAFt_OWhcqGPE^J_^*0764O5WP7|6e#y|od0lq=Jl9+o^xy${{osBq;_LtKwSf7rEuH!5(GGq9YTl+t0)qB|00000 LNkvXXu0mjf&9oKU delta 346 zcmV-g0j2)c1oQ%sBYy!qNkl!y5;^L@5S)$Jv5nL);csGPoro;GlDu05x4^L;Ea=F9KfJ0( zjBl(BlVfEzUUuvYO?2S1;#aX$_0W54xLklt?cMxf@^#BvSAT2!Z^^Nw z&F49J`i>z1CR6+n!p8qfwKg8FhY{?+B~xIw137<+uj!H;M1& diff --git a/assets/nyan/frames/frame_9.png b/assets/nyan/frames/frame_9.png index ad7cba82cd7f08b3eb5321d24b5ed8eb07ac9320..d53fb26a6094556e2b89f007420b8eb2f899b517 100644 GIT binary patch delta 581 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 i9OtXzN<7Oc87?osmY)%qJ6$R* literal 76177 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 X8eA2hCRh0S5!&LxA=+u4=XU-dIydk! diff --git a/docs/display-design.md b/docs/display-design.md new file mode 100644 index 0000000..3d2c4cf --- /dev/null +++ b/docs/display-design.md @@ -0,0 +1,84 @@ +# Signal display design + +Signal gives the calendar, CI status, and idle animation a shared visual style: +dark navy backgrounds, bright numerals, thin progress tracks, and native motion. +Cyan signals activity, amber approaching or waiting, coral urgency or failure, +and mint an active event or healthy CI. Text labels keep each state identifiable +without relying on color alone. + +![Signal screens captured from a BUSY Bar](images/signal-overview.png) + +These are native **72×16 framebuffer captures**, enlarged with nearest-neighbor +scaling. They use fixed demonstration events and runs, not live calendar content. +The screenshots show the start of scrolling titles; the full text remains in +the device payload. LED brightness and diffusion can look different in person. + +## Calendar flow + +The familiar sequence stays intact: upcoming → approaching → warning → imminent +→ start animation → in progress. The redesign makes that sequence easier to read: + +- The large start time and countdown retain their existing size and positions. +- Warning and imminent screens reserve a 16-pixel lane for the animated calendar + icon and a **52-pixel title lane**. The title remains visible through the final + minute, with `IN <1m` below it. +- A bright endpoint makes the progress track easier to locate. Upcoming tracks + drain toward the start; active-event tracks now drain against the event's + duration, alongside the existing `ENDS` countdown. +- The existing full-screen start animation still provides the transition into + an active event. + +![Calendar before and after](images/signal-calendar.png) + +## CI flow + +Failure, waiting, and healthy cards share a fixed heading and a bold lower row. +`CI FAIL`, `CI WAIT`, and `CI OK` stay visible while repository, ref, and workflow +details scroll. The healthy state uses the compact, static `ALL CLEAR` label. +Firmware 1.2.3 adds a small status icon beside the heading; the words remain +readable when icons are disabled or unavailable over an older/cloud route. + +Running builds retain their large ETA. Moving the native spinner to the +lower-right corner gives the title its full width, while a separate text budget +keeps ETA labels clear of the spinner. The progress track uses a cyan-to-mint +gradient with a bright endpoint. + +Quota screens label the left value `GQL LEFT` or `REST LEFT`, and the right +value `RESET`, with a thin separator. Their percentages and reset countdowns +keep the existing meaning. + +![CI before and after](images/signal-ci.png) + +## Idle animation + +Nyan keeps its recognizable silhouette and 12 fps motion, with a navy backdrop, +a rainbow that fades toward the tail, and a star pattern that repeats seamlessly +over the 24-frame loop. The device plays the uploaded animation as before. + +![Nyan source-frame animation preview](images/signal-nyan.gif) + +The GIF is an enlarged preview generated from the committed source frames, +not a recording of the physical display. The encoded `.anim` was also uploaded +under a separate preview application and successfully drawn by the device. + +## Behavior and validation + +This is a presentation change. Event selection, escalation thresholds, +integration priorities, CI rotation and dwell times, poll intervals, frame +timeouts, quiet hours, and manual-session precedence retain their existing +contracts. New shapes use the same cleanup and expiring-frame paths. Motion +runs on the device; no host animation loop, service, dependency, or new +configuration is introduced. + +Validation on firmware **1.2.3 / API 27.5.0** included native captures of five +calendar states, running/failure/waiting/healthy CI, both quota labels, and an +encoded Nyan asset draw. The final-minute `<` glyph, `ALL CLEAR`, and `REST LEFT` +were checked on the native framebuffer. Temporary preview frames used their +own application name, priority below manual sessions, a five-second timeout, +and application-scoped cleanup. Production jobs were not restarted for these +previews. + +The automated suite passes **445 tests**, covering existing selection and +handoff behavior plus title retention, track bounds, spinner separation, CI log +context, and deterministic animation generation. All 24 PNG frames and the +encoded animation were independently regenerated and matched byte for byte. diff --git a/docs/images/signal-calendar.png b/docs/images/signal-calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..9d9aa232a17a1f5bc7d8ce9e528cd54f13aa4ee8 GIT binary patch literal 22194 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 literal 0 HcmV?d00001 diff --git a/docs/images/signal-ci.png b/docs/images/signal-ci.png new file mode 100644 index 0000000000000000000000000000000000000000..1b5e7ec381862935a6db2b276ba1e1aef0f296a6 GIT binary patch literal 18750 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 literal 0 HcmV?d00001 diff --git a/docs/images/signal-nyan.gif b/docs/images/signal-nyan.gif new file mode 100644 index 0000000000000000000000000000000000000000..819aa8d0b49a51fbb412702d183a6841af54173b GIT binary patch literal 89120 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%0D(&b-x8w`yaj1wBZ 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 S)5-fQ0r&AjdNz8* most urgent, with `in_progress` orthogonal to # the other three (an event currently happening, regardless of how it got @@ -22,27 +22,27 @@ STATES = (STATE_NORMAL, STATE_NOTICE, STATE_WARNING, STATE_IN_PROGRESS) BG_GRADIENT = { - STATE_NORMAL: ["#160A2EFF", "#03040DFF"], - STATE_NOTICE: ["#291300FF", "#070301FF"], - STATE_WARNING: ["#30040BFF", "#080103FF"], - STATE_IN_PROGRESS: ["#032B2CFF", "#010809FF"], + STATE_NORMAL: ["#061326FF", "#01030AFF"], + STATE_NOTICE: ["#241704FF", "#060401FF"], + STATE_WARNING: ["#2B0818FF", "#060208FF"], + STATE_IN_PROGRESS: ["#04231CFF", "#010604FF"], } TITLE_COLOR = { - STATE_NORMAL: "#FFD166FF", - STATE_NOTICE: "#FFE3A3FF", - STATE_WARNING: "#FFE7ECFF", + STATE_NORMAL: "#F2F7FFFF", + STATE_NOTICE: "#FFF2CEFF", + STATE_WARNING: "#FFF0F3FF", STATE_IN_PROGRESS: "#83FFF3FF", } # The drain track's groove is a single fixed color in every state; only the # fill on top of it (TRACK_FILL_GRADIENT / TRACK_FILL_IN_PROGRESS) changes. -TRACK_COLOR = "#24193BFF" +TRACK_COLOR = "#142033FF" TRACK_FILL_GRADIENT = { - STATE_NORMAL: ["#1ED6FFFF", "#5CFFB1FF"], + STATE_NORMAL: ["#1597FFFF", "#63FFD4FF"], STATE_NOTICE: ["#FF9F1CFF", "#FFE66DFF"], - STATE_WARNING: ["#FF204EFF", "#FF7A22FF"], + STATE_WARNING: ["#FF416EFF", "#FFC070FF"], } TRACK_FILL_IN_PROGRESS = "#24D6C5FF" @@ -54,23 +54,23 @@ # failure badge does). The v1.3.1 cards (#062238/#062A22, etc.) were # mid-luminance dim fills that read as glow-mud on emissive LEDs, so v1.4 # removes cards entirely rather than trying to re-tune their luminance. -TIME_TEXT_COLOR = "#8DDEFFFF" +TIME_TEXT_COLOR = "#B4CAE6FF" # Fixed -- only drawn for the in-progress state (replaces time + ends). ENDS_TEXT_COLOR = "#8CFFF4FF" ENDS_TEXT = "ENDS" DIVIDER_COLOR = { - STATE_NORMAL: "#643B8FFF", + STATE_NORMAL: "#284469FF", STATE_NOTICE: "#C37A0CFF", STATE_WARNING: "#E02A4CFF", STATE_IN_PROGRESS: "#178C88FF", } DIGIT_COLOR = { - STATE_NORMAL: "#6BFFD0FF", + STATE_NORMAL: "#5AF0FFFF", STATE_NOTICE: "#FFC247FF", - STATE_WARNING: "#FF4B68FF", + STATE_WARNING: "#FF6987FF", STATE_IN_PROGRESS: "#64FFEAFF", } @@ -177,14 +177,8 @@ def _text_width_px(s: str) -> int: # --- v1.6 stock-animation accents: escalation icons + start-takeover ------- # -# ICON_EVENT/ICON_REMINDER are 16x16 stock animations drawn at the panel's -# top-left corner (ICON_X/ICON_Y) during the upcoming path's WARNING state -# (see build_elements): ICON_EVENT while still outside imminent_minutes, -# ICON_REMINDER once inside it (title dropped at that point -- see -# ICON_TITLE_X below). START_ANIM_ID/CAL_ICON_ID are the element ids these -# accents draw under; ICON_TITLE_X is where the title shifts to when an -# icon is present but the title is still shown (leaving x=0..15 clear for -# the 16x16 icon). +# The 16x16 warning animation owns x=0..15. The title and track use +# x=18..71 throughout warning/imminent; the lower row keeps IN + countdown. ICON_EVENT = "calendar_event_16x16" ICON_REMINDER = "calendar_reminder_16x16" ICON_X, ICON_Y = 0, 0 @@ -635,221 +629,79 @@ def _format_countdown(minutes_left: float) -> str: def build_elements(event: CalEvent, now: datetime, cfg: dict, timeout_s: int, in_progress: bool, just_started: bool = False) -> list[dict]: - """Build the v1.4 "airy" Color Horizon layout. - - `cfg` is the `[calendar_countdown]` config sub-dict (needs - progress_window_minutes, notice_minutes, warn_minutes, and, for the v1.6 - stock-animation accents, escalation_icons/start_animation/ - imminent_minutes -- the icon block reads cfg["imminent_minutes"] - directly, so it's a hard requirement whenever escalation_icons is on). - `in_progress` - selects between the "upcoming" layout (countdown to event.start, a - large start-time numeral) and the "in-progress" layout (countdown to - event.end, an "ENDS" label, full-width non-draining track fill). No - card/panel surfaces -- `time` and `cd_text` float directly on `bg` (see - the TIME_TEXT_COLOR comment above for why cards were removed in v1.4). - The countdown itself (`cd_text`) is a plain `text` element re-rendered - from `minutes_left` each poll -- not the native `countdown` element (see - the CD_TEXT_X comment above for why). Draw order below is z-order, - first = behind. - - `just_started` (v1.6, default False) short-circuits everything above: a - full-panel takeover animation (see is_just_started) replaces the normal - layout entirely for the start-takeover window, so it's checked first and - returns before any of the upcoming/in-progress element-building below. - """ - if just_started: - bg = {"id": "bg", "type": "rectangle", "x": 0, "y": 0, - "width": PANEL_WIDTH, "height": PANEL_HEIGHT, "fill": "gradient_v", - "fill_colors": BG_GRADIENT[STATE_IN_PROGRESS], "border_width": 0, "timeout": timeout_s} - anim = {"id": START_ANIM_ID, "type": "animation", - "stock_path": f"shared/{cfg['start_animation']}.anim", - "x": 0, "y": 0, "loop": True, "timeout": timeout_s} - return [bg, anim] - - # Uppercase kills descenders (g, y, p, ...), which is what let the title - # collide with the track below it before the ink-offset fix -- see the - # geometry comment above TITLE_Y. - title = ascii_safe(event.title).upper() + """Compose a title, horizon and equal-size time readouts on the native canvas. + The urgent 16px animation has a reserved lane. Its title stays visible in + both warning stages, with a compact IN label beside the large countdown. + Text y coordinates include the firmware's measured two-pixel ink offset. + Full expiring frames still use the existing priority and redraw contracts. + """ minutes_left = _minutes_left(event, now, in_progress) - state = _state_for(minutes_left, cfg["notice_minutes"], cfg["warn_minutes"], in_progress) - - bg_element = { - "id": "bg", - "type": "rectangle", - "x": 0, - "y": 0, - "width": PANEL_WIDTH, - "height": PANEL_HEIGHT, - "fill": "gradient_v", - "fill_colors": BG_GRADIENT[state], - "border_width": 0, - "timeout": timeout_s, - } - - title_element = { - "id": "title", - "type": "text", - "text": title, - "font": "small", - "color": TITLE_COLOR[state], - "x": TITLE_X, - "y": TITLE_Y, - "width": TITLE_WIDTH, - "timeout": timeout_s, - } - if not _title_fits(title, TITLE_WIDTH): - title_element.update({ - "scroll_rate": SCROLL_RATE, - "scroll_start_delay": SCROLL_DELAY_MS, - "scroll_repeat_delay": SCROLL_DELAY_MS, - }) - - # v1.6 escalation icons: a 16x16 stock animation in the WARNING state - # (upcoming path only -- in_progress never reaches this state visually - # the same way, see _state_for), swapping from the plain event icon to - # the reminder icon once inside imminent_minutes. At imminent, the - # title is dropped entirely (icon + big countdown number only); before - # that, the title just shifts right of the icon (ICON_TITLE_X) with a - # narrowed width so it still scrolls in the remaining gap. The `time` - # element (start-time text at TIME_X=2/TIME_Y=5) is ALSO dropped - # whenever the icon is present -- see the `elif icon_element is None` - # branch below -- since it sits under the icon's 16x16 footprint and - # would otherwise have its leading digits occluded; this applies to - # both the warn and imminent sub-stages, not just imminent. - icon_element = None - if not in_progress and cfg.get("escalation_icons") and state == STATE_WARNING: - imminent = minutes_left <= cfg["imminent_minutes"] - icon_name = ICON_REMINDER if imminent else ICON_EVENT - icon_element = {"id": CAL_ICON_ID, "type": "animation", - "stock_path": f"shared/{icon_name}.anim", - "x": ICON_X, "y": ICON_Y, "loop": True, "timeout": timeout_s} - if imminent: - title_element = None # drop the title at imminent -> icon + big number - else: - narrowed_width = CD_TEXT_X - ICON_TITLE_X - 2 # scroll in the gap - title_element.update({"x": ICON_TITLE_X, "width": narrowed_width}) - # The scroll decision above was made against the full - # TITLE_WIDTH (68px); the icon block just narrowed the title to - # `narrowed_width` (19px), so it must be RE-decided against the - # narrowed width here -- a title that fits at 68px commonly does - # NOT fit at 19px (e.g. "Standup", "Meeting", "Lunch"), and - # without this recompute it would keep the no-scroll flags from - # the 68px check and clip statically instead of scrolling in the - # gap the comment above promises. - if not _title_fits(title, narrowed_width): - title_element.update({ - "scroll_rate": SCROLL_RATE, - "scroll_start_delay": SCROLL_DELAY_MS, - "scroll_repeat_delay": SCROLL_DELAY_MS, - }) - else: - title_element.pop("scroll_rate", None) - title_element.pop("scroll_start_delay", None) - title_element.pop("scroll_repeat_delay", None) - - track_element = { - "id": "track", - "type": "rectangle", - "x": 0, - "y": TRACK_Y, - "width": PANEL_WIDTH, - "height": TRACK_HEIGHT, - "fill": "solid", - "fill_colors": [TRACK_COLOR], - # RectangleElement defaults to a 1px white border, which at this - # height would swallow the fill entirely (found on-device in v1.1). - "border_width": 0, - "timeout": timeout_s, - } - + bg = {"id": "bg", "type": "rectangle", "x": 0, "y": 0, + "width": PANEL_WIDTH, "height": PANEL_HEIGHT, "fill": "gradient_v", + "fill_colors": BG_GRADIENT[STATE_IN_PROGRESS if just_started else state], + "border_width": 0, "timeout": timeout_s} + if just_started: + return [bg, {"id": START_ANIM_ID, "type": "animation", + "stock_path": f"shared/{cfg['start_animation']}.anim", + "x": 0, "y": 0, "loop": True, "timeout": timeout_s}] + + show_icon = not in_progress and cfg.get("escalation_icons") and state == STATE_WARNING + title_x = ICON_TITLE_X if show_icon else TITLE_X + title_width = PANEL_WIDTH - title_x - 2 + title_text = ascii_safe(event.title).upper() + title = {"id": "title", "type": "text", "text": title_text, "font": "small", + "color": TITLE_COLOR[state], "x": title_x, "y": TITLE_Y, + "width": title_width, "timeout": timeout_s} + if not _title_fits(title_text, title_width): + title.update(scroll_rate=SCROLL_RATE, scroll_start_delay=SCROLL_DELAY_MS, + scroll_repeat_delay=SCROLL_DELAY_MS) + + track_x = ICON_TITLE_X if show_icon else 0 + track_width = PANEL_WIDTH - track_x if in_progress: - track_fill_width = PANEL_WIDTH - track_fill = {"fill": "solid", "fill_colors": [TRACK_FILL_IN_PROGRESS]} + duration = (event.end - event.start).total_seconds() + fraction = (event.end - now).total_seconds() / duration if duration > 0 else 1.0 + fill_width = max(1, min(track_width, round(track_width * fraction))) + fill = {"fill": "gradient_h", "fill_colors": ["#159987FF", TRACK_FILL_IN_PROGRESS]} else: - track_fill_width = _track_fill_width(minutes_left, cfg["progress_window_minutes"]) - track_fill = {"fill": "gradient_h", "fill_colors": TRACK_FILL_GRADIENT[state]} - track_fill_element = { - "id": "track_fill", - "type": "rectangle", - "x": 0, - "y": TRACK_Y, - "width": track_fill_width, - "height": TRACK_HEIGHT, - "border_width": 0, - "timeout": timeout_s, - **track_fill, - } - - elements = [bg_element] - if title_element is not None: - elements.append(title_element) - elements.append(track_element) - elements.append(track_fill_element) + fill_width = max(1, min(track_width, round( + _track_fill_width(minutes_left, cfg["progress_window_minutes"]) * track_width / PANEL_WIDTH))) + fill = {"fill": "gradient_h", "fill_colors": TRACK_FILL_GRADIENT[state]} + track = {"id": "track", "type": "rectangle", "x": track_x, "y": TRACK_Y, + "width": track_width, "height": TRACK_HEIGHT, "fill": "solid", + "fill_colors": [TRACK_COLOR], "border_width": 0, "timeout": timeout_s} + track_fill = {**track, "id": "track_fill", "width": fill_width, **fill} + tip_width = min(2, fill_width) + track_tip = {**track, "id": "track_tip", "x": track_x + fill_width - tip_width, + "width": tip_width, "fill_colors": ["#E9FFFFFF"]} + elements = [bg, title, track, track_fill, track_tip] if in_progress: - elements.append({ - "id": "ends", - "type": "text", - "text": ENDS_TEXT, - "font": "bold", - "color": ENDS_TEXT_COLOR, - "x": ENDS_X, - "y": ENDS_Y, - "timeout": timeout_s, - }) - elif icon_element is None: - # `time` sits at TIME_X=2, TIME_Y=5 (ink rows 7-15), which overlaps - # the escalation icon's 16x16 footprint (x=0..15, y=0..15) -- drop - # it whenever the icon is present (both the warn and imminent - # sub-stages) rather than let the icon occlude its leading digits. - # `cd_text` at CD_TEXT_X=39 already clears the icon and remains the - # sole "how much time" readout in that case. - elements.append({ - "id": "time", - "type": "text", - "text": f"{event.start.astimezone():%H:%M}", - "font": "large", - "color": TIME_TEXT_COLOR, - "x": TIME_X, - "y": TIME_Y, - "timeout": timeout_s, - }) - - if icon_element is None: - # The divider's left neighbor is `time`, which is already dropped - # whenever the escalation icon is present (both the warn and - # imminent sub-stages, see the `elif icon_element is None` branch - # above) -- with `time` gone and `title` narrowed away from it too, - # the divider would be orphaned floating alone. Drop it under the - # same condition rather than let it draw disconnected from anything. - elements.append({ - "id": "divider", - "type": "rectangle", - "x": DIVIDER_X, - "y": DIVIDER_Y, - "width": DIVIDER_WIDTH, - "height": DIVIDER_HEIGHT, - "fill": "solid", - "fill_colors": [DIVIDER_COLOR[state]], - "border_width": 0, - "timeout": timeout_s, - }) - - elements.append({ - "id": "cd_text", - "type": "text", - "text": _format_countdown(minutes_left), - "font": "large", - "color": DIGIT_COLOR[state], - "x": CD_TEXT_X, - "y": CD_TEXT_Y, - "timeout": timeout_s, - }) - - if icon_element is not None: - elements.append(icon_element) - + elements.append({"id": "ends", "type": "text", "text": ENDS_TEXT, "font": "bold", + "color": ENDS_TEXT_COLOR, "x": ENDS_X, "y": ENDS_Y, + "timeout": timeout_s}) + elif show_icon: + elements.append({"id": "starts_label", "type": "text", "text": "IN", "font": "small", + "color": TITLE_COLOR[state], "x": 20, "y": 9, "timeout": timeout_s}) + else: + elements.append({"id": "time", "type": "text", "text": f"{event.start.astimezone():%H:%M}", + "font": "large", "color": TIME_TEXT_COLOR, "x": TIME_X, "y": TIME_Y, + "timeout": timeout_s}) + if not show_icon: + elements.append({"id": "divider", "type": "rectangle", "x": DIVIDER_X, + "y": DIVIDER_Y, "width": DIVIDER_WIDTH, "height": DIVIDER_HEIGHT, + "fill": "solid", "fill_colors": [DIVIDER_COLOR[state]], + "border_width": 0, "timeout": timeout_s}) + + # Keep the calendar's final-minute cue separate from shared ETA formatting. + countdown = "<1m" if not in_progress and 0 < minutes_left < 1 else _format_countdown(minutes_left) + elements.append({"id": "cd_text", "type": "text", "text": countdown, "font": "large", + "color": DIGIT_COLOR[state], "x": CD_TEXT_X, "y": CD_TEXT_Y, + "width": CD_TEXT_MAX_WIDTH, "timeout": timeout_s}) + if show_icon: + icon = ICON_REMINDER if minutes_left <= cfg["imminent_minutes"] else ICON_EVENT + elements.append({"id": CAL_ICON_ID, "type": "animation", "stock_path": f"shared/{icon}.anim", + "x": ICON_X, "y": ICON_Y, "loop": True, "timeout": timeout_s}) return elements diff --git a/integrations/ci_status/README.md b/integrations/ci_status/README.md index a5e4cc8..a120a2f 100644 --- a/integrations/ci_status/README.md +++ b/integrations/ci_status/README.md @@ -149,13 +149,13 @@ running spinner implementation details. ### Failure and Stuck Frames (v1.7) -A failing run draws a full-panel red badge (rounded background + bold -white text) reading `CI FAIL owner/repo #42 · workflow`; a stale-queued -run draws the same layout in amber with black text, reading `CI stuck -owner/repo #42 · workflow`. `#42` is the run's PR number where one -exists, the branch name when it doesn't (push/fork-triggered runs), or -dropped entirely (along with its leading space) when neither is -available. Long text scrolls, same as the running badge. Each failing or +Failure, waiting, and healthy frames share a dark two-row card. A fixed +`CI FAIL`, `CI WAIT`, or `CI OK` heading stays visible while the bold lower +row scrolls the complete `owner/repo #42 workflow` context. Coral, amber, and +mint divider lines distinguish the states; firmware 1.2.3 adds a matching +7×7 status icon in the header. The healthy card reads `ALL CLEAR` without +scrolling. The PR number is replaced by the branch for push/fork runs, or +omitted if neither exists. Each failing or stuck run gets its own frame in the rotation — with several failures or stuck runs across repos, expect several red/amber frames in a row before the rotation reaches the running badge or quota frames. @@ -190,17 +190,15 @@ frames, if any: fits at all, and which word if so, is a width-based decision (see `ci_status/logic.py`'s `_eta_label`); nothing to configure. - **When `running_spinner` is true (default)**, an animated 8×8 spinner is - displayed in the top-right corner of the badge, indicating work in - progress. The spinner uses a device stock animation (`spinner_front_8x8`) - and animates continuously throughout the run. The title's available width - is automatically reduced from 68 to 60 pixels to prevent overlap with the - spinner. Set `running_spinner = false` to disable the spinner and revert - to text-only display (title width restored to full 68 pixels). -2. **GraphQL quota** (`show_quota`): title ribbon `GITHUB GRAPHQL`, a track bar + **When `running_spinner` is true (default)**, the native 8×8 spinner + occupies the lower-right corner. The title keeps its full 68-pixel width; + the ETA and its label have a separate 60-pixel budget. A cyan-to-mint track + and bright endpoint show elapsed progress. The device animates between + ordinary polls; no host animation loop is added. +2. **GraphQL quota** (`show_quota`): title ribbon `GQL LEFT` with a separate `RESET` label, a track bar showing the fraction of the bucket used, and two numerals — percentage *remaining* on the left, reset-in on the right (e.g. `18%` / `42m`). -3. **REST quota** (`show_quota`): identical layout, title ribbon `GITHUB REST`. +3. **REST quota** (`show_quota`): identical layout, title ribbon `REST LEFT` with the same `RESET` label. Each quota frame is built from a single `GET /rate_limit` call, fetched fresh once per `running_poll_seconds` cycle while a run is active — this diff --git a/integrations/ci_status/logic.py b/integrations/ci_status/logic.py index b4bce5e..9555a09 100644 --- a/integrations/ci_status/logic.py +++ b/integrations/ci_status/logic.py @@ -198,14 +198,11 @@ def evaluate_runs(repo: str, runs: list[dict], now: datetime, # same left margin as the title/quota "pct" numeral # Running-badge spinner (v1.6, config-gated via cfg["ci_status"]["running_spinner"]): -# an animated 8x8 stock asset in the panel's top-right corner. RUNNING_TITLE_WIDTH_SPINNER -# reserves that corner from the scrolling title so it never runs underneath the spinner -- -# used in place of RUNNING_TITLE_WIDTH, in both the title element's own width and the -# _title_fits scroll-decision call, whenever show_spinner is on (quota frames never get a -# spinner, so OVERLAY_TITLE_WIDTH there is untouched). +# an animated 8x8 stock asset in the lower-right corner. The title keeps the +# full 68px ribbon; only ETA text narrows to leave the spinner a clear lane. RUN_SPINNER_ID = "run_spinner" SPINNER_STOCK = "shared/spinner_front_8x8.anim" -RUNNING_TITLE_WIDTH_SPINNER = 60 # reserve the top-right 8x8 corner (spinner at x=64) +RUNNING_ETA_WIDTH_SPINNER = 60 # --- running-job badge ----------------------------------------------------------- @@ -216,11 +213,11 @@ def evaluate_runs(repo: str, runs: list[dict], now: datetime, # The track's own groove color is a decorative element, not a text-bearing # surface, so it's allowed to be a touch brighter than the panel background # without violating that rule -- same treatment as the calendar's TRACK_COLOR. -RUNNING_BG_GRADIENT = ["#031A2EFF", "#00060DFF"] -RUNNING_TITLE_COLOR = "#7FDBFFFF" -RUNNING_TRACK_COLOR = "#0F2A42FF" -RUNNING_TRACK_FILL_COLOR = "#29B6F6FF" # spec: "solid cyan" -- one flat color, no gradient -RUNNING_NUMERAL_COLOR = "#66E1FFFF" +RUNNING_BG_GRADIENT = ["#081426FF", "#020611FF"] +RUNNING_TITLE_COLOR = "#FFFFFFFF" +RUNNING_TRACK_COLOR = "#13243BFF" +RUNNING_TRACK_FILL_COLORS = ["#29D6FFFF", "#5CFFB1FF"] +RUNNING_NUMERAL_COLOR = "#8CFFFFFF" # ETA label (v1.5.1): a muted gray-blue, deliberately desaturated and # dimmer than the bright cyan numeral it sits beside -- a secondary-tier @@ -308,7 +305,7 @@ def _format_eta_text(run: dict, median_minutes: float | None, now: datetime) -> return f"~{_format_countdown(eta)}" -def _eta_label(eta_text: str) -> str | None: +def _eta_label(eta_text: str, *, budget: int = RUNNING_LABEL_BUDGET_PX) -> str | None: """"remain" / "left" / None, appended after a remaining-estimate ETA numeral (v1.5.1). @@ -339,7 +336,7 @@ def _eta_label(eta_text: str) -> str | None: return None eta_width = _text_width_px(eta_text) for label in ("remain", "left"): - if eta_width + RUNNING_LABEL_GAP_PX + _text_width_px(label) <= RUNNING_LABEL_BUDGET_PX: + if eta_width + RUNNING_LABEL_GAP_PX + _text_width_px(label) <= budget: return label return None @@ -377,18 +374,16 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo native countdown element, same design lineage as the v1.4 calendar layout. Draw order is z-order, first = behind. - `show_spinner` (v1.6, config-gated): when true, reserves the panel's - top-right 8x8 corner from the title ribbon (RUNNING_TITLE_WIDTH_SPINNER - in place of RUNNING_TITLE_WIDTH, for both the element's own width and - the scroll-fit decision, so the two stay consistent) and appends an - animated spinner element there. Defaults to False so existing callers - are unaffected. + `show_spinner` (v1.6, config-gated): when true, leaves the full title + ribbon intact, limits ETA text to 60px, and appends an animated spinner + in the lower-right corner. Defaults to False so existing callers are + unaffected. """ title_text = _build_running_title(info.run, info.repo, info.other_count) eta_text = _format_eta_text(info.run, info.median_minutes, info.now) elapsed = _elapsed_minutes(info.run, info.now) track_width = _progress_width(elapsed, info.median_minutes) - title_width = RUNNING_TITLE_WIDTH_SPINNER if show_spinner else RUNNING_TITLE_WIDTH + eta_width = RUNNING_ETA_WIDTH_SPINNER if show_spinner else None bg_element = { "id": "bg", "type": "rectangle", "x": 0, "y": 0, @@ -399,9 +394,9 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo title_element = { "id": "title", "type": "text", "text": title_text, "font": "small", "color": RUNNING_TITLE_COLOR, "x": RUNNING_TITLE_X, "y": RUNNING_TITLE_Y, - "width": title_width, "timeout": timeout_s, + "width": RUNNING_TITLE_WIDTH, "timeout": timeout_s, } - if not _title_fits(title_text, title_width): + if not _title_fits(title_text, RUNNING_TITLE_WIDTH): title_element.update({ "scroll_rate": SCROLL_RATE, "scroll_start_delay": SCROLL_DELAY_MS, @@ -414,15 +409,23 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo } track_fill_element = { "id": "track_fill", "type": "rectangle", "x": 0, "y": RUNNING_TRACK_Y, - "width": track_width, "height": RUNNING_TRACK_HEIGHT, "fill": "solid", - "fill_colors": [RUNNING_TRACK_FILL_COLOR], "border_width": 0, "timeout": timeout_s, + "width": track_width, "height": RUNNING_TRACK_HEIGHT, "fill": "gradient_h", + "fill_colors": RUNNING_TRACK_FILL_COLORS, "border_width": 0, "timeout": timeout_s, } numeral_element = { "id": "eta", "type": "text", "text": eta_text, "font": "large", "color": RUNNING_NUMERAL_COLOR, "x": RUNNING_NUMERAL_X, "y": RUNNING_NUMERAL_Y, "timeout": timeout_s, } - elements = [bg_element, title_element, track_element, track_fill_element, numeral_element] + if eta_width is not None: + numeral_element["width"] = eta_width + tip_width = min(2, track_width) + tip_element = { + "id": "track_tip", "type": "rectangle", "x": track_width - tip_width, + "y": RUNNING_TRACK_Y, "width": tip_width, "height": RUNNING_TRACK_HEIGHT, + "fill": "solid", "fill_colors": ["#FFFFFFFF"], "border_width": 0, "timeout": timeout_s, + } + elements = [bg_element, title_element, track_element, track_fill_element, tip_element, numeral_element] # ETA label (v1.5.1): appended only when _eta_label decides it fits # (grammar guard + width check -- see its docstring). This changes @@ -432,7 +435,7 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo # for that: main.py's unified shape tracker (frozenset of element # ids on whatever was last actually drawn) already detects any shape # change and clears first, generically, not just at tier boundaries. - label = _eta_label(eta_text) + label = _eta_label(eta_text, budget=eta_width or RUNNING_LABEL_BUDGET_PX) if label is not None: elements.append({ "id": "eta_label", "type": "text", "text": label, "font": "small", @@ -443,7 +446,7 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo if show_spinner: elements.append({"id": RUN_SPINNER_ID, "type": "animation", "stock_path": SPINNER_STOCK, - "x": 64, "y": 0, "loop": True, "timeout": timeout_s}) + "x": 64, "y": 8, "loop": True, "timeout": timeout_s}) return elements @@ -472,14 +475,14 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo QUOTA_TRACK_COLOR = "#12241EFF" QUOTA_BG_GRADIENT = { - QUOTA_HEADROOM_HIGH: ["#031F17FF", "#000A08FF"], - QUOTA_HEADROOM_MEDIUM: ["#231400FF", "#0A0400FF"], - QUOTA_HEADROOM_LOW: ["#2E0509FF", "#0A0101FF"], + QUOTA_HEADROOM_HIGH: ["#081B23FF", "#020611FF"], + QUOTA_HEADROOM_MEDIUM: ["#211707FF", "#020611FF"], + QUOTA_HEADROOM_LOW: ["#240A10FF", "#020611FF"], } QUOTA_TITLE_COLOR = { - QUOTA_HEADROOM_HIGH: "#6FFFCFFF", - QUOTA_HEADROOM_MEDIUM: "#FFCB6BFF", - QUOTA_HEADROOM_LOW: "#FF6B7AFF", + QUOTA_HEADROOM_HIGH: "#FFFFFFFF", + QUOTA_HEADROOM_MEDIUM: "#FFFFFFFF", + QUOTA_HEADROOM_LOW: "#FFFFFFFF", } QUOTA_TRACK_FILL_COLOR = { QUOTA_HEADROOM_HIGH: "#33FFC1FF", @@ -494,9 +497,9 @@ def _build_running_elements(info: RunningInfo, timeout_s: int, show_spinner: boo # Two numerals share the row (percentage remaining on the left, reset-in on # the right) -- the only overlay frame that does, since the running badge -# has just one. No divider element between them (the brief didn't ask for -# one); RESET_X leaves both enough room for their respective worst cases -# ("100%" on the left, an hours-form countdown on the right). +# has just one. A thin accent separator at x=35 makes the split legible; +# RESET_X leaves both enough room for their respective worst cases ("100%" +# on the left, an hours-form countdown on the right). QUOTA_PCT_X = OVERLAY_TITLE_X # 2 -- same left margin as every other overlay element QUOTA_RESET_X = 40 @@ -574,18 +577,15 @@ def _build_quota_elements(info: QuotaInfo, timeout_s: int) -> list[dict]: "fill": "gradient_v", "fill_colors": QUOTA_BG_GRADIENT[headroom], "border_width": 0, "timeout": timeout_s, } - title_text = ascii_safe(info.label).upper() + title_text = "GQL LEFT" if "GRAPHQL" in info.label.upper() else "REST LEFT" title_element = { "id": "title", "type": "text", "text": title_text, "font": "small", "color": QUOTA_TITLE_COLOR[headroom], "x": OVERLAY_TITLE_X, "y": OVERLAY_TITLE_Y, - "width": OVERLAY_TITLE_WIDTH, "timeout": timeout_s, + "width": 38, "timeout": timeout_s, } - if not _title_fits(title_text, OVERLAY_TITLE_WIDTH): - title_element.update({ - "scroll_rate": SCROLL_RATE, - "scroll_start_delay": SCROLL_DELAY_MS, - "scroll_repeat_delay": SCROLL_DELAY_MS, - }) + reset_label = {"id": "reset_label", "type": "text", "text": "RESET", "font": "small", + "color": "#FFFFFFFF", "x": 45, "y": OVERLAY_TITLE_Y, "width": 25, + "timeout": timeout_s} track_element = { "id": "track", "type": "rectangle", "x": 0, "y": OVERLAY_TRACK_Y, "width": PANEL_WIDTH, "height": OVERLAY_TRACK_HEIGHT, "fill": "solid", @@ -606,7 +606,12 @@ def _build_quota_elements(info: QuotaInfo, timeout_s: int) -> list[dict]: "color": QUOTA_NUMERAL_COLOR[headroom], "x": QUOTA_RESET_X, "y": OVERLAY_NUMERAL_Y, "timeout": timeout_s, } - return [bg_element, title_element, track_element, track_fill_element, pct_element, reset_element] + separator = {"id": "quota_separator", "type": "rectangle", "x": 35, "y": 8, + "width": 1, "height": 7, "fill": "solid", + "fill_colors": [QUOTA_TRACK_FILL_COLOR[headroom]], "border_width": 0, + "timeout": timeout_s} + return [bg_element, title_element, reset_label, track_element, track_fill_element, + pct_element, separator, reset_element] # --- overlay rotation -------------------------------------------------------------- @@ -682,15 +687,16 @@ def build_overlay_payload(descriptor: dict, timeout_s: int, *, return {"elements": _build_quota_elements(info, timeout_s), "priority": PRIORITY_OVERLAY, "led": None} if kind == OVERLAY_FRAME_FAIL: - text = "CI FAIL " + _fail_line(descriptor["repo"], descriptor["run"]) - return {"elements": _badge_elements(text, "#A32D2DFF", "#FFFFFFFF", timeout_s), + text = ascii_safe(_fail_line(descriptor["repo"], descriptor["run"])) + return {"elements": _badge_elements("CI FAIL", text, "#FF7A8AFF", timeout_s), "priority": PRIORITY_OVERLAY, "led": None} if kind == OVERLAY_FRAME_STUCK: - text = "CI stuck " + _fail_line(descriptor["repo"], descriptor["run"]) - return {"elements": _badge_elements(text, "#BA7517FF", "#0B0B0BFF", timeout_s), + text = ascii_safe(_fail_line(descriptor["repo"], descriptor["run"])) + return {"elements": _badge_elements("CI WAIT", text, "#FFCB6BFF", timeout_s), "priority": PRIORITY_OVERLAY, "led": None} if kind == OVERLAY_FRAME_GREEN: - return {"elements": [_text_element("CI ok", "#00FF00FF", timeout_s)], + return {"elements": _badge_elements("CI OK", "ALL CLEAR", "#6FFFCFFF", timeout_s, + scroll_body=False), "priority": PRIORITY_OVERLAY, "led": None} return None @@ -727,17 +733,26 @@ def _text_element(text: str, color: str, timeout_s: int, font: str = "normal") - def _fail_line(repo: str, fr: FailingRun) -> str: - """"owner/repo #42 · workflow" (the ref is dropped when empty).""" + """"owner/repo #42 workflow" (the ref is dropped when empty).""" ref = f" {fr.ref}" if fr.ref else "" - return f"{repo}{ref} · {fr.workflow}" + return f"{repo}{ref} {fr.workflow}" -def _badge_elements(text: str, bg_color: str, text_color: str, timeout_s: int) -> list[dict]: - """Full-panel rounded-rect background + bold scrolling text over it.""" +def _badge_elements(header: str, text: str, accent_color: str, timeout_s: int, *, + scroll_body: bool = True) -> list[dict]: + """Two-row CI status card with a fixed header and full context body.""" # border_width=0: RectangleElement defaults to a 1px white border, which # would draw an unwanted white outline around the badge (verified on-device). bg = {"id": "bg", "type": "rectangle", "x": 0, "y": 0, "width": 72, "height": 16, - "radius": 2, "fill": "solid", "fill_colors": [bg_color], "border_width": 0, + "fill": "gradient_v", "fill_colors": ["#111A2BFF", "#020611FF"], "border_width": 0, "timeout": timeout_s} - return [bg, _text_element(text, text_color, timeout_s, font="bold")] - + header_element = {"id": "ci_header", "type": "text", "text": header, "font": "small", + "color": "#FFFFFFFF", "x": 2, "y": -2, "width": 68, "timeout": timeout_s} + rule = {"id": "ci_rule", "type": "rectangle", "x": 12, "y": 6, "width": 58, "height": 1, + "fill": "solid", "fill_colors": [accent_color], "border_width": 0, "timeout": timeout_s} + body = {"id": "ci", "type": "text", "text": text, "font": "bold", "color": "#FFFFFFFF", + "x": 2, "y": 6, "width": 68, "timeout": timeout_s} + if scroll_body: + body.update({"scroll_rate": 2000, "scroll_start_delay": 1000, + "scroll_repeat_delay": 2000}) + return [bg, header_element, rule, body] diff --git a/integrations/ci_status/main.py b/integrations/ci_status/main.py index e918b7e..99bfe74 100644 --- a/integrations/ci_status/main.py +++ b/integrations/ci_status/main.py @@ -265,7 +265,10 @@ def run_once(client, poller, cfg: dict, now: datetime, overlay_state["frame_index"] = frame_index + 1 overlay_state["last_dwell_end"] = now + timedelta(seconds=OVERLAY_DWELL_SECONDS) - text = next(e["text"] for e in payload["elements"] if e["type"] == "text") + text = next(e["text"] for e in payload["elements"] + if e["type"] == "text" and e["id"] != "ci_header") + header = next((e["text"] for e in payload["elements"] if e["id"] == "ci_header"), "") + text = f"{header} {text}".strip() return f"{text[:40]!r} -> {result.value}" diff --git a/src/busybar/presentation.py b/src/busybar/presentation.py index bb909a3..ffacf5d 100644 --- a/src/busybar/presentation.py +++ b/src/busybar/presentation.py @@ -70,7 +70,7 @@ def commit_frame(state: dict | None, elements: list[dict], priority: int, def ci_bitmap_accent(elements: list[dict], kind: str, timeout_s: int) -> list[dict]: - """Add an inline 7x7 status symbol beside the existing scrolling CI text. + """Add an inline 7x7 status symbol beside the fixed CI header. These are fixed local assets, not a general image parser. The text remains the meaningful fallback if the client must use an older/cloud endpoint mid-draw. @@ -78,10 +78,10 @@ def ci_bitmap_accent(elements: list[dict], kind: str, timeout_s: int) -> list[di rows = _ICON_ROWS.get(kind) if rows is None: return elements - colors = {"fail": "#FFFFFF", "stuck": "#0B0B0B", "green": "#00FF00"} + colors = {"fail": "#FFFFFF", "stuck": "#FFCB6B", "green": "#6FFFCF"} data = "! XPM2\n7 7 2 1\n. c None\nX c " + colors[kind] + "\n" + "\n".join(rows) + "\n" - frame = [{**element, "x": 11, "width": 61} if element["id"] == "ci" else dict(element) + frame = [{**element, "x": 12, "width": 58} if element["id"] == "ci_header" else dict(element) for element in elements] frame.append({"id": "ci_status_icon", "type": "xpmbitmap", "data": data, - "x": 1, "y": 4, "timeout": timeout_s}) + "x": 2, "y": 0, "timeout": timeout_s}) return frame diff --git a/tests/test_calendar_logic.py b/tests/test_calendar_logic.py index 9a8842d..96d88bf 100644 --- a/tests/test_calendar_logic.py +++ b/tests/test_calendar_logic.py @@ -1,3 +1,4 @@ +import pytest from datetime import datetime, timedelta, timezone import sys from pathlib import Path @@ -213,11 +214,11 @@ def test_build_elements_upcoming_shape(): els = build_elements(e, NOW, CFG, timeout_s=90, in_progress=False) by_id = {el["id"]: el for el in els} # v1.4 "airy": no card elements -- time_card and cd_card are gone. - assert set(by_id) == {"bg", "title", "track", "track_fill", "time", "divider", "cd_text"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "time", "divider", "cd_text"} # Draw order is z-order (first = behind); set-membership alone wouldn't # catch a reorder that visually breaks layering. assert [el["id"] for el in els] == [ - "bg", "title", "track", "track_fill", "time", "divider", "cd_text", + "bg", "title", "track", "track_fill", "track_tip", "time", "divider", "cd_text", ] bg = by_id["bg"] @@ -325,12 +326,12 @@ def test_build_elements_in_progress_shape(): e = ev(-5, dur_min=30, title="Standup") els = build_elements(e, NOW, CFG, timeout_s=90, in_progress=True) by_id = {el["id"]: el for el in els} - assert set(by_id) == {"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"} assert "time" not in by_id # no start-time label while in progress # Draw order is z-order (first = behind); see the upcoming-shape test # for why set-membership alone isn't enough to catch a reorder. assert [el["id"] for el in els] == [ - "bg", "title", "track", "track_fill", "ends", "divider", "cd_text", + "bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text", ] bg = by_id["bg"] @@ -341,9 +342,9 @@ def test_build_elements_in_progress_shape(): assert title["text"] == "STANDUP" track_fill = by_id["track_fill"] - assert track_fill["fill"] == "solid" - assert track_fill["fill_colors"] == [TRACK_FILL_IN_PROGRESS] - assert track_fill["width"] == PANEL_WIDTH # no drain while in progress + assert track_fill["fill"] == "gradient_h" + assert track_fill["fill_colors"][-1] == TRACK_FILL_IN_PROGRESS + assert track_fill["width"] == 60 # 25 of 30 minutes remaining ends = by_id["ends"] assert ends["type"] == "text" and ends["font"] == "bold" @@ -763,30 +764,27 @@ def test_icon_present_drops_time_element(): assert any(e["id"] == "divider" for e in no_icon_els) -def test_warn_stage_title_scrolls_in_narrowed_icon_gap(): - # "STANDUP" (7 chars * SMALL_FONT_CHAR_PX=5 -> 35px) fits the full 68px - # TITLE_WIDTH the scroll decision is first made against, but does NOT - # fit the icon-narrowed gap the escalation-icon block later shrinks the - # title into (CD_TEXT_X - ICON_TITLE_X - 2 = 39 - 18 - 2 = 19px). The - # scroll decision must be recomputed against the NARROWED width, or a - # title that's the common case (most real meeting titles) gets no - # scroll flags and is statically clipped in the 19px band -- contradicting - # the code's own "# scroll in the gap" comment (CODE-BUG-A). - warn_ev = _ev(NOW2 + timedelta(minutes=4)) # 4m out -> warn, > imminent - els = build_elements(warn_ev, NOW2, _cfg(), 15, in_progress=False) - title_el = next(e for e in els if e["id"] == "title") - assert title_el["text"] == "STANDUP" - assert "scroll_rate" in title_el - assert title_el["scroll_rate"] == SCROLL_RATE - assert title_el["scroll_start_delay"] == SCROLL_DELAY_MS - assert title_el["scroll_repeat_delay"] == SCROLL_DELAY_MS - -def test_imminent_stage_uses_reminder_icon_and_drops_title(): - ev = _ev(NOW2 + timedelta(seconds=30)) # 0.5m out -> imminent - els = build_elements(ev, NOW2, _cfg(), 15, in_progress=False) - icon = next(e for e in els if e["id"] == CAL_ICON_ID) - assert icon["stock_path"] == f"shared/{ICON_REMINDER}.anim" - assert not any(e["id"] == "title" for e in els) # title dropped at imminent +def test_warning_title_uses_full_reserved_lane_and_scrolls_only_when_needed(): + event = _ev(NOW2 + timedelta(minutes=4)) + frame = {e["id"]: e for e in build_elements(event, NOW2, _cfg(), 15, False)} + assert frame["title"]["text"] == "STANDUP" + assert (frame["title"]["x"], frame["title"]["width"]) == (18, 52) + assert "scroll_rate" not in frame["title"] + event.title = "Quarterly planning review" + frame = {e["id"]: e for e in build_elements(event, NOW2, _cfg(), 15, False)} + assert frame["title"]["scroll_rate"] == SCROLL_RATE + assert frame["title"]["scroll_start_delay"] == SCROLL_DELAY_MS + + +def test_imminent_keeps_event_context_and_reads_less_than_one_minute(): + event = _ev(NOW2 + timedelta(seconds=30)) + frame = {e["id"]: e for e in build_elements(event, NOW2, _cfg(), 15, False)} + assert frame[CAL_ICON_ID]["stock_path"] == f"shared/{ICON_REMINDER}.anim" + assert frame["title"]["text"] == "STANDUP" + assert frame["starts_label"]["text"] == "IN" + assert frame["cd_text"]["text"] == "<1m" + assert _format_countdown(.5) == "0m" # shared CI formatting is unchanged + def test_just_started_returns_takeover_animation(): ev = _ev(NOW2 - timedelta(seconds=10)) @@ -799,3 +797,25 @@ def test_escalation_icons_off_is_unchanged(): ev = _ev(NOW2 + timedelta(minutes=4)) els = build_elements(ev, NOW2, _cfg(escalation_icons=False), 15, in_progress=False) assert not any(e["id"] == CAL_ICON_ID for e in els) + + +@pytest.mark.parametrize("minutes", [0, .01, .5, 1, 4, 15, 60, 90]) +def test_progress_highlight_and_warning_lane_stay_within_track(minutes): + event = _ev(NOW2 + timedelta(minutes=minutes)) + frame = {e["id"]: e for e in build_elements(event, NOW2, _cfg(), 15, False)} + track, fill, tip = (frame[k] for k in ("track", "track_fill", "track_tip")) + assert track["x"] <= fill["x"] <= tip["x"] + assert tip["x"] + tip["width"] <= fill["x"] + fill["width"] <= track["x"] + track["width"] <= 72 + assert tip["height"] == 1 and tip["border_width"] == 0 + assert all(e["timeout"] == 15 for e in frame.values()) + if CAL_ICON_ID in frame: + assert track["x"] >= 18 and frame["title"]["x"] >= 18 + + +def test_active_event_progress_drains_and_handles_invalid_duration(): + event = ev(-15, dur_min=30) + frame = {e["id"]: e for e in build_elements(event, NOW, CFG, 15, True)} + assert frame["track_fill"]["width"] == 36 + event.end = event.start + frame = {e["id"]: e for e in build_elements(event, NOW, CFG, 15, True)} + assert frame["track_fill"]["width"] == 72 diff --git a/tests/test_calendar_loop.py b/tests/test_calendar_loop.py index 6d209a2..0dd34e5 100644 --- a/tests/test_calendar_loop.py +++ b/tests/test_calendar_loop.py @@ -46,7 +46,7 @@ def test_draws_countdown_for_upcoming_event(): assert kwargs["priority"] == PRIORITY_AMBIENT == 20 by_id = {el["id"]: el for el in kwargs["elements"]} # v1.4 "airy": no card elements. - assert set(by_id) == {"bg", "title", "track", "track_fill", "time", "divider", "cd_text"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "time", "divider", "cd_text"} assert by_id["cd_text"]["text"] == _format_countdown((event.start - NOW).total_seconds() / 60) assert by_id["title"]["text"] == "STANDUP" # uppercased after ascii_safe assert "drew" in summary @@ -59,7 +59,7 @@ def test_draws_in_progress_event_targeting_active_over_upcoming(): summary = run_once(client, lambda hours: [active, upcoming], CFG, NOW, dry_run=False) kwargs = client.draw.call_args.kwargs by_id = {el["id"]: el for el in kwargs["elements"]} - assert set(by_id) == {"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"} assert by_id["title"]["text"] == "ACTIVE" assert by_id["cd_text"]["text"] == _format_countdown((active.end - NOW).total_seconds() / 60) assert "time" not in by_id @@ -98,7 +98,7 @@ def test_no_clear_on_first_draw_with_fresh_state(): # (state["last_shape"]) -- see main.run_once's docstring. The upcoming # layout's id set (no icon: CFG has no "escalation_icons" key). assert state["last_shape"] == frozenset( - {"bg", "title", "track", "track_fill", "time", "divider", "cd_text"}) + {"bg", "title", "track", "track_fill", "track_tip", "time", "divider", "cd_text"}) def test_no_clear_across_polls_with_same_state(): client = Mock() @@ -118,7 +118,7 @@ def test_clears_on_upcoming_to_in_progress_transition(): client.clear.assert_called_once_with("calendar_countdown") # v1.6: the in-progress layout's id set ("ends" instead of "time"). assert state["last_shape"] == frozenset( - {"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"}) + {"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"}) def test_clears_on_in_progress_to_upcoming_transition(): client = Mock() @@ -148,8 +148,8 @@ def test_failed_draw_leaves_state_unchanged_and_retries_next_poll(): client = Mock() client.draw.return_value = DrawResult.DRAWN state = {} - upcoming_shape = frozenset({"bg", "title", "track", "track_fill", "time", "divider", "cd_text"}) - in_progress_shape = frozenset({"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"}) + upcoming_shape = frozenset({"bg", "title", "track", "track_fill", "track_tip", "time", "divider", "cd_text"}) + in_progress_shape = frozenset({"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"}) run_once(client, lambda hours: [make_event(23)], CFG, NOW, dry_run=False, state=state) assert state["last_shape"] == upcoming_shape @@ -187,7 +187,7 @@ def test_clear_failure_does_not_block_state_commit_when_draw_succeeds(): run_once(client, lambda hours: [active], CFG, NOW, dry_run=False, state=state) client.clear.assert_called_once_with("calendar_countdown") assert state["last_shape"] == frozenset( - {"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"}) + {"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"}) def test_state_reset_after_no_event_clear(): client = Mock() @@ -541,7 +541,7 @@ def test_shape_change_triggers_clear(): # FakeClient that returns a chosen DrawResult for the takeover draw (the one # carrying START_ANIM_ID) and DRAWN for everything else. -IN_PROGRESS_SHAPE = frozenset({"bg", "title", "track", "track_fill", "ends", "divider", "cd_text"}) +IN_PROGRESS_SHAPE = frozenset({"bg", "title", "track", "track_fill", "track_tip", "ends", "divider", "cd_text"}) class ResultFakeClient: diff --git a/tests/test_ci_logic.py b/tests/test_ci_logic.py index 6337711..49dbbe1 100644 --- a/tests/test_ci_logic.py +++ b/tests/test_ci_logic.py @@ -254,10 +254,11 @@ def test_overlay_ci_badge_shape(): # v1.5.1: a fitting remain-estimate ETA ("~11m" here) picks up the # "eta_label" element too -- see test_eta_label_* below for the full # fit-decision and grammar-guard coverage. - assert set(by_id) == {"bg", "title", "track", "track_fill", "eta", "eta_label"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "eta", "eta_label"} assert [e["id"] for e in payload["elements"]] == \ - ["bg", "title", "track", "track_fill", "eta", "eta_label"] + ["bg", "title", "track", "track_fill", "track_tip", "eta", "eta_label"] assert by_id["eta_label"]["text"] == "remain" + assert all(element["timeout"] == OVERLAY_DWELL_SECONDS for element in by_id.values()) bg = by_id["bg"] assert bg["fill"] == "gradient_v" and bg["border_width"] == 0 @@ -271,8 +272,14 @@ def test_overlay_ci_badge_shape(): assert track["y"] == 6 and track["width"] == 72 and track["border_width"] == 0 track_fill = by_id["track_fill"] - assert track_fill["fill"] == "solid" # spec: "solid cyan", no gradient + assert track_fill["fill"] == "gradient_h" + assert track_fill["fill_colors"] == ["#29D6FFFF", "#5CFFB1FF"] assert track_fill["width"] == _progress_width(3, 14) + tip = by_id["track_tip"] + assert tip["fill_colors"] == ["#FFFFFFFF"] and tip["height"] == 1 + assert tip["width"] == min(2, track_fill["width"]) + assert tip["x"] == track_fill["x"] + track_fill["width"] - tip["width"] + assert tip["timeout"] == OVERLAY_DWELL_SECONDS eta = by_id["eta"] assert eta["font"] == "large" and eta["y"] == 5 # numeral-floor rule: large font @@ -286,7 +293,7 @@ def test_overlay_ci_badge_shape_no_history_has_no_label(): info = running_info(run=run_, median_minutes=None) payload = build_overlay_payload({"kind": OVERLAY_FRAME_CI_BADGE}, OVERLAY_DWELL_SECONDS, running=info) by_id = _by_id(payload["elements"]) - assert set(by_id) == {"bg", "title", "track", "track_fill", "eta"} + assert set(by_id) == {"bg", "title", "track", "track_fill", "track_tip", "eta"} assert by_id["eta"]["text"] == "3m in" def test_overlay_ci_badge_title_scrolls_when_long(): @@ -310,25 +317,32 @@ def test_overlay_quota_gql_shape(): assert payload["priority"] == PRIORITY_OVERLAY by_id = _by_id(payload["elements"]) - assert set(by_id) == {"bg", "title", "track", "track_fill", "pct", "reset"} + assert set(by_id) == {"bg", "title", "reset_label", "track", "track_fill", "pct", "quota_separator", "reset"} assert [e["id"] for e in payload["elements"]] == \ - ["bg", "title", "track", "track_fill", "pct", "reset"] + ["bg", "title", "reset_label", "track", "track_fill", "pct", "quota_separator", "reset"] - assert by_id["title"]["text"] == "GITHUB GRAPHQL" - assert by_id["title"]["font"] == "small" + title = by_id["title"] + assert title["text"] == "GQL LEFT" and title["font"] == "small" + assert (title["x"], title["y"], title["width"]) == (2, -2, 38) + reset_label = by_id["reset_label"] + assert reset_label["text"] == "RESET" and reset_label["font"] == "small" + assert (reset_label["x"], reset_label["y"], reset_label["width"]) == (45, -2, 25) assert by_id["pct"]["text"] == "52%" # floor(2600/5000*100) = 52 assert by_id["pct"]["font"] == "large" # numeral-floor rule assert by_id["reset"]["text"] == "42m" assert by_id["reset"]["font"] == "large" assert by_id["track_fill"]["width"] == _quota_used_width(2400, 5000) assert by_id["track"]["y"] == 6 and by_id["track"]["border_width"] == 0 + assert by_id["quota_separator"]["x"] == 35 and by_id["quota_separator"]["y"] == 8 + assert by_id["quota_separator"]["width"] == 1 and by_id["quota_separator"]["height"] == 7 + assert all(element["timeout"] == OVERLAY_DWELL_SECONDS for element in by_id.values()) def test_overlay_quota_rest_uses_core_bucket(): info = quota_info(label="GITHUB REST", limit=5000, remaining=100, used=4900) payload = build_overlay_payload({"kind": OVERLAY_FRAME_QUOTA_REST}, OVERLAY_DWELL_SECONDS, quota_by_bucket={"core": info}) by_id = _by_id(payload["elements"]) - assert by_id["title"]["text"] == "GITHUB REST" + assert by_id["title"]["text"] == "REST LEFT" assert by_id["pct"]["text"] == "2%" def test_overlay_quota_none_when_bucket_missing(): @@ -344,30 +358,43 @@ def test_overlay_quota_none_when_bucket_missing(): # --- build_overlay_payload: fail/stuck/green frames (descriptor dispatch) ------- -def test_fail_frame_is_red_badge_at_overlay_tier(): +def test_fail_frame_has_coral_divider_and_full_context_at_overlay_tier(): d = {"kind": OVERLAY_FRAME_FAIL, "repo": "o/r", "run": FailingRun("tests", "#42")} payload = build_overlay_payload(d, OVERLAY_DWELL_SECONDS) assert payload["priority"] == PRIORITY_OVERLAY - assert _bg_element(payload["elements"])["fill_colors"] == ["#A32D2DFF"] - t = _text_element(payload["elements"]) - assert t["text"] == "CI FAIL o/r #42 · tests" and t["color"] == "#FFFFFFFF" + by_id = _by_id(payload["elements"]) + assert by_id["bg"]["fill_colors"] == ["#111A2BFF", "#020611FF"] + header = by_id["ci_header"] + assert header["text"] == "CI FAIL" and header["font"] == "small" + assert (header["x"], header["y"], header["width"]) == (2, -2, 68) + assert "scroll_rate" not in header + assert by_id["ci_rule"]["fill_colors"] == ["#FF7A8AFF"] + body = by_id["ci"] + assert body["text"] == "o/r #42 tests" and body["font"] == "bold" + assert (body["x"], body["y"], body["width"]) == (2, 6, 68) + assert all(element["timeout"] == OVERLAY_DWELL_SECONDS for element in by_id.values()) def test_fail_frame_drops_ref_when_empty(): d = {"kind": OVERLAY_FRAME_FAIL, "repo": "o/r", "run": FailingRun("tests", "")} - assert _text_element(build_overlay_payload(d, 10)["elements"])["text"] == "CI FAIL o/r · tests" + assert _by_id(build_overlay_payload(d, 10)["elements"])["ci"]["text"] == "o/r tests" def test_stuck_frame_is_amber_badge_at_overlay_tier(): d = {"kind": OVERLAY_FRAME_STUCK, "repo": "o/r", "run": FailingRun("deploy", "#7")} payload = build_overlay_payload(d, 10) assert payload["priority"] == PRIORITY_OVERLAY - assert _bg_element(payload["elements"])["fill_colors"] == ["#BA7517FF"] - assert _text_element(payload["elements"])["text"] == "CI stuck o/r #7 · deploy" + by_id = _by_id(payload["elements"]) + assert by_id["ci_header"]["text"] == "CI WAIT" + assert by_id["ci_rule"]["fill_colors"] == ["#FFCB6BFF"] + assert by_id["ci"]["text"] == "o/r #7 deploy" -def test_green_frame_is_quiet_text_at_overlay_tier(): +def test_green_frame_is_static_mint_card_at_overlay_tier(): payload = build_overlay_payload({"kind": OVERLAY_FRAME_GREEN}, 10) assert payload["priority"] == PRIORITY_OVERLAY - assert _text_element(payload["elements"])["color"] == "#00FF00FF" - assert not any(e["type"] == "rectangle" for e in payload["elements"]) + by_id = _by_id(payload["elements"]) + assert by_id["ci_header"]["text"] == "CI OK" + assert by_id["ci_rule"]["fill_colors"] == ["#6FFFCFFF"] + assert by_id["ci"]["text"] == "ALL CLEAR" + assert "scroll_rate" not in by_id["ci"] def test_sequence_orders_fail_then_stuck_then_badge_then_quota(): states = [RepoState("o/r", [FailingRun("a", "#1")], [FailingRun("b", "#2")])] @@ -560,6 +587,12 @@ def test_eta_label_falls_back_to_left_when_remain_does_not_fit(): assert _text_width_px("~1h00m") == 40 assert _eta_label("~1h00m") == "left" +def test_eta_label_uses_spinner_budget_when_requested(): + # The spinner shares the lower-right lane. At its 60px ETA budget, + # "remain" no longer fits beside a short ETA, while "left" still does. + assert _eta_label("~59m", budget=60) == "left" + assert _eta_label("~1h00m", budget=60) is None + def test_eta_label_omitted_when_neither_fits(): # Synthetic, deliberately-wide input -- _format_eta_text/_format_countdown # never actually produce a string this wide in practice (the h+mm full @@ -615,14 +648,15 @@ def _running(): repo="me/repo", other_count=0, median_minutes=8.0, now=SPINNER_NOW) -def test_spinner_present_and_title_reserved_when_on(): +def test_spinner_preserves_full_title_and_narrows_eta_when_on(): p = build_overlay_payload({"kind": OVERLAY_FRAME_CI_BADGE}, 10, running=_running(), show_spinner=True) els = p["elements"] spin = next(e for e in els if e["id"] == RUN_SPINNER_ID) assert spin["type"] == "animation" and spin["stock_path"] == "shared/spinner_front_8x8.anim" - assert spin["x"] == 64 and spin["y"] == 0 + assert spin["x"] == 64 and spin["y"] == 8 title = next(e for e in els if e["id"] == "title") - assert title["width"] == 60 # reserved so the scrolling title never runs under the spinner + assert title["width"] == 68 + assert next(e for e in els if e["id"] == "eta")["width"] == 60 def test_no_spinner_and_full_title_when_off(): p = build_overlay_payload({"kind": OVERLAY_FRAME_CI_BADGE}, 10, running=_running(), show_spinner=False) diff --git a/tests/test_ci_loop.py b/tests/test_ci_loop.py index e8971e3..a6d2823 100644 --- a/tests/test_ci_loop.py +++ b/tests/test_ci_loop.py @@ -29,9 +29,9 @@ def _run(conclusion: str) -> dict: "conclusion": conclusion, "created_at": "2026-08-03T13:30:00Z"} -def _drawn_text(client) -> str: +def _drawn_texts(client) -> list[str]: els = client.draw.call_args.kwargs.get("elements") or client.draw.call_args.args[1] - return next(e["text"] for e in els if e.get("type") == "text") + return [e["text"] for e in els if e.get("type") == "text"] def test_draws_red_on_failure(): @@ -40,7 +40,8 @@ def test_draws_red_on_failure(): summary = run_once(client, poller, CFG, NOW, {}, dry_run=False) assert client.draw.call_args.kwargs["priority"] == PRIORITY_OVERLAY # 21, not 60 assert client.draw.call_args.kwargs["led_notification_color"] == "#FF0000FF" - assert "FAIL" in summary + assert "CI FAIL" in summary + assert CFG["ci_status"]["repos"][0] in summary def test_failure_rotates_with_no_running_job(): @@ -95,7 +96,8 @@ def test_green_folds_into_rotation_at_overlay_tier(): cfg = {"ci_status": {**CFG["ci_status"], "show_green": True}} run_once(client, poller, cfg, NOW, {}, dry_run=False, overlay_state={}) assert client.draw.call_args.kwargs["priority"] == PRIORITY_OVERLAY - assert "ok" in _drawn_text(client) + assert "CI OK" in _drawn_texts(client) + assert "ALL CLEAR" in _drawn_texts(client) def test_clears_when_green(): @@ -296,10 +298,10 @@ def test_overlay_state_resets_when_run_ends(): # --- unified shape tracking across alert / quiet-green / overlay tiers ---------- def test_overlay_then_alert_clears_stale_overlay_shape(): - # Running badge draws first (shape {bg,title,track,track_fill,eta}); + # Running badge draws first (shape {bg,title,track,track_fill,track_tip,eta}); # the next poll turns up a failure. Once the rotation's next dwell slot - # lands on the failure frame (shape {bg,ci}), the stale - # title/track/track_fill/eta ink from the badge must be cleared first -- + # lands on the failure frame (shape {bg,ci_header,ci_rule,ci}), the stale + # title/track/track_fill/track_tip/eta ink from the badge must clear first -- # not left to linger until its own ~1.5x-poll timeout. Both the failure # frame and the running badge are now part of the SAME dwell-gated # rotation (Request B), so `frame_index` is pinned to 0 to deterministically @@ -325,9 +327,9 @@ def test_overlay_then_alert_clears_stale_overlay_shape(): assert client.draw.call_args.kwargs["priority"] == PRIORITY_OVERLAY def test_alert_then_overlay_clears_stale_alert_shape(): - # Symmetric direction: an alert draws first (shape {bg,ci}); once it + # Symmetric direction: an alert draws first (shape {bg,ci_header,ci_rule,ci}); once it # resolves and a run is active, the running badge's shape ({bg,title, - # track,track_fill,eta}) differs and must clear the alert's stale + # track,track_fill,track_tip,eta}) differs and must clear the alert's stale # elements first. The second poll must land at or beyond a full dwell # (Request B: the failure frame is dwell-gated like every other overlay # frame now, unlike the old unconditionally-drawing alert-tier path). @@ -349,11 +351,8 @@ def test_alert_then_overlay_clears_stale_alert_shape(): client.clear.assert_called_once_with("ci_status") def test_quiet_green_then_overlay_clears_stale_green_shape(): - # Quiet "CI ok" text (shape {ci}, no bg) draws first when show_green - # is on and nothing is running; once a run starts, the badge's shape - # differs (it has a bg + several more ids) and must clear first, or - # the old green text -- drawn with a ~1.5x-poll timeout, e.g. 180s at - # the default -- would linger behind/around the badge for minutes. + # The green card draws first; once a run starts, the badge's shape + # differs and must clear before drawing its own full frame. # Green now draws at the overlay tier too, so the second poll must # land at or beyond a full dwell for its draw to even be attempted. client = Mock(); client.draw.return_value = DrawResult.DRAWN @@ -364,7 +363,7 @@ def test_quiet_green_then_overlay_clears_stale_green_shape(): summary = run_once(client, poller, CFG_GREEN, NOW, {}, dry_run=False, running_cache={}, overlay_state=overlay_state) client.clear.assert_not_called() - assert overlay_state["last_shape"] == frozenset({"ci"}) + assert overlay_state["last_shape"] == frozenset({"bg", "ci_header", "ci_rule", "ci"}) assert client.draw.call_args.kwargs["priority"] == PRIORITY_OVERLAY poller.fetch_running_runs.return_value = [_running_run()] @@ -402,7 +401,7 @@ def test_rotation_cycles_ci_badge_then_quota_frames(): if "eta" in by_id: seen.append("ci_badge") else: - seen.append("quota_gql" if by_id["title"]["text"] == "GITHUB GRAPHQL" else "quota_rest") + seen.append("quota_gql" if by_id["title"]["text"] == "GQL LEFT" else "quota_rest") t += timedelta(seconds=2 * OVERLAY_DWELL_SECONDS + 1) assert seen == ["ci_badge", "quota_gql", "quota_rest"] @@ -711,11 +710,11 @@ def test_overlay_dwell_rejected_during_calendar_elevation_resumes_after(): running_cache={}, overlay_state=overlay_state) assert "drawn" in summary2 assert overlay_state.get("last_dwell_end") is not None - assert overlay_state["last_shape"] == frozenset({"bg", "title", "track", "track_fill", "eta"}) + assert overlay_state["last_shape"] == frozenset({"bg", "title", "track", "track_fill", "track_tip", "eta"}) assert client.draw.call_count == 2 # both attempts drew (1st rejected, 2nd landed) -- no crash anywhere -def test_modern_ci_preserves_text_and_icon_when_failure_background_is_removed(): +def test_modern_ci_draws_complete_green_card_after_failure(): client = Mock() client.supports_display_v2 = True client.remove_elements.return_value = True @@ -730,8 +729,10 @@ def test_modern_ci_preserves_text_and_icon_when_failure_background_is_removed(): assert state["last_priority"] == PRIORITY_OVERLAY poller.fetch_runs.return_value = [_run("success")] run_once(client, poller, cfg, NOW + timedelta(seconds=21), {}, False, overlay_state=state) - client.remove_elements.assert_called_once_with("ci_status", ["bg"]) + client.remove_elements.assert_not_called() client.clear.assert_not_called() frame = client.draw.call_args.args[1] - assert {e["id"] for e in frame} == {"ci", "ci_status_icon"} + assert {e["id"] for e in frame} == {"bg", "ci_header", "ci_rule", "ci", "ci_status_icon"} + by_id = {e["id"]: e for e in frame} + assert by_id["ci"]["text"] == "ALL CLEAR" assert all(e["timeout"] == 10 for e in frame) diff --git a/tests/test_nyan_asset.py b/tests/test_nyan_asset.py index 241f627..5d9d3ff 100644 --- a/tests/test_nyan_asset.py +++ b/tests/test_nyan_asset.py @@ -1,6 +1,7 @@ import json from pathlib import Path from tools.anim_encoder import parse_header +from tools.build_nyan_anim import FRAMES, H, W, render_frame, render_frames ASSET = Path(__file__).resolve().parents[1] / "assets" / "nyan" / "nyan_72x16.anim" META = Path(__file__).resolve().parents[1] / "assets" / "nyan" / "meta.json" @@ -10,5 +11,25 @@ def test_committed_asset_parses(): assert h["magic"] == b"bicycle0" assert (h["width"], h["height"]) == (72, 16) assert h["color_mode"] == 0 - assert h["n_display"] >= 1 + assert h["n_display"] == FRAMES assert h["fps"] == json.loads(META.read_text())["fps"] + + +def test_renderer_is_periodic_and_pixel_bounded(): + frames = render_frames() + assert len(frames) == FRAMES == 24 + assert all(len(frame) == W * H for frame in frames) + assert all(0 <= channel <= 255 for frame in frames for pixel in frame for channel in pixel) + assert frames == render_frames() + assert all(render_frame(t) == render_frame(t + FRAMES) for t in range(FRAMES)) + + +def test_generated_frames_have_panel_dimensions(): + from PIL import Image + + frame_paths = sorted((ASSET.parent / "frames").glob("frame_*.png"), + key=lambda path: int(path.stem.split("_")[1])) + assert len(frame_paths) == FRAMES + for path in frame_paths: + with Image.open(path) as image: + assert image.size == (W, H) diff --git a/tests/test_presentation.py b/tests/test_presentation.py index 94d4f92..3d5345b 100644 --- a/tests/test_presentation.py +++ b/tests/test_presentation.py @@ -134,13 +134,22 @@ def test_capability_needs_positive_evidence(): @pytest.mark.parametrize("kind", ["fail", "stuck", "green"]) def test_bitmap_has_transparent_margin_and_does_not_obscure_fallback_text(kind): - original = [text_element("ci")] + original = [ + {**text_element("ci_header"), "x": 2, "y": -2, "width": 68}, + {**text_element("ci"), "x": 2, "y": 6, "width": 68}, + ] frame = ci_bitmap_accent(original, kind, 10) icon = frame[-1] lines = icon["data"].splitlines() assert lines[:3] == ["! XPM2", "7 7 2 1", ". c None"] assert len(lines[4:]) == 7 and all(len(row) == 7 for row in lines[4:]) - assert icon["x"] + 7 <= frame[0]["x"] - assert frame[0]["x"] + frame[0]["width"] == 72 + assert icon["x"] == 2 and icon["y"] == 0 + assert frame[0]["x"] == 12 and frame[0]["width"] == 58 + assert frame[1]["x"] == 2 and frame[1]["width"] == 68 + color = {"fail": "#FFFFFF", "stuck": "#FFCB6B", "green": "#6FFFCF"}[kind] + assert f"X c {color}" in lines assert icon["timeout"] == 10 - assert original == [text_element("ci")] + assert original == [ + {**text_element("ci_header"), "x": 2, "y": -2, "width": 68}, + {**text_element("ci"), "x": 2, "y": 6, "width": 68}, + ] diff --git a/tools/build_nyan_anim.py b/tools/build_nyan_anim.py index 909c10c..588898f 100644 --- a/tools/build_nyan_anim.py +++ b/tools/build_nyan_anim.py @@ -1,7 +1,7 @@ """Render a fixed Nyan Cat loop and compile it to a bicycle0 .anim. Renderer geometry adapted from the community reference app -(maxswinkels/busybar-apps, apps/nyan-cat). Deterministic (seeded RNG) so the +(maxswinkels/busybar-apps, apps/nyan-cat). Deterministic and analytical so the committed asset is reproducible. Build-time only. uv run python -m tools.build_nyan_anim @@ -9,7 +9,6 @@ from __future__ import annotations import json -import random from pathlib import Path from PIL import Image @@ -20,36 +19,55 @@ FRAMES = 24 # ~2s loop at 12 fps FPS = 12 +BACKGROUND=(0x04,0x06,0x14) CRUST=(0xFF,0xCC,0x99); FROSTING=(0xFF,0x99,0xFF); SPRINKLE=(0xDD,0x33,0x88) GRAY=(0x99,0x99,0x99); BLACK=(0,0,0); CHEEK=(0xFF,0x99,0x99); STAR=(0xFF,0xFF,0xFF) RAINBOW=[(0xFF,0,0),(0xFF,0x99,0),(0xFF,0xFF,0),(0x33,0xFF,0),(0,0x99,0xFF),(0x66,0x33,0xFF)] CX,BY=44,3; HX,HY=CX+9,5; TRAIL_END=CX-5 -def _blank(): return [(0,0,0)]*(W*H) +def _blank(): return [BACKGROUND]*(W*H) def _rect(buf,x,y,w,h,rgb): x2,y2=min(W,x+w),min(H,y+h); x,y=max(0,x),max(0,y) for yy in range(y,y2): base=yy*W for xx in range(x,x2): buf[base+xx]=rgb -def _stars_state(): return [{"x":8,"y":3,"p":0},{"x":26,"y":13,"p":2},{"x":46,"y":1,"p":1},{"x":66,"y":11,"p":3}] -def _tick_stars(buf,stars,rng): +def _stars_state(): + return [{"x":8,"y":3,"offset":0},{"x":26,"y":13,"offset":2}, + {"x":46,"y":1,"offset":1},{"x":66,"y":11,"offset":3}] + +def _plot(buf,x,y,rgb): + """Plot a star pixel with horizontal wrapping at the panel edge.""" + if 0 <= y < H: + buf[y*W + (x % W)] = rgb + +def _star_pixels(x,y,phase): + if phase == 0: + return ((x,y),) + if phase == 1: + return ((x,y),(x-1,y),(x+1,y),(x,y-1),(x,y+1)) + if phase == 2: + return ((x,y),(x-2,y),(x-1,y),(x+1,y),(x+2,y), + (x,y-2),(x,y-1),(x,y+1),(x,y+2)) + return ((x-2,y),(x+2,y),(x,y-2),(x,y+2)) + +def _tick_stars(buf,stars,t): for s in stars: - s["x"]-=3; s["p"]=(s["p"]+1)%4 - if s["x"]<-2: s["x"]=W+rng.randint(0,10); s["y"]=rng.randint(1,H-2) - x,y,p=s["x"],s["y"],s["p"] - if p==0: _rect(buf,x,y,1,1,STAR) - elif p==1: _rect(buf,x-1,y,3,1,STAR); _rect(buf,x,y-1,1,3,STAR) - elif p==2: _rect(buf,x-2,y,5,1,STAR); _rect(buf,x,y-2,1,5,STAR) - else: - for dx,dy in ((-2,0),(2,0),(0,-2),(0,2)): _rect(buf,x+dx,y+dy,1,1,STAR) + x=(s["x"] - 3*t) % W + phase=(t + s["offset"]) % 4 + for px,py in _star_pixels(x,s["y"],phase): + _plot(buf,px,py,STAR) def _rainbow(buf,phase): for band,color in enumerate(RAINBOW): y=2+band*2; x=0 while x list[tuple[int, int, int]]: + """Render frame ``t``; all motion is periodic analytical state.""" + phase=(t//3)%2 + buf=_blank() + _tick_stars(buf,_stars_state(),t) + _rainbow(buf,phase) + _cat(buf,phase) + return buf + def render_frames(): - rng=random.Random(1) # deterministic - stars=_stars_state(); frames=[] - for t in range(FRAMES): - phase=(t//3)%2; buf=_blank() - _tick_stars(buf,stars,rng); _rainbow(buf,phase); _cat(buf,phase) - frames.append(buf) - return frames + return [render_frame(t) for t in range(FRAMES)] def main(): out_dir=Path(__file__).resolve().parents[1]/"assets"/"nyan"