From 33df77e27b180f777782c7803651022e22602dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Wed, 6 Oct 2021 16:29:38 +0300 Subject: [PATCH] Refactor and fix IRC formatting handling (#360) This PR implements IRC formatting parsing, as described in https://modern.ircdocs.horse/formatting.html Previously we were only parsing the color codes (starting with `\x03`), rest of the formatting characters were ignored. We now parse all formatting characters, but formatting characters other than two-digit color codes, "reverse" (reverses current background and foreground colors), and "reset" (resets formatting to default) are currently ignored. Handling those is left for another PR. Logger now filters out all control characters before writing to a file. I also refactored mapping IRC colors to termbox colors. Previously, for some colors, we used ANSI terminal colors in range 0-15, but those colors change depending on the terminal color scheme (not sure if this is part of the ANSI standard, or just something terminals do to implement color schemes). So we now map IRC color codes to ANSI colors in range 16-255. --- ARCHITECTURE.md | 13 + CHANGELOG.md | 6 + Cargo.lock | 2 + assets/README.md | 6 + assets/crate_deps.dot | 2 + assets/crate_deps.png | Bin 15614 -> 18039 bytes crates/libtiny_logger/Cargo.toml | 1 + crates/libtiny_logger/src/lib.rs | 2 + crates/libtiny_tui/Cargo.toml | 1 + crates/libtiny_tui/src/config.rs | 4 +- crates/libtiny_tui/src/msg_area/line.rs | 113 +++-- crates/libtiny_tui/src/notifier.rs | 4 +- crates/libtiny_tui/src/utils.rs | 103 ----- crates/libtiny_wire/src/formatting.rs | 535 ++++++++++++++++++++++++ crates/libtiny_wire/src/lib.rs | 2 + 15 files changed, 631 insertions(+), 163 deletions(-) create mode 100644 assets/README.md create mode 100644 crates/libtiny_wire/src/formatting.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8c56c4bc..776ca380 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -103,6 +103,19 @@ for user input events and a TUI handle to update the TUI. The types are: | term_input | Input handling (reading events from `stdin`) | | termbox_simple | Terminal manipulation (drawing) | | libtiny_common | The "channel name" type | +| libtiny_wire | Parsing IRC message formatting characters (colors etc.) | + +### libtiny_logger + +Implements logging IRC events (incoming messages, user left/joined etc.) to +user-specified log directory. + +#### Dependencies of `libtiny_logger`: + +| Dependency | Used for | +| -------------- | ------------- | +| libtiny_common | The "channel name" type | +| libtiny_wire | Filtering out IRC message formatting characters (colors etc.) | ### libtiny_wire diff --git a/CHANGELOG.md b/CHANGELOG.md index 5930c73d..7de69b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ - `/join` (without arguments) now rejoins the current channel. (#334) - Key bindings can be configured in the config file. See the [wiki page][key-bindings-wiki] for details. (#328, #336) +- Handling of IRC formatting characters (colors etc.) in TUI and logger + improved: + - TUI now handles "reset" control character, to reset the text style to the + default. + - Logger now filters out all control characters before writing to the file. + (#360) [key-bindings-wiki]: https://github.com/osa1/tiny/wiki/Configuring-key-bindings diff --git a/Cargo.lock b/Cargo.lock index 273428d5..de39aa3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,6 +359,7 @@ name = "libtiny_logger" version = "0.1.0" dependencies = [ "libtiny_common", + "libtiny_wire", "log", "time", ] @@ -370,6 +371,7 @@ dependencies = [ "bencher", "libc", "libtiny_common", + "libtiny_wire", "log", "mio", "notify-rust", diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 00000000..6b6643f3 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,6 @@ +Update `crate_deps.png` with: + +``` +dot -Tpng crate_deps.dot > crate_deps.png +optipng crate_deps.png +``` diff --git a/assets/crate_deps.dot b/assets/crate_deps.dot index 5c161bb2..70b88224 100644 --- a/assets/crate_deps.dot +++ b/assets/crate_deps.dot @@ -24,8 +24,10 @@ digraph mygraph { "libtiny_client" -> "libtiny_wire" "libtiny_logger" -> "libtiny_common" + "libtiny_logger" -> "libtiny_wire" "libtiny_tui" -> "libtiny_common" + "libtiny_tui" -> "libtiny_wire" "libtiny_tui" -> "term_input" "libtiny_tui" -> "termbox_simple" diff --git a/assets/crate_deps.png b/assets/crate_deps.png index 0022296e85085c63ee7ca8ac653819eca3213bf2..e953bcf2168decc53b66368f875916f29338ead9 100644 GIT binary patch literal 18039 zcma&O1yqya|2PUFNK870l!S~B7%8JvQo@na-6=3ox;q5v8XzDHln&|cPAO?dcSy$_ zet+@5_nvdlJ!fYep7(h_{luphrUaFFiu()~4Grz7>^n&nG&Bq-8XCF|_G8q@I}UO@ z)SpMj@-mWW_aV^(&1h)SU|GqxYOb?;=~lj^zZzN3<0<8PKH!&d>8H$WtsKV0m~)zv%BB|6>u#@= zDn-YCi#C4v^x^M80oVI+)lb_uJ8;DIgcd*?n?A4`-1mq+kT5om5cMAfhd%HtToS!B zekSA3!yt6@*4PZA)U3iDalj9Ylk%4gSd^yL6_{;wByIqlr)) zrNnC+a8Z`O4f-Kh0WVu&>;JBP^t0vLuaQJ>3m@`OC{y}RDZ}`gLn0!hy z{BgkcHw=jab>PS{HDw7=C#{~@hMpxt0Kq>54AX_kr_X9q=$u5ZN9T!^xxFacO*xkZ z%um=d%YKvaX!>V8-FflACK1!;(s&KG(EHqQ6QCRwAQ;>TY~Q+p|G6Z&H~c= zmCtqrT!r@Zo;z z?>@jNSw>j$WllKuE%1HdeEvF#ULuN$px$TX;5e(uD)Nx&bR0EaUP8rpgEOtv_-)`| z3Xyr81FxuV2bS^8_sL~^k2xO}GD9ox7z6Fv2DOaA&!l;yn^1Rs6TLUXu1}r<7&{&> zgvR+zAs-(;3n4aVJLgFx49H)T*ZYNf@9l@N5RpDJYAFi<(U}qMt3$p_%$tU4aGWR( zFROX)r`M5A*5CY#CywZTY{}uoC8xFC?4UNL-kg^f`1}grgo7D^m1Sv8rZovSn*4;^ zRmES>RkC4}^x{l{zhE=E1q||mvD%faFSU~@QGx)+ld^(e?3vPh+Lad0E1OWqist#l z`{y*U+%tqbumH(!i2k86CsJ-<43jeui3BC`q{&14qk!QMU#Hr*DLe4%D?V6YBxKk*Kz&wM=)Ul$UxoVHrGjoDLmC8 z_rprw#aeUkqHNBZR(DW})KRn25r&>XYSU7TQt7KpIAj8xC;$}VNqn}BG(*QdIOHB@ z$??}t%3z!GbgA-Pu3XNu>^@GI;CX}bIp{0Aq3tudisFlQY$M>i7k^)>z2+oWkqx|? zsJ$52{Ma3CH=)vp&TS9>3{H}LR>~fTj8|+j#ZPh)wBvR*r=B_#vH66Fc8H%qTK0-p zPaocXtM7B*1b(5rXluC7W6f`1nKH0Qpp}`XH>Q(`4c>Sb&*`0+|H2B(T%$W+sT@=8j~16xam@DGsJyKI5wTx?Gvd zxqnT-_YX8{6H`RCcO4pcdU}aphX|MBm})d(*Ek0;LrWIuO2-U;^oYcjc)&Pyy0}3)d0@! z>Cf7hk9iH^2_%KaB1G22-{1Zd8_$aTvwY)M05XMau|w5T^LtyX*Q)xoxCVH$~xuzPc9CR2NjB6!PL-s@k=2WPw{@+dfF)P9$-M8R8E%NlxYfnZ zdZ&%T#sY*iwmM%K0|&?&iY~st7qjTY^M@N^fRws&tZss^c5#U;AzlMql>SDF-t-)1 zEO|FW@e}|^>t{~p0UE2@FAp=`K#Gi*O>jno*wzbf#V{JURrQsSkCkLXw=3`EP)M*t z;lEBSn3bg|5y$btugqDPO9tX8m>{jUk~J&=6ILi0izSV#6z37pSfvZ1B`<~I;V>C4022E~J$)@bW{{S=EE?dl2wR8H3ppB{2&> zAM-xEa`@7#z?#<~*ALcn{2alrE3YARjMt<>YvI?7dj1Q(#3Sb_hVYm`J=cvNSKnqu zVEpzhx*3#H@J?gp+JsrJ0E9Lg{2tWkAO%(A2~^=6&-T^&pm7I(cHq zZEqnQOV|Js72U%3(-FPRyla>S&_hV$WroDn2nsy)*YPR1M6!hId$&}nHRM6cv5FZ6 zUg4fJ;G}5y;EL(5{a7L;{=UHj8(DDLEO^F%x=~+t+u6N>TS0jgkHXw%fBUg}H2MsO zQ*6`*BZ-f!gp=SX7wi*gsZF*fvvPfc(i4LaT4Hkd1e+`38<;4fVpY5N9EJBN+t~Yh z0XSnQNGFf8$gSB-0;0#7Q@kJ(2~l_`SWJjWw#g&O8Xm(6a=})7&WZxh00C9~ZQCpf zUVbvCy5XTE(OIR{*-;hB(x2&hSy^Ty~Qi>jxtW!Y)IWmFmYHc@3vOW@x#;xW7JK zgLuk4DIREdTz#@zjNk|ccZy^)fN(XS2C(OA&xYf(Q-8eVrxf2EItNE0qs6FPw*eI0 ze7m=Uh>b%V2Dfe5DbJRDJyNNX@@IP5+2FNPA`hN7c!u9EoE^;7-V3uwDU1BHEpru3 z<*L6_pA-hR`?_M6rE2PaTTHW*&s%(0?+<8?}NbsYIYqX1OZtc zqnG6HDG3~xHGWL0{Ewp?&7mZf3zaM{A%h6U?o$2Q#F*623%|vR3&UV9I)MQD>#LV} z;{p6`?_1T_>6+&msC(G-46~Jxr(AVM^On)PF_C+}ta$=wbmrd~X9xj;{ls2J{B~YF z7NNcWDagOKYs#aE=MdWVbsXmI82AiNMQway6K{JOkw?q`%uvRXq6}WvUd-$)ieFQH z{Ay+ai<04ViAK+uVG}mn@h*6_(4-=MW>(v_LU6)eE?l#uK*ISO-4jRZ1YHHa)NA=C z&7G^x3bU6n2OJq3fb~cbvLzZoq#Gw!cCs;pvR(o#SI#um?A?U|)M7kAN?`G|xyNuG zN0N?^9t&yoO-GKOL&my=r7`9G2Q`>xa9|1Y6ilC?ph?MgIGA?&Yu&*<+Uz;Vr*oCA zNzN|&#?IJ&sLzJmTTO}!Y$pEwB_c?cTtjuK@k1$Tv}fn4clf0yEEXQ<0kbxx>tcdi z%lIY1NeO(5PB*$^<4~SY+&EyiOcOOI$$sha#H{cuoC&lX1=r6BttWHsm90!|PMhJd zUrlEMC4@bS8z&EjDU7|%!EHc(cimla7h1C_M!7C#*)7P^=1My=hpb0j5LVjyi{Q^F zH$fKKW(Dz*6E4cdn-n#{z4HC}*vWd&Ig%_gqe z52R>cdZ2B{Lu2C{RG=~r`6!rSd?)A9b(OOV&rwhrOdz5GvR1~F{{R?YMiMH@K1`N_ z=A?XUk%uw_+=wRAy?YpvhH@cpv|m-UaG3&c-54TFEt*4i9T}hQ_pkzw4&|gYBPubdR(ep<8gkgx_9Pd+Zgo&mKdK4&Oq&$< zDko#pG7M>&gw@O10!!@}<>X_0|An2L#uZDml?8x69EQH;FV-YIp)O7K1{Px)(gxE` ziidpwa>~P)xlnGpe9M$VWbH8Y_T_#@DIFd9PY8|)|K2oWz-G{WAtJ=f-6z_}W#Lhx zBVjOJjttd6NXzvu*_ZSOYObgQjBu%DT1&t{ax0CQ1$Y0}TcolVWnhYn4A^?7t@TDXf+O+h2RJ zg1990V8p**?r?^t9(glYtAX4Z(Hvv$_N$wB15U#n>W*&Yaiv?fpx2!a66zM*_!1UB zebihc`Db4!_cd@LJiFV)8dt#s;qo)Bz#W?2A!=YIQs(t$~s2 zXcFvLeBuSjh#f)a@J`!jU%_meBga_T&MSjV^`*M9IHir?llD)t!^oAwHD`s0q}s;V zya7d6pNz=vxQ7BBc2=*T+R-AR2r7_`)Wc8$BZ*++# z`snIT48grtnVs`M_hW(5;Qr0dQ+1WZi#d2~T;_^V=E61j6sc!OnFCfsFHNN#HLuH* zj5fb7@`{JCBe=L5kd%;1j_o6vwCF636FPwZU~_`ioE+M`H*KO!oaCtR%{$zAk*EskkwEU#rSZyPW8oZ= zuBCXL^q9EZ!nIlm7s^=LIU9uqUkZKh9T63JwnqZ}PWR*(>=Oz}oky*Lz9RVDmt{SI z-d=maxevi<$ZtLj*);&f~#!Ap7G-4x@ zg$07C1pUJzd*!-2)k5dL?3e5lUAbS=G~aY)I~!1w5f^uK5h`l3Aot>avetD@ief6_ zD2FY`dJ?Y~F4j@{+`-S_A--hNv_;|4T_p!Qb`w!odK>hqdOp)W)_ccOg!7$SZ;E(~ zs+c_Ls6m5>1{v8cJ@R=!kpJnPNCW~QiOvNA=-C9*$cj8sv!xxvkjRt=s@_09?dCn4 zY5h0zk~jpi&?XLvwg6aa>pRrpN36AeU;uuZfI72dgdyp@kN<1z#co`P@k>a7!edS7 zs-VWhk(ziZ%+%^${&4>C#E%2Le>}kadE-UWzVk2!V?`|A=srwTzx1HQ(9v4dKwTRo zQ(?0aKOgP^rZLTbw%XU+Si555`XqhV_?XOb93QZ4Z5(7`A7!Cpv|)37R+)TLFcs8c z-hp}-zEI!TIqMzJe z{uga(GC>bFhREY{+&X@pl6Mc-hLERB!A~h4-zl5hI9aA;_`%`f-HhQfZLWp6yi*%h z!21d2s%Ue<)ezh!01iRlmL33)_iLPhMKw)$$i}K8L{Bqr%D9k2!|a`_67-8?vfkGe z2iC^v)QU#CWq0?HjLajqH^&wBM}I;_$enV4oA^d+O_IeN8#Uk>W!-M^okJP`pq(N*O*efAT5b zA)c{+qp0)H7Ogll4w%}WmS%ea*TymgwhmOf;}_?I6MnEAP!WTq&9sk-s zXp0#)g|dv;Cii4P7B|Svc|wWVPI&%k7gu@#J0}DuULL;)PV)5GYY1FUX+UI$)hc8s z52+pLr~1_UwmMrt@l{n@_wX|hbeklSWnZe(=mTMOF&UVtPq~aQ5E$3K+QF}hD!9Evvy)a3vRSc*8R};K|cI@qO?=!3NjL4)7J}Ix-8BCg2;AG<; znpy557qBa;U`1R$#X-&AhG~A43L_6IYk2K$q z*=@f2ApMHSz~Lp)lZ?nGPU-9x%?a-AV^P*k%6hc65WDO^85Fc_R6b+fbZ= zbE%J>EqBgH?vUtXZ>(F1#!grrkEmj|zJ%zt_OLt^&ush2F4X64|IO^#GTYR#5#y)R zZ|qgJDqgfGU%6FR=^JHP*vFf(W%E!7b)Zqs$C)Ja<^F)}96P9&l@ucSqUH+V3)uEqof(jUf>TLRNm^oYO8%apcL-6P{d5 zc@v6ZdHD64{&%W&JzGl;@^D{9kEODKNBRvmvr%8P7k#y|xQ1M|jeJ;u~IAQ@ju+%YZV z)A6Tp_%GFEhZ24+C$1N6$wc;0jK8@G@cs2g0Q-XvtRv&q>Va=VQdaA>h|ZHQ0~N7S|xw{hCFqN z3{ahPO=o8r{^RjgF-4lHui(?s;4qz4zh=`=|K)aOHuNnkp0()kf}$ zB8S!+%JA{ItLs}uCnSi*!SgLW1z~rb1X(A<{j8r29YZHB{htYqr9ab{d{P(3z&Y^e zPqEJCTK&!{wC!fb@AjhwXBdF!pLMrK<;C{9Jd>d@#xbBmN3_r1YfsEW_8>glzvx%{8cl|!Q-ff4D5zsHu);eLSN8 zvWHz3P^-Yu_EGIbdB|RDn`Ql?fp12?oiMuVQ1$8f-)i`h?b(?XXi?X}Fhj;_*I4vl zlzh$gEyi?`Py9?15P$P9wY5fBM~?76dFdaf$9$ZFlx!2MM;3Fp?chFp_aXFBgSs^;BJ011OPUGc8;z z4w{T>(qLKo=DVDhiO+qd5lmWij>zVQtrg^Mc^y^kvn+3 zxeLHDoA>k1bqziWKEW2XOX43Peg(A6xav;Ny?DG|SEDi$)fq`}eJ&T#I6b&<34cag zjZm`}=LlGjt{3M{GIR=0z((5f2Vy*FA+?TfB;o*Rm6|Q-8SNyR-fmF-+KbmyugQu2 zFAsAq&*QV^-S;j8e@nb*SHnOxr;No$bz(6jk?YC+gQ+2*uHHmPYEu(>1N+pweSz<` z90V!c1npm&vtaOUVwFX?2b)YsXeWKd%J@dY=rm%)=+fYSG>|svJPOv!?Aam%_;8FA z&d6~kB`ps{PNLxSK9_cP7@P^H?Wm}|;&QLBpUs(HK%{a`w)yUtez698usu@SZ}dBg ze<>>*g|a+1Nb9*^jUk<5N#0LI?V-4$g8HZfa1Ud8cN#A|(}VBz##l_wxck_gEeD_m zRK@ujqSZxTX_5G(M5g_T7}xDT5RLNR4L4@4{dx9}-dpcYP0Y8Qvz%|vcC{Z1?b#Ze zt0a-MV3nrH@LlSW~J= z=~`-?uwX(|!`kQs{)+o>Zb6t0wxd+IvVA2v)?C0We1I+nDzp=N=r!CZGuJBvyrT?# z_NLuglmqeUbma+Vs;IrO>x_j}l%DFjv7D~Qb&ObJZKnyV_AsmDMRUCOHU00?Z=q#H zpJjSQZ6GPwRT+(r+EKmj;xy1Kqq>X)5)ru`jPbzr5DCHFkb;HUypO_dBD=MfqVgDu z;G|k-Bdmjpz-X<#Fp8^)nWT>mBijd`IleDMY&V>4c&5EQJvLoyPF;IxM8suWlTa)2 z9^)ykSDicoiqm}flIGbbLv5ee;D~H^N#!bjbElFxLDJ;J`}7E~qfQS-(G}Nt0EYIL zzdY7kdp&Kw1;=Q1kbxYUDuVJbyH^H-WHFJSCdOj)C)^6{P5t_}Fyv~Bp7UB#HG0)r z-lkQq5u=Rz>%lJvbow8nI`j95 zN)glK9-iMm)fJ9`^-!5WYs;PMp{x`~8a-HLRJ4aD)V|4Penz-4@8eb7!9?~WJl^p& zK825y3XO@qq{?Z?^ZtU|%)brs&E$b()Z$O>HkWrkKFxKkrX7A0f`R$9QWQM#1yEkPpLZANz6cB&doxdK830 zjGDHU?o5QH3$NlW-_O&T#ES7FGeBwlcck@m6sI4;!K994!osFW4or({kUty4|GBa@ zEE2qaLlyP+t$i6qY5{8-5^W0asNI=@m{~HRVFi_C5MZEuIkDuk(Di*Jq@P_Rd^sy{ zSdC+9;ucN}==zO73|3mH2i4ZXFDT+w16!3k@Tg@1Gab=_Nm6fru$t5Vy?Oo9HYb(v zcT)|ceYQ(Qb=cd?tl~@X{3>e~;R*#$#2-|m0(i#OQyd?{GYVq8eYKa8cuFxiHPOg7 z3s_zEosJ8YWG_YM6ie7&%HO<{-W)`u&3V-c49|X=v8Xf*rXa^sNZ^9_pNZ_Jm8+19 zmuj@twM;!-#2@!!o@F3hF|X~^gS7J0xw@7^H(Vo2x$?kPNHpo(k;heEQUF<@aV!zF z!;P&~&SKOYU7d5A#&^0#qWqLQGh3cQIv=dKr^=nOA_Sbjhr5Upk6xY{mu@Wbbso#| z{gR60`g&!=elMd~*EI(V^5i?xdV_pSK)61Pne_h8*03|(_Y;*_gh=!PzImCVf+G)S zlZQ4%V?8I#Q9Jo{_a|oo?(*$EB|uKiuteOgm;zPTeYcJdj08&Tawe}UpK2a zybBTL-#%ss6MhiWP5Mi}!~CtM^K#~duIQk$W)zQTS&Onu4`OF7_=I#W4e`5j6{?6h zq;k)#{e1A7BYb>_;Zr?(L1P*?_^VCM=Vvp`QlO1QoenAGUqkND?|~j(TIvGh=n~28 zc=_JRb|-4Id6k9;ul_i=AyK+WV{id4d^jUxU&))cKDX+4`>Lk3wEomN1h3%M9~{hC z{008AoXGJe#ly_6A=fixzXAED97f+MsaqS>&?2vvPKl0nLtJD%rh_J%~qh0z_*$t>_hSWa-%d?*r2J*VrxXq#DX<15=A0F;TC(`-yEP-dip zApAT*v4MkYTk08?Yk1_B#YC)LkiB;~``B#Js5+5D@;$+lZyD(y^8%hEL4~6lN#>XY zU6voG-1KbO;OEM>;aJ`>gYK6L;r%Q@VEm6(Z3J|k=$mn3&oyvWLq=0Z3 z5nwUx&AdPb#A8*pZ@8pHKD^1Q`6?>%NmhX8D=XU@RPxqicL-a3z4DkeR@JW`hi$9H z1Cu2D9OtMqWXy@mPBZTj5_J`f+oztib}B;!6J()Qi}le)hU_k0Z;fBBio_-URb$eV z)W__(hPnHlMJgI~k@d==0tsX5@>slcC%nUJ;gCQP-tFsWLPrU>U!&JfDf*IQ$3!R} zf+-NQ7K_v6P+!iTxme~iq)f)UUJYkt5{j_Iai@4k?eO`?hlB^LFZ6t~tpn|RJ$g-j z)+1Zu(k%YZ0MoYz9z_+?;(tHTh001cNb7L2ggO-FFjl)S)fR|0tDqkj!S%mCO|Y69 zLd}Rn#&bJ`Dyb5pUvl4`+{J53OsP|}s-qwOfG>Q*P#voaX8MQCf3dc5NxbRuE7<8c z)&Hrp;f%xnN#Fp zWztSODooSMDqqZsEh_qD*}zTsiroE3wVM+j1ju=A<8qSAE zB)~78L`}7$@J+uJO?nM0my9=Fdq0Hb%^xOE(%Pw0xSQB&HXEd>O8})Yxi#7&5a~(x zo1HD=tF>H-%bDHum2$VO{sMzaM`Ww{?+L&iY9pW=GC-TAu70-u?IF?O@Wf>_T1EOu z0L{iSz^LhO#Gaa=>x;K?K7Px3pE(I&PtnY%(+({&O@M76Y8j6@2X*I`Q!MYdKH&vR zH{YG!3cWyhU_1JdOZ_fBR-k@TUr{xjq7t>tEOhMDWGX0Hm?l@BkHH74s#>hL=(ySk zK9mOf1Y{YEwJzIp$7BR<_KkAC()t{u054CoQ!)3ELOlDc+uK_k%Ea)SIEiKBV~gZQ z!=-3&TwI0}8uIb`o6vXPsLsiWh621bMuQjEk^4i%EYaWnar;(~eE*->ob;-`{S=_L z@$HHfz>ApeTp!@%o5s5Ayo3j!cp)-7^yqaeL!U9Z-eMOvAUG-z?7U%u5;1^CzOXi4 zHCu2xv&d^G9!9E%O|nirYrLso;-4=;5@G_b>^7Vpw4S~TJOk=KRv*I|R{(NcCPRS? z@#9z$j%tekc*9%y#~bZ`^Y)k6BGc(JOis56gC6Aj^O4xT)gzfY?}0y?Jy_ky%D4hY z*metV3$AqM93#kMH}jc>TDTj&rowB$)FUu~1cA-dMO5A83xzEM?m2Y$u%`5*6Ye?q zgPy<&MvTH2WLWJCOHjb|5m9Ms92kCpT5UOu78Y7+<5rUuuXV2BthrP_FBpSuru)DE zI;P001Mb}+ofaPaHc4U3`6we6on-$ko-MbVXYH1gk92Ay^*L6+RY7p#+`^o_zd)3l zXg?v+{+36-gQ>7OofE9C zvQ8s*pJC=Tsn~Gsu?ykKcFVREo%hoA(YlE%2LY^(ZymOm5d;5RB>WdU z_-JxNr;y?FC9_clddp`9IPNJ@48UHC*{&nIHX(3TlIh4djJ^tX{&nU2se9xPKWgzi zU>%xGO7YV`W*#hrN&SuIM7L44{hL$+y!$NDIB~Lju})Ie-Xf9CDa!udJNL-uY}M$x zaaf-i!&V<_Q^(APE_y$Xw|w%pZ{HSlwq`c#CIm33h2EzIo#%-iFJ?DXkA(jpi78XG zoM~Sp9GXk2aq!@JP;`=NeT^vf_zI>yR}!+a;`+h^!t!IQr}xfw&xx5I(>Y?_McqG# zHaN=(m)AgK_+1++mJhY(er?JO+fGD=Lq)ft{jAra-`Not4@QZ}0lE;CQouu+(ot>o zJuRlOtv&6}hn)y~?{?f8xv2iRRiR>zE*^UVd~TS@gNO{YCKN+|bt> z-}CdXk)%GqW_k*$KnN^AOniN2$?~Utv&Cdi?jcbKziE1Ntvjrblc6?gdRtjAi11pyEQvPHxl!v83-U94BG!=GubemEY^Sb#=Gi*^VcudQLo~#}sz4 zbTXUssM`;$WR#s<-|P%@m%2#9%A09dOExERGISsiqL{fJR@KPrc(S$qt=;crU^{b_Zuj9LnT&~a`+LuAa?si@;zC2^lW_uh`Lc}wws+A*Wtmg*mHGbE3&UFV>iwd9ZO&TL76Ju z-~Y*=jSfL9H0=rO+e+=n(yE-m@E&(Vsw5L2%3QTuO}f!VCNRY&5uvpy`D*c#Gd>rQ*{1T zDQZ7PF2PNv{v`ZQoFtE9AG5J-Nq3@#^S1Kv-;R>n=upwE_!>#bD>3FgjMPR-xqk|SO{bJ zu2_cq+6)i=nMGPLd^daH-BVQk0bI_ed1sU43-$n`q%5ydr71c{>1`-yR-__UMq%=N}uLmTa!PhVHY` zPP&49WeN%>TsNib1N6z}PSdDd-s9%^rq2+m+OOl!&dlhVdaacI{)wf8N{yvx-8VFw zN}pYBNe*GYgO+9-XfqB6NQmZIeNyWQEMbLu@UR^8Tj2x?44ND7P`H3<`quc&xDfAe z2)wisr}}vB1<88F6I9PGSs_(@Uc%U)|P z80;s8Y{jybI4VCV!ijhqnag{BD7nV^aDl^11jmaN5AW;&xJ;;PO%{4FIo2hW(2q0V zP0NIJLZ6>L6we@$Y(F94XCkzr;Jw8@If0pxq-VXo<1p<$@6+Yxi~^CMhpTR{zz-Z{ z8s9gI5@A&ObyjM;pqOAys3uSBaDhH@(0#}bJCum zu3@oo5iXEsm!C7LZt#gUq?4N$VtIJ=L`Ec@3>2vnIvmygX~Wz z{NUt8B>!~<63GGK`1ab%bw2JtBBLJb_)F^ejLMVhhX!{;;;NUv{Oe&o!B|z2Pw}wt zt4?fk|AaxLpXX0s^wMF?3?F$)eQ@M4n5e3Xe(VnyTf+`7aCCgc`5%l!D)@OA(U*Lh z=c@2lUHq^aXvn5E1XV?iJS9x0Ol;f!{?A9il!K1+@QEupxw3sgjOn+2fXppEQW(qq zqvku8Fm9pnyhCvsdDN%+pb3dN8A_R|I?rl~+HhLip_zs4%~;3755A~$KlAoh&$otC z)`uK$Z!d2{FPi&t`9WKiKk@$X%mQ@zqL33VlTyS{Was<{!eI*N)Xiw8iarhBRwSc( zQmugeUe2Gk6~qeC>}Lws_1c3|K`mT|&Z>l8nZE$pZP_2eu7V?2%}r@RtmLW&R%E9>kR||%uL_aih>y%KOs(PL*n6{~1Zw~#X zXMku)AL-(sFU@!jDp#(CwNTudsS&TREU1dIJlHT%wQ8U8%M)4a>ahrRSZSu$MeW9r z9g)OVr@t@7&HITlzqfq(_@oc+5gc<@_EpBItaV8qJ5|M8w>iVW*|ghWE?y3`c6Fcd zfDzhWey{4V2&y?`wFLRm_*t-=!9e3VrrBKFn%KI@MT@G%2UOy!P7Lk0-on~Dadn#FE1DXezN9ytdFVyZ|TPo zTLsG%{Poma=rDx@UdT|dq*Q2iKkjp7X_4|G`yD@)>Hg)0;(^{R(HnkA!J!j-F??B% z)`RuPWrk>YuW@jh@Ck+v;iRg(jJxu|MfgLT0y?S;dy(>;%iYoz-%xgFG|$}*vyjR` z*okWb?d~uFo#U}iJ_24mfpw#o`cT`KH2q+7##I)oHD4A6dM-*{cN_*k8Cs#06NKz! zge;h%`Xat@{PxVLgFfhE{{pXmJUU4vt!W2ka*J(P;m5^+dg;&owH~jRb9Jg++QMF< zq-JpVViDBA_e?aU?nAuU9N#%!@EYXr4#ZEz9P6;n-WZ<|hc}_~$11811_J36bvAsx zaBMm|WOM)!Hc&K_(^qYZUv}QG_p9G@ITUPK zazbAv+nglt`zWMWwd zvc0VE=|pE!LJEK$m9Z{zfB&$g(mV$=8Pq&%r7OF!E1340ueg{MpC-JYFia-T@At;z zrBvWfxY79ZATxxg=iBfJExdR^FcZZ$UEb%o2)Y?l37=h@#WAV z#+gEe#HBL)pl;+33}Ew!=(alld-8n#vdwR!2Vpdv-%NZ#Z`kyUZ;%Py7`uvCm_r(0 zrJ|hs4`_L!cE+3};9bSWMBInvf&B2f$>*x24YivXrDyZWi=Br}@~k?oe$Ib8PRs;6WjRDyvtrNH zY;aI};qeo82u-`J+g@2!I=U|WxFnMaIra$uU?fTOBD!Stn)5^a=ofa;=C&kMj|ps@ievWcPO7ww{VRLJiHQ$E! z4yRA|YD7C1)p_TFNPiRMg%^o^*}%2ll1MIdZ)bYg6AwIlMgt4_#tR)Ocmz8mww$33 zf*fDqvP4y4Usgq!l@%brJqv~89;}_v(wJFvT9oT2X`^B&LiK7b>!xQkzHkLBbQn*t zS{?PTk;a|lC+EZdqv`pcjGh^3^ft6CQhv(?qo%c0?Iz+504sWB$T{vszh~8bsU3#u z>|@PuXw$FE?OG%IwoWLQWuZ%wD}vN~mXXD1axtbcIxC4`f>ZXSD)r2r%eE@U1vw9!ySnHL z%975T;a4Jk5e@As>qb19=s!OFa)>Gsdh3a*cX&u|sTXh(KbmfNWK&Pn?zwYPk777Q z!azuMW2^zTv)NO0ldy_ok~*kUmT(5N!5a_AqV6-jGa&D_UXZnAJ3MPa`9^pM5Pw zPH=0{b+dzU_T438a`Jt&*#ytXc67Bq1%i-6FzB7GD7QQemy-@g9gO4o%!S}yWhkl) zmBS{9V#}|T%4{&`)^nD6IjC7vZ<77qb$B#vG{a!ZQk0icJ{jh{eu9$_=nw_MuF41X ziBwkpKq9f7fbVpyEh8`Y_2p8M0=!;1(n^uOs{bxmTR3BXH@_0gzJ3}ReU8Tf6c<6C zAX(;9jK-ZEs(`;0mP_uT^ZKnPfn7Z#9s>VO5*uefEy=M*Nln96pRpgf8Q{GXCjtFyaT?GjLdtpesSx~5TnPRI6o^8wKc=#Wh>lQw$L(i zkf!Y_Y}^V3WY_Nh)xiMWtH(u#6BXFb8dAMt`DVIY#j;N zZh48%6?oDxQb@i9?D>VNV@3Bdv!8V!${l^OaqBep`|*kd(*$y+rLL`doAsL;N!$b=AI z{Y$P0k%DK&-Kux*R*UTWgh&R#RD-XZyYg}hmbEFIK0XBzw;iUFkHOowDiNVMwP_{J zemF5wdzC*;56DNf7?==&eB5lsu>><>c;A~hPO=X&tu-%}9N=ZY z(UE^4P<_#iD3a$K|BS>Ltoun}0(|o&;wSCkcRu%zMBalUdCGo9K>jF&UM5K+Snh|h z#Y8x6#Ng=*>Z$4>!t7)XSm`|nwb_Sdj!Z@Y&DA&_s=zBx1xC~4U#N>6#iBsbMx}Wz zSsr`vZLPjGF++DlbSoHKx&eZ`FvL)F!a#5bG61CFr80BcoR=0Mf^FJojFCo0bN7+0 zhx3tVGd*bHK!!j;c!)0T=WF}3N-J!y4-|2PQQ(0!m)=;_A-69xTDAnr{Za~s_BPs+ z3;?}?Ve3rc>iR9e<(ogT`e+_RC*VD_P=sF(;T+hVPeuF5_Ph0{Krro3fr`Ba5uDmNwb7)z6-9$uy% zayT<$Y21{em>tItaCB&OqsP^OzAgUw0<`RUXANU`Xy=xNsodc{9FZuG;8+9I)e2Kt zE4V>5=f%R++;G=Q9y-0h_o-f@+5wp$1;?a)2LIklKpdmm{ZRehKSUCmQ5T-rI5x$9 z?Xv$nqS~sKfk!FrlhLzMGjs43M$C{5V~d}C3+u~2hLiESoc&KR|JA`WK+^2K1yS%v zfyL^%090?Z##($nsc;Z)Rlpw$dI&_#O-vS(OpWQQs{B}zD*eJ9YOh@*=w($(*mwv4 zqdLt|)Lp)X-ewPa5eT5VC;pFkZ{@=ON4ytxPleG)ux4>3cv9c~iut*v$V~?C1Wmf+ zQn$Z%Ms9faI=F+`d#lepO65zCO#aM>j$Bua=2E%N%fia0YwC7Fv1P&8@t}&rTt1Qn zb`O;zn$MF$ud4GuEnxv(my6xgw)e^ow?#>A>Gct8DN531ezd%Ulx>7NMfkMP>BlABJUDW$>ShHW6YdK!S5<(|zLQo~`mHD4` z*Wzn9%ah{t+%-l(>yh=POtM8LnFMG2z1_}jgjsL_V0hdnD&}6MWp53K3mpPV{gr`G z#bsr8>$x?J|96Z03)vk!;Or6cAwLOsWVC)p{Rw$#WB^##QqOx{vy+Znw_QEh=;HbR z(21L)b@0FOTe_n7r>fIE_})RSr7T9Za!|G^iKoF)j#W|V@G+ao3dMu@T{}@m?@sQ~ zH1liiZZW!Dn^*%A;Gni4d(vospKLEH0w&aXCGx*vSYYE`&@!tb)jW4Y98(cLkh5H@ z1ZkQ#@>ro&i7KZ=w6MP_vFjU)iTht%jzA$DQTKY2O_~JNKb!X6U85pNypYZ$0Thc6 zREjJPD2_?y+~wf>-{{U+^OEI0uCX>H!r@7()saXw8YhGDpXCNpIA(>*)PSiMqHz&+ zTyZAax`gv%iT?*G6K!YkCRtJZAG1TISgk!ngB`&ah}R?^KX=K13O?%eBUcx-ffw&JOKtw@9EbZ+-) z<(GR4^uj-lLMIP}&dco;o`8D`68fBkvD)+Ek$k-mYH8WLe>tQL{oexr0jb*9FgSXP z74T!^%=l8}nrSmCx513ju5vNvuj>U8*w`$?H?YcJfW>~BPUHmtnl zKlFy7nwJ`1Ak^50Gk_WLvQRS-yuzjD0M(rPiWowppK%uEFl$t0KXBi2O+mk9#81Qb zH$2pc2kqQwv$wv5N7^V~4%O=5foi~C?qO{rUHp;-vd6HgiPTpZj)RA8a|f~@{BdbF{#bqK z13QO%>jW&n6t4sYW`zcEd!T-$K@W^cyic!X=JW9543+Mj-C8-=s4l}pRy1KjKtL7- zmV}E9wYU(hd=a%j;>tjea$O&){&(XmSY%*wh|l@0U?LKYoycg`5>ybe-O7!8t7nT7 z5X|#`?G#60afsiyt$nq2H?FHdh9ajV8QjscTpCrGmt?g!*)8XMmvr`Dh26)p><`FI z*%03yDH8@><-gh!xXOR^oA;oDABt}tTg$Vo#V+Jn&;Gn6ZU01q{ZhZI4TZVL(@)pi zFy|51uOOfJbG_5LfaQqa+YQBWvwlllS#<5@5uRT{3+H%E+g>EL5;#U|#df)^wzB1W z(vy_%*EgOw*ZMMXTQqVxl_ZMrFO3NI_LF-EJVN5gpXw#0c z)4!DH^Zs1rt)lL~fyG(++VPsp`tROlO$926KdJdUXwKd_e?lgE_{jnH+FyJKtgo6B zm%lj~=l5?dP|LzUqLU_109I{U^}r4BC{^1O@WB#zDm!4!s5xan_*@e$|EKGL-6Nn0 zD0L>V9)ik%j1ixLB8gO?J~_hKpR~>F__m9aCV%LhQ@85VE8vL_4o_0HSndLD&yz2@ z^lbjFBQKgnoj4xujRu~_0y^*ncr**>Toi$QlY^Vha&K+Py!>qK+yBfyMf{81PRzOi QJV}PZ)78&qol`;+00YV7v;Y7A literal 15614 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 a;CHA-3I3{PJOeH?V=2q4%T>r&1pgn={A*(X diff --git a/crates/libtiny_logger/Cargo.toml b/crates/libtiny_logger/Cargo.toml index 73fd20e6..de0942f0 100644 --- a/crates/libtiny_logger/Cargo.toml +++ b/crates/libtiny_logger/Cargo.toml @@ -7,5 +7,6 @@ edition = "2018" [dependencies] libtiny_common = { path = "../libtiny_common" } +libtiny_wire = { path = "../libtiny_wire" } log = "0.4" time = "0.1" diff --git a/crates/libtiny_logger/src/lib.rs b/crates/libtiny_logger/src/lib.rs index 3ce2f710..71025402 100644 --- a/crates/libtiny_logger/src/lib.rs +++ b/crates/libtiny_logger/src/lib.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use time::Tm; use libtiny_common::{ChanName, ChanNameRef, MsgTarget}; +use libtiny_wire::formatting::remove_irc_control_chars; #[macro_use] extern crate log; @@ -306,6 +307,7 @@ impl LoggerInner { _highlight: bool, is_action: bool, ) { + let msg = remove_irc_control_chars(msg); self.apply_to_target(target, |fd: &mut File, report_err: &dyn Fn(String)| { let io_ret = if is_action { writeln!(fd, "[{}] {} {}", strf(&ts), sender, msg) diff --git a/crates/libtiny_tui/Cargo.toml b/crates/libtiny_tui/Cargo.toml index 7e778daf..0b09e0f5 100644 --- a/crates/libtiny_tui/Cargo.toml +++ b/crates/libtiny_tui/Cargo.toml @@ -14,6 +14,7 @@ desktop-notifications = ["notify-rust"] [dependencies] libtiny_common = { path = "../libtiny_common" } +libtiny_wire = { path = "../libtiny_wire" } log = "0.4" notify-rust = { version = "3", optional = true } serde = { version = "1.0", features = ["derive"] } diff --git a/crates/libtiny_tui/src/config.rs b/crates/libtiny_tui/src/config.rs index ed523b5e..730a707c 100644 --- a/crates/libtiny_tui/src/config.rs +++ b/crates/libtiny_tui/src/config.rs @@ -34,10 +34,10 @@ fn default_max_nick_length() -> usize { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Style { - /// Termbox fg. + /// Termbox fg pub fg: u16, - /// Termbox bg. + /// Termbox bg pub bg: u16, } diff --git a/crates/libtiny_tui/src/msg_area/line.rs b/crates/libtiny_tui/src/msg_area/line.rs index 9568eea5..af5a9a8c 100644 --- a/crates/libtiny_tui/src/msg_area/line.rs +++ b/crates/libtiny_tui/src/msg_area/line.rs @@ -1,10 +1,7 @@ -use crate::line_split::LineType; -use crate::{ - config::{Colors, Style}, - line_split::LineDataCache, - utils::translate_irc_control_chars, -}; -use std::mem; +use crate::config::{Colors, Style}; +use crate::line_split::{LineDataCache, LineType}; + +use libtiny_wire::formatting::{parse_irc_formatting, Color, IrcFormatEvent}; use termbox_simple::{self, Termbox}; /// A single line added to the widget. May be rendered as multiple lines on the @@ -13,6 +10,7 @@ use termbox_simple::{self, Termbox}; pub(crate) struct Line { /// Line segments. segments: Vec, + /// The segment we're currently extending. current_seg: StyledString, @@ -35,7 +33,7 @@ pub(crate) enum SegStyle { /// of the color list, so make sure to use mod. NickColor(usize), - /// A style from the current color scheme. + // Rest of the styles are from the color scheme UserMsg, ErrMsg, Topic, @@ -78,9 +76,6 @@ impl Default for StyledString { } } -// TODO get rid of this -const TERMBOX_COLOR_PREFIX: char = '\x00'; - impl Line { pub(crate) fn new() -> Line { Line { @@ -103,7 +98,7 @@ impl Line { if self.current_seg.string.is_empty() { self.current_seg.style = style; } else if self.current_seg.style != style { - let seg = mem::replace( + let seg = std::mem::replace( &mut self.current_seg, StyledString { string: String::new(), @@ -115,31 +110,36 @@ impl Line { } fn add_text_inner(&mut self, str: &str) { - fn push_color(ret: &mut String, irc_fg: u8, irc_bg: Option) { - ret.push(TERMBOX_COLOR_PREFIX); - ret.push(0 as char); // style - ret.push(irc_color_to_termbox(irc_fg) as char); - ret.push( - irc_bg - .map(irc_color_to_termbox) - .unwrap_or(termbox_simple::TB_DEFAULT as u8) as char, - ); - } - let str = translate_irc_control_chars(str, push_color); - self.current_seg.string.reserve(str.len()); - - let mut iter = str.chars(); - while let Some(char) = iter.next() { - if char == TERMBOX_COLOR_PREFIX { - let st = iter.next().unwrap() as u8; - let fg = iter.next().unwrap() as u8; - let bg = iter.next().unwrap() as u8; - let fg = (u16::from(st) << 8) | u16::from(fg); - let bg = u16::from(bg); - let style = Style { fg, bg }; - self.set_message_style(SegStyle::Fixed(style)); - } else if char > '\x08' { - self.current_seg.string.push(char); + for format_event in parse_irc_formatting(str) { + match format_event { + IrcFormatEvent::Bold + | IrcFormatEvent::Italic + | IrcFormatEvent::Underline + | IrcFormatEvent::Strikethrough + | IrcFormatEvent::Monospace => { + // TODO + } + IrcFormatEvent::Text(text) => { + self.current_seg.string.push_str(text); + } + IrcFormatEvent::Color { fg, bg } => { + let style = SegStyle::Fixed(Style { + fg: u16::from(irc_color_to_termbox(fg)), + bg: bg + .map(|bg| u16::from(irc_color_to_termbox(bg))) + .unwrap_or(termbox_simple::TB_DEFAULT), + }); + + self.set_message_style(style); + } + IrcFormatEvent::ReverseColor => { + if let SegStyle::Fixed(Style { fg, bg }) = self.current_seg.style { + self.set_message_style(SegStyle::Fixed(Style { fg: bg, bg: fg })); + } + } + IrcFormatEvent::Reset => { + self.set_message_style(SegStyle::UserMsg); + } } } } @@ -150,7 +150,6 @@ impl Line { } pub(crate) fn add_char(&mut self, char: char, style: SegStyle) { - assert_ne!(char, TERMBOX_COLOR_PREFIX); self.set_message_style(style); self.current_seg.string.push(char); } @@ -230,28 +229,28 @@ impl Line { //////////////////////////////////////////////////////////////////////////////// -// IRC colors: http://en.wikichip.org/wiki/irc/colors // Termbox colors: http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html // (alternatively just run `cargo run --example colors`) -fn irc_color_to_termbox(irc_color: u8) -> u8 { +fn irc_color_to_termbox(irc_color: Color) -> u8 { match irc_color { - 0 => 15, // white - 1 => 0, // black - 2 => 17, // navy - 3 => 2, // green - 4 => 9, // red - 5 => 88, // maroon - 6 => 5, // purple - 7 => 130, // olive - 8 => 11, // yellow - 9 => 10, // light green - 10 => 6, // teal - 11 => 14, // cyan - 12 => 12, // awful blue - 13 => 13, // magenta - 14 => 8, // gray - 15 => 7, // light gray - _ => termbox_simple::TB_DEFAULT as u8, + Color::White => 255, + Color::Black => 16, + Color::Blue => 21, + Color::Green => 46, + Color::Red => 196, + Color::Brown => 88, + Color::Magenta => 93, + Color::Orange => 210, + Color::Yellow => 228, + Color::LightGreen => 154, + Color::Cyan => 75, + Color::LightCyan => 39, + Color::LightBlue => 38, + Color::Pink => 129, + Color::Grey => 243, + Color::LightGrey => 249, + Color::Default => termbox_simple::TB_DEFAULT as u8, + Color::Ansi(ansi_color) => ansi_color, } } diff --git a/crates/libtiny_tui/src/notifier.rs b/crates/libtiny_tui/src/notifier.rs index 0f2d49c1..f507186f 100644 --- a/crates/libtiny_tui/src/notifier.rs +++ b/crates/libtiny_tui/src/notifier.rs @@ -1,4 +1,6 @@ -use crate::{utils::remove_irc_control_chars, MsgTarget}; +use crate::MsgTarget; + +use libtiny_wire::formatting::remove_irc_control_chars; #[cfg(feature = "desktop-notifications")] use notify_rust::Notification; diff --git a/crates/libtiny_tui/src/utils.rs b/crates/libtiny_tui/src/utils.rs index ceed3f8c..0b81a5eb 100644 --- a/crates/libtiny_tui/src/utils.rs +++ b/crates/libtiny_tui/src/utils.rs @@ -52,106 +52,3 @@ pub(crate) fn is_nick_char(c: char) -> bool { || c == '-' // not valid according to RFC 2812 but servers accept it and I've seen nicks with // this char in the wild } - -//////////////////////////////////////////////////////////////////////////////// - -use std::{iter::Peekable, str::Chars}; - -/// Parse at least one, at most two digits. Does not consume the iterator when -/// result is `None`. -fn parse_color_code(chars: &mut Peekable) -> Option { - fn to_dec(ch: char) -> Option { - ch.to_digit(10).map(|c| c as u8) - } - - let c1_char = *chars.peek()?; - let c1_digit = match to_dec(c1_char) { - None => { - return None; - } - Some(c1_digit) => { - chars.next(); - c1_digit - } - }; - - match chars.peek().cloned() { - None => Some(c1_digit), - Some(c2) => match to_dec(c2) { - None => Some(c1_digit), - Some(c2_digit) => { - chars.next(); - Some(c1_digit * 10 + c2_digit) - } - }, - } -} - -//////////////////////////////////////////////////////////////////////////////// - -/// Translate IRC color codes using the callback, replace tabs with 8 spaces, and remove other -/// ASCII control characters from the input. -pub(crate) fn translate_irc_control_chars( - str: &str, - push_color: fn(ret: &mut String, fg: u8, bg: Option), -) -> String { - let mut ret = String::with_capacity(str.len()); - let mut iter = str.chars().peekable(); - - while let Some(char) = iter.next() { - if char == '\x03' { - match parse_color_code(&mut iter) { - None => { - // just skip the control char - } - Some(fg) => { - if let Some(char) = iter.peek().cloned() { - if char == ',' { - iter.next(); // consume ',' - match parse_color_code(&mut iter) { - None => { - // comma was not part of the color code, - // add it to the new string - push_color(&mut ret, fg, None); - ret.push(char); - } - Some(bg) => { - push_color(&mut ret, fg, Some(bg)); - } - } - } else { - push_color(&mut ret, fg, None); - } - } else { - push_color(&mut ret, fg, None); - } - } - } - } else if char == '\t' { - ret.push_str(" "); - } else if !char.is_ascii_control() { - ret.push(char); - } - } - - ret -} - -/// Like `translate_irc_control_chars`, but skips color codes. -pub(crate) fn remove_irc_control_chars(str: &str) -> String { - fn push_color(_ret: &mut String, _fg: u8, _bg: Option) {} - translate_irc_control_chars(str, push_color) -} - -#[test] -fn test_translate_irc_control_chars() { - assert_eq!( - remove_irc_control_chars(" Le Voyageur imprudent "), - " Le Voyageur imprudent " - ); - assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo"); - assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo"); - assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo"); -} diff --git a/crates/libtiny_wire/src/formatting.rs b/crates/libtiny_wire/src/formatting.rs new file mode 100644 index 00000000..74111cf9 --- /dev/null +++ b/crates/libtiny_wire/src/formatting.rs @@ -0,0 +1,535 @@ +//! Implements parsing IRC formatting characters. Reference: +//! https://modern.ircdocs.horse/formatting.html + +const CHAR_BOLD: char = '\x02'; +const CHAR_ITALIC: char = '\x1D'; +const CHAR_UNDERLINE: char = '\x1F'; +const CHAR_STRIKETHROUGH: char = '\x1E'; +const CHAR_MONOSPACE: char = '\x11'; +const CHAR_COLOR: char = '\x03'; +const CHAR_HEX_COLOR: char = '\x04'; +const CHAR_REVERSE_COLOR: char = '\x16'; +const CHAR_RESET: char = '\x0F'; + +#[derive(Debug, PartialEq, Eq)] +pub enum IrcFormatEvent<'a> { + Text(&'a str), + + Bold, + Italic, + Underline, + Strikethrough, + Monospace, + + Color { + fg: Color, + bg: Option, + }, + + /// Reverse current background and foreground + ReverseColor, + + /// Reset formatting to the default + Reset, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Color { + White, + Black, + Blue, + Green, + Red, + Brown, + Magenta, + Orange, + Yellow, + LightGreen, + Cyan, + LightCyan, + LightBlue, + Pink, + Grey, + LightGrey, + Default, + Ansi(u8), +} + +impl Color { + fn from_code(code: u8) -> Self { + match code { + 0 => Color::White, + 1 => Color::Black, + 2 => Color::Blue, + 3 => Color::Green, + 4 => Color::Red, + 5 => Color::Brown, + 6 => Color::Magenta, + 7 => Color::Orange, + 8 => Color::Yellow, + 9 => Color::LightGreen, + 10 => Color::Cyan, + 11 => Color::LightCyan, + 12 => Color::LightBlue, + 13 => Color::Pink, + 14 => Color::Grey, + 15 => Color::LightGrey, + 16 => Color::Ansi(52), + 17 => Color::Ansi(94), + 18 => Color::Ansi(100), + 19 => Color::Ansi(58), + 20 => Color::Ansi(22), + 21 => Color::Ansi(29), + 22 => Color::Ansi(23), + 23 => Color::Ansi(24), + 24 => Color::Ansi(17), + 25 => Color::Ansi(54), + 26 => Color::Ansi(53), + 27 => Color::Ansi(89), + 28 => Color::Ansi(88), + 29 => Color::Ansi(130), + 30 => Color::Ansi(142), + 31 => Color::Ansi(64), + 32 => Color::Ansi(28), + 33 => Color::Ansi(35), + 34 => Color::Ansi(30), + 35 => Color::Ansi(25), + 36 => Color::Ansi(18), + 37 => Color::Ansi(91), + 38 => Color::Ansi(90), + 39 => Color::Ansi(125), + 40 => Color::Ansi(124), + 41 => Color::Ansi(166), + 42 => Color::Ansi(184), + 43 => Color::Ansi(106), + 44 => Color::Ansi(34), + 45 => Color::Ansi(49), + 46 => Color::Ansi(37), + 47 => Color::Ansi(33), + 48 => Color::Ansi(19), + 49 => Color::Ansi(129), + 50 => Color::Ansi(127), + 51 => Color::Ansi(161), + 52 => Color::Ansi(196), + 53 => Color::Ansi(208), + 54 => Color::Ansi(226), + 55 => Color::Ansi(154), + 56 => Color::Ansi(46), + 57 => Color::Ansi(86), + 58 => Color::Ansi(51), + 59 => Color::Ansi(75), + 60 => Color::Ansi(21), + 61 => Color::Ansi(171), + 62 => Color::Ansi(201), + 63 => Color::Ansi(198), + 64 => Color::Ansi(203), + 65 => Color::Ansi(215), + 66 => Color::Ansi(227), + 67 => Color::Ansi(191), + 68 => Color::Ansi(83), + 69 => Color::Ansi(122), + 70 => Color::Ansi(87), + 71 => Color::Ansi(111), + 72 => Color::Ansi(63), + 73 => Color::Ansi(177), + 74 => Color::Ansi(207), + 75 => Color::Ansi(205), + 76 => Color::Ansi(217), + 77 => Color::Ansi(223), + 78 => Color::Ansi(229), + 79 => Color::Ansi(193), + 80 => Color::Ansi(157), + 81 => Color::Ansi(158), + 82 => Color::Ansi(159), + 83 => Color::Ansi(153), + 84 => Color::Ansi(147), + 85 => Color::Ansi(183), + 86 => Color::Ansi(219), + 87 => Color::Ansi(212), + 88 => Color::Ansi(16), + 89 => Color::Ansi(233), + 90 => Color::Ansi(235), + 91 => Color::Ansi(237), + 92 => Color::Ansi(239), + 93 => Color::Ansi(241), + 94 => Color::Ansi(244), + 95 => Color::Ansi(247), + 96 => Color::Ansi(250), + 97 => Color::Ansi(254), + 98 => Color::Ansi(231), + _ => Color::Default, + } + } +} + +struct FormatEventParser<'a> { + str: &'a str, + + /// Current index in `str`. We maintain indices to be able to extract slices from `str`. + cursor: usize, +} + +impl<'a> FormatEventParser<'a> { + fn new(str: &'a str) -> Self { + Self { str, cursor: 0 } + } + + fn peek(&self) -> Option { + self.str[self.cursor..].chars().next() + } + + fn next(&mut self) -> Option { + let next = self.str[self.cursor..].chars().next(); + if let Some(char) = next { + self.cursor += char.len_utf8(); + } + next + } + + fn bump(&mut self, amt: usize) { + self.cursor += amt; + } + + fn parse_text(&mut self) -> &'a str { + let cursor = self.cursor; + while let Some(next) = self.next() { + if is_irc_format_char(next) { + self.cursor -= 1; + return &self.str[cursor..self.cursor]; + } + } + &self.str[cursor..] + } + + /// Parse a color code. Expects the color code prefix ('\x03') to be consumed. Does not + /// increment the cursor when result is `None`. + fn parse_color(&mut self) -> Option<(Color, Option)> { + match self.parse_color_code() { + None => None, + Some(fg) => { + if let Some(char) = self.peek() { + if char == ',' { + let cursor = self.cursor; + self.bump(1); // consume ',' + match self.parse_color_code() { + None => { + // comma was not part of the color code, revert the cursor + self.cursor = cursor; + Some((fg, None)) + } + Some(bg) => Some((fg, Some(bg))), + } + } else { + Some((fg, None)) + } + } else { + Some((fg, None)) + } + } + } + } + + /// Parses at least one, at most two digits. Does not increment the cursor when result is `None`. + fn parse_color_code(&mut self) -> Option { + fn to_dec(ch: char) -> Option { + ch.to_digit(10).map(|c| c as u8) + } + + let c1_char = self.peek()?; + let c1_digit = match to_dec(c1_char) { + None => { + return None; + } + Some(c1_digit) => { + self.bump(1); // consume digit + c1_digit + } + }; + + match self.peek() { + None => Some(Color::from_code(c1_digit)), + Some(c2) => match to_dec(c2) { + None => Some(Color::from_code(c1_digit)), + Some(c2_digit) => { + self.bump(1); // consume digit + Some(Color::from_code(c1_digit * 10 + c2_digit)) + } + }, + } + } + + fn skip_hex_code(&mut self) { + // rrggbb + for _ in 0..6 { + // Use `next` here to avoid incrementing cursor too much + let _ = self.next(); + } + } +} + +/// Is the character start of an IRC formatting char? +fn is_irc_format_char(c: char) -> bool { + matches!( + c, + CHAR_BOLD + | CHAR_ITALIC + | CHAR_UNDERLINE + | CHAR_STRIKETHROUGH + | CHAR_MONOSPACE + | CHAR_COLOR + | CHAR_HEX_COLOR + | CHAR_REVERSE_COLOR + | CHAR_RESET + ) +} + +impl<'a> Iterator for FormatEventParser<'a> { + type Item = IrcFormatEvent<'a>; + + fn next(&mut self) -> Option { + loop { + let next = match self.peek() { + None => return None, + Some(next) => next, + }; + + match next { + CHAR_BOLD => { + self.bump(1); + return Some(IrcFormatEvent::Bold); + } + + CHAR_ITALIC => { + self.bump(1); + return Some(IrcFormatEvent::Italic); + } + + CHAR_UNDERLINE => { + self.bump(1); + return Some(IrcFormatEvent::Underline); + } + + CHAR_STRIKETHROUGH => { + self.bump(1); + return Some(IrcFormatEvent::Strikethrough); + } + + CHAR_MONOSPACE => { + self.bump(1); + return Some(IrcFormatEvent::Monospace); + } + + CHAR_COLOR => { + self.bump(1); + match self.parse_color() { + Some((fg, bg)) => return Some(IrcFormatEvent::Color { fg, bg }), + None => { + // Just skip the control char + } + } + } + + CHAR_HEX_COLOR => { + self.bump(1); + self.skip_hex_code(); + } + + CHAR_REVERSE_COLOR => { + self.bump(1); + return Some(IrcFormatEvent::ReverseColor); + } + + CHAR_RESET => { + self.bump(1); + return Some(IrcFormatEvent::Reset); + } + + other if other.is_ascii_control() => { + self.bump(1); + continue; + } + + _other => return Some(IrcFormatEvent::Text(self.parse_text())), + } + } + } +} + +pub fn parse_irc_formatting<'a>(s: &'a str) -> impl Iterator + 'a { + FormatEventParser::new(s) +} + +/// Removes all IRC formatting characters and ASCII control characters. +pub fn remove_irc_control_chars(str: &str) -> String { + let mut s = String::with_capacity(str.len()); + + for event in parse_irc_formatting(str) { + match event { + IrcFormatEvent::Bold + | IrcFormatEvent::Italic + | IrcFormatEvent::Underline + | IrcFormatEvent::Strikethrough + | IrcFormatEvent::Monospace + | IrcFormatEvent::Color { .. } + | IrcFormatEvent::ReverseColor + | IrcFormatEvent::Reset => {} + IrcFormatEvent::Text(text) => s.push_str(text), + } + } + + s +} + +#[test] +fn test_translate_irc_control_chars() { + assert_eq!( + remove_irc_control_chars(" Le Voyageur imprudent "), + " Le Voyageur imprudent " + ); + assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo"); + assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo"); + assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo"); +} + +#[test] +fn test_parse_text_1() { + let s = "just \x02\x1d\x1f\x1e\x11\x04rrggbb\x16\x0f testing"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("just "))); + assert_eq!(parser.next(), Some(IrcFormatEvent::Bold)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Italic)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Underline)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Strikethrough)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Monospace)); + assert_eq!(parser.next(), Some(IrcFormatEvent::ReverseColor)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Reset)); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(" testing"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_2() { + let s = "a\x03"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_3() { + let s = "a\x03b"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("b"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_4() { + let s = "a\x031,2b"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue) + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("b"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_text_5() { + let s = "\x0301,02a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x0301,2a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x031,2a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: Some(Color::Blue), + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text("a"))); + assert_eq!(parser.next(), None); + + let s = "\x031,a"; + let mut parser = parse_irc_formatting(s); + assert_eq!( + parser.next(), + Some(IrcFormatEvent::Color { + fg: Color::Black, + bg: None, + }) + ); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(",a"))); + assert_eq!(parser.next(), None); + + let s = "\x03,a"; + let mut parser = parse_irc_formatting(s); + assert_eq!(parser.next(), Some(IrcFormatEvent::Text(",a"))); + assert_eq!(parser.next(), None); +} + +#[test] +fn test_parse_color() { + let s = ""; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), None); + + let s = "a"; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), None); + + let s = "1a"; + let mut parser = FormatEventParser::new(s); + assert_eq!(parser.parse_color(), Some((Color::Black, None))); + + let s = "1,2a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); + + let s = "01,2a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); + + let s = "01,02a"; + let mut parser = FormatEventParser::new(s); + assert_eq!( + parser.parse_color(), + Some((Color::Black, Some(Color::Blue))) + ); +} diff --git a/crates/libtiny_wire/src/lib.rs b/crates/libtiny_wire/src/lib.rs index 07a23162..a5d900d5 100644 --- a/crates/libtiny_wire/src/lib.rs +++ b/crates/libtiny_wire/src/lib.rs @@ -5,6 +5,8 @@ //! This library is for implementing clients rather than servers or services, and does not support //! the IRC message format in full generality. +pub mod formatting; + use std::str; use libtiny_common::{ChanName, ChanNameRef};