pax_global_header00006660000000000000000000000064135131532710014513gustar00rootroot0000000000000052 comment=4cca61d83fa2cc0f485c478ff768b0108f6591d6 termui-3.1.0/000077500000000000000000000000001351315327100130215ustar00rootroot00000000000000termui-3.1.0/.gitignore000066400000000000000000000000461351315327100150110ustar00rootroot00000000000000.DS_Store .vscode/ .mypy_cache/ .idea termui-3.1.0/CHANGELOG.md000066400000000000000000000060111351315327100146300ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [3.1.0] - 2019-07-15 ### Added - Added Tree widget [#237] ## [3.0.0] - 2019-03-07 ### Changed - Added sync.Locker interface to Drawable interface ## 2019-03-01 ### Changed - Change scroll method names in List widget ### Fixed - Fix List widget scrolling ## 2019-02-28 ### Added - Add `ColumnResizer` to table which allows for custom column sizing - Add widget padding ### Changed - Change various widget field names - s/`TextParse`/`ParseStyles` - Remove `AddColorMap` in place of modifying `StyleParserColorMap` directly ## 2019-01-31 ### Added - Add more scrolling options to List ### Changed - Make list scroll automatically ### Added ## 2019-01-26 ### Added - Add scrolling to List widget - Add WrapText option to Paragraph - controls if text should wrap automatically ## 2019-01-24 ### Added - Add image widget [#126] ### Changed - Change LineChart to Plot - Added ScatterPlot mode which plots points instead of lines between points ## 2019-01-23 ### Added - Add `Canvas` which allows for drawing braille lines to a `Buffer` ### Changed - Set `termbox-go` backend to 256 colors by default - Moved widgets to `github.com/gizak/termui/widgets` - Rewrote widgets (check examples and code) - Rewrote grid - grids are instantiated locally instead of through `termui.Body` - grids can be nested - change grid layout mechanism - columns and rows can be arbitrarily nested - column and row size is now specified as a ratio of the available space - `Cell`s now contain a `Style` which holds a `Fg`, `Bg`, and `Modifier` - Change `Bufferer` interface to `Drawable` - Add `GetRect` and `SetRect` methods to control widget sizing - Change `Buffer` method to `Draw` - `Draw` takes a `Buffer` and draws to it instead of returning a new `Buffer` - Refactor `Theme` - `Theme` is now a large struct which holds the default `Styles` of everything - Combine `TermWidth` and `TermHeight` functions into `TerminalDimensions` - Rework `Block` - Rework `Buffer` methods - Decremente color numbers by 1 to match xterm colors - Change text parsing - change style items from `fg-color` to `fg:color` - adde mod item like `mod:reverse` ## 2018-11-29 ### Changed - Move Tabpane from termui/extra to termui and rename it to TabPane - Rename PollEvent to PollEvents ## 2018-11-28 ### Changed - Migrate from Dep to vgo - Overhaul the event system - check the wiki/examples for details - Rename Par widget to Paragraph - Rename MBarChart widget to StackedBarChart [#237]: https://github.com/gizak/termui/pull/237 [#126]: https://github.com/gizak/termui/pull/126 [Unreleased]: https://github.com/gizak/termui/compare/v3.1.0...HEAD [3.1.0]: https://github.com/gizak/termui/compare/v3.0.0...v3.1.0 [3.0.0]: https://github.com/gizak/termui/compare/v2.3.0...v3.0.0 termui-3.1.0/LICENSE000066400000000000000000000020641351315327100140300ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Zack Guo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. termui-3.1.0/Makefile000066400000000000000000000001431351315327100144570ustar00rootroot00000000000000.PHONY: run-examples run-examples: @for file in _examples/*.go; do \ go run $$file; \ done; termui-3.1.0/README.md000066400000000000000000000055161351315327100143070ustar00rootroot00000000000000# termui [demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)](./_examples/demo.go) termui is a cross-platform and fully-customizable terminal dashboard and widget library built on top of [termbox-go](https://github.com/nsf/termbox-go). It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib) and [tui-rs](https://github.com/fdehau/tui-rs) and written purely in Go. ## Features - Several premade widgets for common use cases - Easily create custom widgets - Position widgets either in a relative grid or with absolute coordinates - Keyboard, mouse, and terminal resizing events - Colors and styling ## Installation ### Go modules It is not necessary to `go get` termui, since Go will automatically manage any imported dependencies for you. Do note that you have to include `/v3` in the import statements as shown in the 'Hello World' example below. ### Dep Add with `dep ensure -add github.com/gizak/termui`. With Dep, `/v3` should *not* be included in the import statements. ## Hello World ```go package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() p := widgets.NewParagraph() p.Text = "Hello World!" p.SetRect(0, 0, 25, 5) ui.Render(p) for e := range ui.PollEvents() { if e.Type == ui.KeyboardEvent { break } } } ``` ## Widgets - [BarChart](./_examples/barchart.go) - [Canvas](./_examples/canvas.go) (for drawing braille dots) - [Gauge](./_examples/gauge.go) - [Image](./_examples/image.go) - [List](./_examples/list.go) - [Tree](./_examples/tree.go) - [Paragraph](./_examples/paragraph.go) - [PieChart](./_examples/piechart.go) - [Plot](./_examples/plot.go) (for scatterplots and linecharts) - [Sparkline](./_examples/sparkline.go) - [StackedBarChart](./_examples/stacked_barchart.go) - [Table](./_examples/table.go) - [Tabs](./_examples/tabs.go) Run an example with `go run _examples/{example}.go` or run each example consecutively with `make run-examples`. ## Documentation - [wiki](https://github.com/gizak/termui/wiki) ## Uses - [dockdash](https://github.com/byrnedo/dockdash) - [expvarmon](https://github.com/divan/expvarmon) - [go-ethereum/monitorcmd](https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/monitorcmd.go) - [go-jira-ui](https://github.com/mikepea/go-jira-ui) - [gotop](https://github.com/cjbassi/gotop) - [termeter](https://github.com/atsaki/termeter) ## Related Works - [blessed-contrib](https://github.com/yaronn/blessed-contrib) - [gocui](https://github.com/jroimartin/gocui) - [termdash](https://github.com/mum4k/termdash) - [tui-rs](https://github.com/fdehau/tui-rs) - [tview](https://github.com/rivo/tview) ## License [MIT](http://opensource.org/licenses/MIT) termui-3.1.0/_assets/000077500000000000000000000000001351315327100144625ustar00rootroot00000000000000termui-3.1.0/_assets/demo.gif000066400000000000000000004605201351315327100161040ustar00rootroot00000000000000GIF89av%(8&3  ' 8 -= /.%: &*' '($(1.**)544W LrC#H4 J2q#U*S;#F4,u&*HN*Q,y)lV(n##x#jMJlRQK-gS7[h"mt%og";U)3I QcX'SbJOPjaZ &*1$8'4%)9(qz!%_dRnTyfk2(*).2727.2t.Do2(32-)rhj)'{j|Sf5=|=ɛ9üBBA1.y_u;0úƹ—öķƯƱ!! NETSCAPE2.0,v ē+OOMM ORحMԘ$QPM OՒٸ $$P()'P, ,PޑHQH ʹHrH(E7r V AF'MNHXHr&Xm;JI3[jPJ >+Ly( AOL8qRgȟYɊ.k"p)T! fx%(+ *4 iWI%-@P }P!#aʈ2NBJ }UqNPzBgDCvpЬ ͻ7ݚ8(AiGލ}w%YzEy=%-S tmO.˟OϿ(h}e~F2Hi 36qRWA$h(,&: P@5! U"vT Km =cL6PF)TViXf\vP!"zpq$E9W!!mXzg|矀*蠄]^jbQ=lQ/Q3xe駐(ʈ2DO&A0K+g qz-sK@8EBADh;!0)YP ]Laeԡh ޑa"\ʬ`@ 藺 qQcFЃMq[B#c5Gh Ӎd5q [v4|g2$=P7b4DDhzJ- >X< n~[\Px]~uC}`3A԰'јrٴ$N-0=T R^ql*'t)n`406ڳ B)T  I ~*dW5kW{PbϮ.:0y*6T>Qv *.Nwҫ*B]bL!c`+"-mOOqh .,^6e Mrb,b-x{\Z׺u `(|xDֻ5c]pKOw/o|J~Ż_CplU~ |LaI&.1x{ GL@m:2 L0gL81lx]cl._Qk9A^Kdk<QAQb\ Y޲7eb|9cN%[';׸j;3orγ$ ;ӌz4{{_Yi>/ E#zˎ{GKҺ4/0m!s;O"Ժ05Z:^u}c=WֲF/ca[:+zm a;nkcӶ٫X@9W~s; mUH{|Ͷ !nr_gSI6 1s\@ߴ\7*:!Sz鐇PAgEӄT 8, nRز4! (HwB0NzH#*9(f*p9, 8핇3'Rg@pf-  v#2A/ehBN))b Xct!J'2Ss8!wtI.Lq(GRq*BN/Гx8a|IV0A A Rp vFֻWNX! Xȹ"}4Zf;EwGyLg*@ v0u)tmI6[^,)e!,r7a3D1 ,.RD@{Bv/-Hh0g _ S!s/0 q43d^prO_eww>[{fGf׶di1X }6PdV9Hわ`P]KN1`E`V&}hfTv03d., p p蠌"@nqFp(S0 WPh -@ꨅTfgEn usT?GHH&P9E@ S20Y0VP*)2`Z2 #3(w(_0t{q58Y!x@ch,WpY 1d(- E-@6WbegEH XP?Sc1_":S|P:c0"2Bz@w4zpr TaV)H10]r-H(8 U0 hHoY@  @pr@}Hw9xy` UR)``t 0iOUXP<iʘ^ t&dӕB{pt |`AiJ9@ٙ8QY٨\p/X5jy H!؟)J-dY<:)p_J)"[J~`@{gP 9`3:ZH5X( 1u(:h {ZwH`FYn0yK C> o5؋ W`X0 *@XpsUW ( `Y`ޘ ZԪʬJ0~ a ˗`|@0;Z _ܨXz*19Ԙj˩Xj  pqm o:tx I:IΨ.@ J b)Z+i-2,ڏA;UWg iдNNy5C;X{DH"]=˴R Pf( ^ [UyS#UБI1c hey? _q۶K`t 芪,yjh h+;1ַ+\!K9Iz9 Wpjkk›kjk -ۺ 0S;}):' iiʨV 0 "I30,I6/ik hkTpQ^@)@`4Ъɺ۵m W{1I)M0^)}ɜa9&Ы2iq9jY|2/u \)#Q[{ a<ɚi˛{JnǨѧ:I؊ ȷKpƉyyu; ȫK Ul'*0j۱O  ,,eg ̯hzzc7;|.h@MJ̷KÅp I, ZL^, 8$4ŃdƖ0f\>̆ Ϝ D|Lyl-̦ȿz X ѵ ,, )+)傺 [ƫϲK59(řzj(2RXAj&Ѓ~: ݨ*Iz<˔^^pf}l8`# f]ho|r\hz]1=ϱ,b/-jQ}DGZ0,y믽jYL5:Z?ڭzWPPK2łpoґ@A@ p)}bQ \7BKӃL,8MYR ru ;  ߶ I=T5< ΂Э VW,]=2n JqL &k8(+*N k2,,'&!6>lBn5@ p pۑ3,ݼ <-R%ũE>]HbpfR.B,*!$a_b k˛Q@nr}U3 d~i oP> 0xꥮ pxHv,6MFʐLwU.F'S׵K`>NƲξeӎ’ ӊp!Nn ٕSᛐt'ھ$Ԏ$k\,0^t¬ L: yPxֵ%)o?L,:PK`Lp1L /95@`sX쎾[?& [嫌9U é8#9[5I9S:>IBw$,50ʦ&?^;h]pkUގ JeI2YϽ3\?&e_؜lX 2Ф ڜZ-^ ]c?K,nq99E9I{i^>2rL% ?;_., hxHxP"Ȑe`Xաx؀eY p95X@P0Up: @ ؐuP@)P+ -= h w$HMs u3&B/?Oo-m͔o߳X[` A&2)@@ ++4Ž>ȑ$4{XƂ Wf>3%1 Y AH]j(D*dOl4j!Z]A,zdݹgcϞ8t{q_LCW6޽||E|,-,\P@0B@㒋W!cjfPJv1SJ Y2+AB;-E(6ꫯ޳aRS\:C.>{W,` -Ηp ?lqF}'ZZ{(RV4p2P&A 1GTH2"U LE.4a%W4C!RGxGHx Cn%_)E ,} W F! YBBf&A ED,q'g*|qI'`B^bMphOȐEEU$Y[RVhqa*KaQ©SĒ!hťRj)H{6n 8!,4X7`NK͗Y"2'?Pg{ DJFZ} ~kHJth5f8TE3@QHN{ 14 0նRW8{'X ܊2a3ӏ0Ӽ.AFC L!tROW4a|/Xg֠ ތ5{ N#1”i*r yx}=:V5tN7`.?ʐ?82^yo+1[Kn:軒;mK1DVWno|-zG~?ͺ/?mPTj&`^jQUd)#~p 5h~*B7Hwh5BA!4RՁ`E R dPJ3&ԂpUЁ#ExJYXL*aLڦy ^GNys/+8MyuiYպV. M aF;::XdU`CCW*%L BPfwLE.PSdFc!^CZt(͏ a^*rKa ,bKmҒbmF.t+)`Z$ Z N"NW=@4P|ފN׻Cxë=g0]G|F^]L2&ё6)+m0c{ȖZ&n$ѸmnEʂxk1MOHǁYm=U\Xn6~Ԟ묮N ƞ ֞) >d}!+$R  N'p>),NR0 cN߲( ?~&@@q ,'Q+v0(~흮~,(^~`p**NpMѐ~'G+@n ~+׮NN  Q@./$@QPSO Q OPt~St$ /tP&@( g}/O0,֫_Pt?Q'0RouOo&x/P%$o O' $$MO %RPP$ P P N()'O%P $ P ,O&جPáᲟ QޥMRekcIܯM2f ?}DjPMD"J'O4!* ),Tb$tF"+J :ǭ M5,52ūXjʵׯ`ÊKٳf0xäBzRPmR3z )Uhi#%9BaO)L[9m,U8cVE'ą 9}9$@HM0"4 YDŽe;YU)UKNسkν3ZY8P.G?3MNҦ2ىhV"&71Cs5T2GfM`d`G^2L DM# sH2E6, iB EhC B2dY^Z#DiHRy=J#gK]-6XJ%8 >`&^BO؋*v 89'`8g̒,a]R7<EP`rHQD(DŨ<5jR8J+K8a?"&+2IPPPJ*OE-:/ijRX^ Zcpk 0 #fY1.0Bm i4 bp-3S14-+3,wXǡ<@;CR; z[q%Cl?;<#?}=Do=Cb:3q#Cq3>O?$?s^ֻˍ,~^r09 xC`>y^) " 14P DD| s(|̵pza CƮ;HOHxH_ ɧ%qP"IX +p0@V"MhDuF>h`8`+LͷƽrB*+@ Z4v=:n} $8G<8`5Vul$Iɽ2eVol*] ˤ-"E\R9A9As X?h. /sȨ1L43 iK2sB2 v P/!"9I5ʡyP'2$gOQu PePl"r6t` ȳtSt;~R-])Z3v,)ި y > (-gZ IB \4Rd$`{CK~ρo=uVUtaYVYM`as@)CC  >3jE^vS&m, R:d#՜eَ! m-rnσȋ gqjϺvnnj x.k{cŽ)Xe䠱|*׍ a{~%l-fѷM3ǎVYBxS,E} P藿~]gYh vEq* qZŢi;ذk@E{&w^e,&1* @vou$rYAP\sseuFj\qv)$/+JAf!FH-p*,I_уCAAY8 ,Ҡ>/EJGzHʺ8԰s6k/z^Xuz[VOvӑ{󲧍 M.6JnWZ\5Pۓtn];7eLegk3rbdj!wE%DX݀sכ+-S @M ^b-bMmfiqnux-,˖Y46]9Fo]q(pڕa=[2W`1 7|P`p\l0t3] G[׬؍sR,:/?zԾ6-L=t;ឤU{[v_5R:;LW mŽe'lu&&ýH+[^إ/Nz~*^Ԭߣi^5p2ә0GjoyI@7;Mg>Pe̳gІ'C? l,_aG[/JjRɦ45::e $Q_`~`T'H7M6yGU`%V0UVeV"s2:p4WΓA '7XXbuq0ɷJ qLpUb Xe[5`[fHrr%r1p5YZɠ iw4c4YĤ^5VƵ; 1A667^إALgX^Ǖ\m\K D4F9a9h_˔ O1`R ȌЀUuqp|YϳU`0* Z fD?=k <Dwʠy_~[pc+vPz^ Uq4 d(ZTN` 0V>=xs ?Xߞ_efJ֗#7 IE:/;X_wZ/ ^zpx_ U)uq g!3^ @SP @HH D(PHA7yJH㧡* whE{ < Ls7[犧C!(IYx<\-Yy)칞W@@Ї9 AxM0BL$;i,@7Y(g|*na#tLȘBy&ӃB{sI$I?q0j뗬v10qb"ȓ TwVrz#E塄bDbFL$$!H/Yej:еX8KH{LwFlwSڔsΑHD7lX5Nu]xGl%`q0#'*~zI(˂\w8Ph$(xz|IwxΉ @8b#`?`P;` Z:Seve=%4U6 <)dS0PHM ]b'@Y nKh> iNJiy^jin0iJj)$jCDJkޚkJlDZ$"llNR.m~쵍p nn+. ovKoZkoo믺K:pP> qOLq_qoq r"<Ұcr*ə\s2Ls6ߌs:s> 4@MtF4TtN? u83LuV_uZou^ v!2,!d,  )$(6&5 ' 8 -= //#9 '41 '(0,***445SrC#H4 L4p#U*R;&l7HN+Qm-y(mU*o$#x#3W'MJkRSL,eS:Zh"mu&qg2J":U.:K(UcX1KV"XfKOPrZJfhf &+.9('9(qz!"T)jdRpXzhj2(*).2727.2t./}2(3-2)gsj)'|`s83=B=ɛ9üBBɓm2.z_s;/ºǺôĘƱtuuv.2uJ t~yJwKovuu-us:-tv4u~uq zzI4;~:3~.9oy|J949 8&@@%v!G>I Vt(Z<0=ё#@ÇsE=\簀Uk[CNWi#ݽX5`y ÓOj sc$!̽PqRn4#g)sn4L:•Vᆜny|BGLd7 ~hEquJ}ēv' 1ɂ票bQԊ0 Y<:GBgU$ Ap[+!I .1hYXa(di8ah.pGGoMh׬h]%X~RR=({ڕV]| (ʤÀTQW =bZcbfgu񑘘d%. d8ɖ<uF\GVY%Ɨ[6 {cvcӐB:Xu >.GDP3~"J*q3q}@XD|vo2뮄.E9Sv҈ț '\ ),Mu 6r+5;77 6?`6k_׻[(@: |'H Zp#z G@ktnj[VRr)[ gHPw1[ wCpi9aHħhAlBDhIl =-qAh:,搋` chr,Z|"ɢ66Q#c8D8zLt# 1<#BN"!9DzK$-HJx,NM'G5PJLL *T$"ZRnr4[@KAdD4Qh 'DA , L Xsxq&]~XZ!BFh7ˑt :ǙL(.Lԧ JOxs !R'tc  SXEP)B3)% L'd9dNӘ"(A6)4 EQ\5@) !` k ) F '(ũW)HLJUsX0)W_ՠ^ Pi< W7bX ېNkK DXkA VQP(4@ۦoÁuP]f)T`  66 [Vl;F6%^(= <` B`6~ `0ȇk@yo!1py0IMכaCTm@氃 I7Qsh0εpj@\Vr K X` bpZHdAV `ZP Fh!0`(]X؀~{+y0߽3CO(m`^f`Iht] -ܛ [З[`>$,8ZX^-x +[/`@jQ} 頄PSFa0a; E[lC{V{ Zn7 '/oݗ@}Z m'o`~*}0X~"Ȁi3sUx`K ,npZ(' _ f,@|y  bP݇``'o_m+ `o@}>g}\gl` UR)p!_z'ABXgS8 ;@Zr( [0(X ŗZ}}(oէ Xa*@Z|/(oH~i(yP UBo` :b.40SXYh|lH~GYpxx `x7V|XP|Wp臂yp/e,btP{p/u!;`px2?Wp|pmƧs s* @ݑi'P4ԄYuO6hF9nݠjvgjWC B y/9Јސ/:XQ9J  i9vJj0tFuIP2ܰsWnYT摗syP96'@ kv 6 ipᦐ0ҘVY{Ij jTPo`wY(V ؛ްofC"iIY9: XY/Yw*ҘlkY鹞lɚԇ9Rh 8PqgLFw`m 8msu8mEr/wW o#Gw7 =3ɒ>d)ҘsY=Y 6Z ;韵Y1 * жwqvvPx*wwqX XjvMw9oE|wy+;2s1>IAn/ Z{hW|ښ*l;UPH<0 D:_IU+`Dk~*QT{>*H+ ;Hx|xYxq|W vx l7]ʎ,,cEyiƒ P kًkT˸>K[p`K@ܛ<3{ˬLlX.WK @l( t:wW*c > [' ㋾k+< ېsK8P*t#Z&8B A>Š,KL |$4<w9r3◧B{V ȥR)\{D]| ưLT l9 ZyA\J8L̲,|4L,G:w&q 0pi B={7`p7DlpK :7L=sCY@ͤx v xp8Pϡ@y DL7Ly$+${)}*{HXl4 i<y`@hBH`0DZ= ^sڡP՝d7.* [gLciuHꊮjk(ֻ6nH:+Dp1)U,)?>ԠSO^9D-CѠ[뭐o#;!"y݅FܗVks|k|||k}mx ،E>G7ղ=O L0.LMYpc "6 ꍎѻ{wKʌN׀VgVj~nklW@YƊ 鐎 9 03dߢ M[N N[n)O7$#/o87 =WNY@& k l0Z5vE!ȈKoq2= _ͬ/ꂯv*- ϜJnO¯DZ:d액Ҙl,LojiN..x..#Q.$$^oI޽} @)]BP(poXs%z$c";|1].\hs@&Cqb;֔i!J1G :} 5B I1$!5ku ʖE`bTqºڵT gT$~ZdPnLxQ?0vcČpp.O.z :s6.n\V荰nBC<(ljl@(,G! @.{Jak.H ToL0fc+ ŧJyl"(`@i- Vppo)1dA jy EGuQ{,%H̫f%H+t动'v-Ju0M_\%ikQLwݣC[@߳?!ziWED\ 8s!.Mx^DMzbANwz -Z*C{l3h?|ʷ$;K;7Ox.}~A\~6c8ߏސ_? p=p , (?& hAI0`0m0$,g@hB^)l pH op< q7!Ph`Ll(JqT,jq\|N/,b,hF1l|!Ȟ5q!|#D;ꑃv @2} OhC*[lH2dT%/)>bI1&KQr:%+=WʲG%#Tipq/ LF,1b23 hJ9"#ljsܬ ps,9ωtsl; xs=| kXC:O{ aPVBA-f8.`QT2`3`/$"I  l jK&A ;=MW/LD 8<ԟ2`pLYY4`%A8=P4^  QUqԹB5p_+P W}eVŀ3!* /AcOWI<}I(M O7*-ImPz+]4ӋN94+&9N :WSG}FNԆՃf.I %WPd `qӎ Qw$C|-؁=V׳:NBZ5iMՐm `mٓ٘>@'Һ?tڨoڇp6#X-Ks ږN5آ$Mܸ=ɽ@# +3=FW DZ W`m_ݭݏ32jQCm7{`m]"NfvPۊt}A#GL۰ ~A0{ ~D~ v y0܊D-9J;R #n:>,H aB/Q 3~?X E` -M*s + ~rVՆ ՙIWS Y# \?m<ݰ{ p+ +Pݖ`NI/ @ ֕7J-fma#֌l ,ߢa@|6wIё vu}؅7J.׮5^m@ )D=~o /gyٔ=؎J}ٛnNgvf2 !4*7a_q(A|0AJ#O۰*JMv(w`R}^j~P"`/14/0%|@'7 $?J(`3/ 4k `TW=ON5E쥳3=s %~n#U_7]K h:i_ kNJwORߥ?GZqAd<B@^\mbC^?H_4HOoD?k1`빞FAحj?S,rA;PnFAtOQܓԒ F@ßBz YW9z+ˆmΞѴmŃ#ϐZ[ZY /* vQ[{-8!ŋ3J9Cn)Ʌ=V$yrY:IgA3yZ˟@yEL{Z jN.I, x] ճh %DcXu嘚vEJ?`"tP$S&Μ3lvP-ư>xs+N4s蘑po%<à&=3!mo9k3Q1>!GXqz0AB<zW^`p8D >ؠvHFH}ܧ_dE*)a!S1(`"{ݢu> #s=Ar$#WH4/G6 +4 NS ;4^ gه ٢2g Cv>''wY m&h:fj򀉅1t |,y7X2*ҐT_GLA "JI DZ~H+!QG6l8*쵕j@XGldY-$dR R>F{[ 2pӌhK>ȳL$/, Ԍ @)|ɲL232>|Ll8Bp@s+>LYF'=Ш _Ҳ-=wgW+hTJgױsaptpݠ(wo%n?7$R@i3|!XhC7%37a Ib-3 zLmG^.rԛ(. ܲNyOi-{$6o5[ۨ\ѥhw Zh!K<N 0_OOq,E?Ƿ_&~Sx&0pGJ-2~{9P":RxBRXc_03Rnᆆhï`HD *<@甛tS$yHtiC";+ qJE\@te{vB\ $ nt71%dB\890h&'N~02WyH715M`! QS֒4diyq@%)BwQ0KUtفEm̃AK$}h@)LZBm->u$J >P;:(NAVII<+5঄XS=b{IDE*Vu ao0y^m M;mEV \pZګ!SD`,h] Z_\5 ] cUu!YNXjOGS`dG&tۓvh4h2H,7F,l=.ВX$ !d,  )%)6'5 ' 8 -= 0/$241 '(0,***456W LtrC#H5 K4r#U*Q9&u&*HN*Rm-y(mU*o$#x#1V(LKkRTL-fR8Zh#nu&qh*R#;U"ScX%UaJOPjd] &*.1$4%8'/!9(u"T)jeRygk2(*).2727.2t./}2(22-)eqj)'|as83=|B=ɛ9üBB?.2.z_s;/ºǺõĘƱ<@BBB>B<??@ >!?@!BBˠ r z?AD?< 0p *@8=ac~P>k aC#53 CEC (Zϟ@b3\:H*}dHA|GÏ B$(@XL+%Aj?FRFYdem`6AࣇDbvǐ5e3UkXӨ<Ҥ[g+HH8Kcё5Tۺp+. =Bhǎسk/%h6tËSI^$!H $@Bl@٥ gC2 _`q4ƒy_y``(daވ(h?\`q\\sW!\A2` ڂw,>r\uB28AHdrҩݰawGq)e'(|əd9ʅK^ԥ VhX V NfZI>1DlR^(@V66bvG.e JЦcM 4J\8m*(|^CDU ]%KVjy?}P&V[G?a-B_zw.T ˵V/ꭣ]&%FM íO0iY 1C&L)?(,H945'֬<%߽"DmHLq*"-MWmK,K#NX-<"~6lv)Zuj{xe]Muހ޳[ )]܇#4qFcyky瘃~9ky觇Nzoz櫛^:.믳N~;Nz_;onN|gG-=O|/㇏쯯~_/O~~;_PƳ=1@E Se \A(SB&0 уPTCA!vz"qB &*p ,HEARD,q`,b+0cL2kl?qx`s4Q`՚Mpj]kLN [Bj(7l6dٶUNQɣhs>Q2`w4Z5k4-ǀ映eI33Eu2"Z0X-TBlRT'X̻Cx ~ñQMRFZ0h `(++DaopCpk>I1l;Wv)M ,`FīЂU]bU[@Ms 0BdE ui |a9@q}c| Ѐ,wMQlc!0`AͳP5͉*XPMaN}޹ZPS,΁h>7v=78,P$ E> a4 /Dx)k1`7۱P@rF?O 8`y' ]l ~= ֘,@BeAaꣀuGy z` v0vbvv '&'77$yjYmه}~}mxƁ ~@'G :8~@{vuupxo`{c0uFdwpwj|wkGmsG  Y6( @xfx~vm떆m쇃 `~hY pwm XtF8XuXH UpMP`kzЅ`7&e(~u( `B|(ewmj}WncvE7wm$}}-n ~ږPy 8 vPp\u{' (e8}Zlv|oSpmSVwh̶VZPVs8ٷl "yP~0~@{먎wn~(&))3(~hrX 0nFtا > PD5`hHG+Pl[o$|aK`)9~Ts2@)~np`2 ji|mٱ9k9rim8gט~Ǘ@qih\ w酓ٕY7Ir׈ ChY!ə18kܧk jVmy8 Up လbq$֙5_{ɗN,5׃ H1!8.l밞BymlD#b )٠ n9'rr'2u10Z l$0PsV r-jArd, @s07ڗ 7`ʝz  ^&un\a*+ "[ 8knkwqZК*`H}G@aڐ3Lܚ;KPy[9)}t{4PVZ9J;L>6qR*lkpH N8~wЧ@b+tl{Ĉo`PHmV~R ^jک Rвjߺʛ@ػ{;{gF˸ XJI}PmThp1WQY!yU_K *ؑU W|' <7P00|+;+ 0<;뼂)šh< pC۫J4< ךڐ z}Q P~hď!h}eGOk\9x;řÿ9H[KR2+&y10gpx^jxDz~|[ ",_l*zGj)kܸ27lD5WAŊ,$l¡<ʦ񪘏Ål<9hj|^9jBcuJ h6 ܮ<hR\ ,`< J@ ,aSɘ|Ɩ8,˭@$mɡʎ̠6<ϳFԚG\K\4KP\ΰX ލ-mhKWJ02- Y{@Nh(i YnELI$ mՍ> RI,0U)ďΖ}]O]׎x,M ^3=G <Θە>j.G<%^NΦИ~ :뺮뺎I\NN4ȎI\Ia4|ZPԾJi@~\ nxP~ 8`  y,)pP~ps{|`.u h1 C["=& քIN 7p1{0P|0@z`_`` >@Q7py`X.7@R8ʝh/A/r22Dn#h?jbl2(zX'v\׉`< ˙xp`ًs[0܏z@v-x hO>d_"OqXIGZ} *_'k׌# x`u oz\_= @?Oþ _-y@Z XPhxHWr()yhXG8Yw ٩ D@yyYCw ,=3^N~q,}>}^!h?.ha*XC?_qA Rq'XUh(w,[| 3BeCNjwzvhʲy]6PDcX|=WO9źDž̹t=u8.-'IV\ "1N Yd;B ?.Tt!8P8l#0*2w ãl9p7w >|©ofk0xgirƉx}& @G~p {(X,Es" *E0,aQGB".ǂ˄%pc:21zG$W$9RI> $3TV RTyeK\!XPIZziAgdiy; g=6Sxp'G9| B2 iNJs5&٦~ j9=5ZS3jH))ΪJkʫKs vh,&,K2ZlNKm]VS:m~ ̵z^㞋nKm o+on[oJJۚl .p? qOLq_1Ƴ[A"Lr&r*r. syk4':9L@Mg>=H/LtRKԓ muʨZb#csj2av-J׍e|lۀ'0xW-yL_yo^y袏NW̪z뮿$N{nȨ{|O||/|?}OO}^T$5MF #jL0tϾ!3a@!f/L7@"^AxQxa{BhplPCJ$&/Ѐ۞G8PmA `` `g_4(g=x-l(Bj 1'Іpw3@@aG F/ġ 0|D@?Ҁ p= jC HJr!Bد`710$7B1lHe)rhyr/ La-$DAᔽB!H8`Q#I*zML`k15i!@! 1l\dž @ l`d(b Er͜ 4=<9('dG".+U' Bn@58ӤuC,`3`TzKi76|&/JmS UhU]fcTլju* ְU\V' s Fڸ֖o0 ХSu|m0aflD!p nad'Ք2Գ@C|0a01%"2;Qat!(Ԟ+cpAp bPm6@>XtŐ;M0$ R,DjWvmQ `t4^ LY̠Goŗټ"b,"@ Gh+ Ozt N=.| 3ĥ N1ƶUo^Cw|gOo]7Ct vpcEl x exD+=&$)m,n_KS;_%8B3\Ć#bv8Vjr͐A7ǻ5t$tc1+Hzu=e+, 3a΅x(M_U:6 îуWCw*[A]t8"@+U%s3½/l}Wduњo9 t&S_zͰaǿ Rw4ZЁ`xp OP^pzݒ $u9tlV@v'>|!VY h({1Evz|wx@u.!,ږe@-; ! i,mO4 hprh3`mXU =eP4p{i m c_ۏw~¿Q_mtݐ)VZ 8&Pi *eoeouCVPrvE$e57S!(zSU'6+/(Mr3hQ77;Ӄ?(Ch҂8dIKȄMOOXLUh#;W[ȅt_a(cHehgikȆmo#r=sw/A` zuчxxt#5 ?1$ (`.1OM@O0 {Q0ĠMsA $0 #?h X ( #јXX ܸ( ( ؋XM ((*h Np)#(wXh%RD p* ROxO@N(&it Њ$M$i-')+YyߘHR0M= ix`Ɋy"i6=,;)7*`X. "`R Dy"x*Q=OВuJ7("e)) 0x'QPwQ@"p{ٗ8%PH#)P)E9('9鋄ip*Nvɑ( <9)I)) *"ЈA )P%hMYP0 3ȟ 0靈$٠: 8^Zz( -JYX 'PY**)I#5Zz#Yy'Z ((9$8:6z अ"GM9(#P`o*.zV?[:Zi9dJZKQʩJ:ט(zFYJCyQpMp m $:Zdy)꫏* Pʪ[z*ȋ0XzOp $|$ڙ@Z5ʥzSʪʟyКЅ2!S:8(Zڎ׈Эx:Rڦ@@*:Tkښ&?zz RP)4*WIʊź7+9 N$X*ʡ,R0K!@W٭IXQ:볯HAFۓ L; ZMaZ.[)J :X  +ʮ ȬN0u) N둓QO p({s뉄鸏+ y H+J90ཇɪ2!Py@+8kt17+)IXS(npY/)X*9S#l7|kg<8c%*'|,xr `ÆPÇÆp9L.# !\<0 PJL @ P,KS|V oce0!&r/vQ#> @2|~o ͏;.<r(Ƀҟt1W- VX4^ղ.Z`}C3M+LŌ4p "Y\+꼾+/. ]Kc.ڧqܹY0pE޽pB0꺲dSL Q#ʛ ..q,"զNﻞ:ͭ*N.v檒 #*L3N@u*@+-o-51n,BcDs?_ L3~p=GOвLm]= 6.ۆp*'}^ - o?|/ 3> ܅Ԧ`֭RL2.nD3mO֊O*g}CPyMvm] ?.͎.O>43t^/`ף"ď~ isݎ?lԎ*y, To1 }OPzo[4~cIzH4yH8,9~w}/{. ?ÿʟ?҃<֚88/pt~|%$Ç#JHQ^Co80xSFl)?0d {tNpSIѣH^KLjYg'=UjJH@үXVBBe rطpQPe *{6[BJ%}q\= +^̸1 ;GK uU2ϠC+-Qe6H-(ְcˆ:\┵gy߾{HqǓ+-x}N*,kŞIh=#A5dGȣp%)䒪8wREV2yyÊD^"c(&hj e9ᘩH)QmfUms$3?80 6=b@gg l7O@bZ=l,iaW֊ഢ cv[q-jԚː].]+l%.5~̡#wvV,8L~yj1!1&`An  \c!T3c\\yyn',Q-q T1/ v#]6`lXi#+ 8\rur5r@J5m4LP`xߡY(ir^"ju"r,8G8#1 ND4GLutp'p!" /eq1|dSz z!W}}б=Τ}ep>wV^2I(<͜ pkӈ>?ɥCJSL6="#:P G`A2("{9$6EݾA(ߤBa،1 8c!O;T(cBa.l f9dՉ@bar@&( *:z< ka,R,^D@ae'4I `H*Y Hm5pC Р1c"}F RbW(r/Բ勅L<^JP/h%(gecUKSlJeTY_Lbd2"Ť@!d,  )$)6&5 ' 8 .= 1/#9 '41 '(0,***445RrC"I4p#U*R;&l7HN)Sm-y(mU*o$#x#/V*KKUK.bS=lQ3ia*R2K":U.:K&XcX1KV"WfKNPrZJehg &+.9(0 '9(u"T)jdRpXzhj2(*)5-.2728/2t-/}+2-.sgj)(|`s83=|B=ɛ9üBB?.2.z_s;/ºǺôĘ˯Ʊsttu80tH us~xHvYn~tr+mrt+su2t,vpy{H29|82~,7nxyH7qtsw#ɒ>w-G!`(,L` P`46 ЄmTDAPЄn(>9t'`@oJznB 0 H8M`(yF@A&NS 8A!h6'La r F֒vR( 1NÆ'xBYͺOA !*.ʀ4uy*5Zv0w*`lgKͭnw\VY @M aBDK>(.X@uBz Z~ZtŹVA4P`-@o~/֗ L7ry}4&8 @D(̹ PPʞu"X+K(NvѮCZ bQP Fo@Y(Dө%.0\{`1C.{ @)^|^6jPr@(pU lUS(8>&`d4 m.k&07aM' ^F\m[Ib4t cjejIL g-CSRs20+gjz\WVc6kZ_q"(=1]:ƺan"*؀ T`?  T*v:@n3s6CYxnjc((RpA.PX.kS \TAEzl;6L WaS:U^)c. Sko )ctV!@.:\M7H*l_a/G*S(b&-p?@X8EMOW&y,؂:Ct` XzXa)'bw-[ rhH \y}/@|ҖO3^JE.{ 3w:A4 p`J'otypRpyW" 0rq`Faksx0xPO/Q±!CsW~gs17 xrXp')r~6uQx{n'gu&o"~ y[؃Mak1.rNbwpI@0a`y BTJyPXtN'>oV "7s7+ p{&Xhu](TGrV"Pan`@2%#X)r&B~;x~~N1‡񧈢V TA!^h'yVȉg'&go|{P(dV#i-2 ub wu,"~p8 8eb8(SHPjqVЌ "pgUPiz ~WpCzQ\r'x`}p/9Ch:(1=G `"  h  ƕ"p| &P3D6¸8H&*t?.It99vxɔKٖ4swٔylqppٙipٙclXw mNi| 99 ٚ)I1!9)7JٛyOa!ԉi9m`kkYꙞjj k)sɚٟ=&О3Y0 "97Yq4:<i02ܩɛ٢ʡ 8k !;;9ڣf:#FJCFQڣ*?ݩJ, 3*eJܠc;ThZTe;ppm;& ?6r:c\]: " d¨eJ S|4~[ڜW*j hz\ऊ=2pj@ Jʪ\;#Ze/ jqƦcP@09b TZ<:5ܚ :ʟ95Z*#zLpZ[a3Z i̊Xh`˱\=۱{ڙ% !۱ǚ: zpL) '˙;;=?;zI4K[6 5R.+fv?U Pص@8PjB&Kʶ2tJ tJ k+el`2{[I9IȚB ;ʴO+֧Jj6˹+}JJJ5P@ه0K615+13۹c3{뻃+Jp{n{`{`"sc#!p+7# J!qckJP;b {Jxolw W' R%Gb#[9d7k  [6KAԸ;u }taTq*`p4KP;!K[6{r2l˾ PƓvB,F ̾UƹYLɚKǛ<6rd{̎+ͱ;ٓ"8 <|L|N -E˵l;-ЂщoqZGkȤ~ -& $촕;~ҿ<7mG+\8}|n8J;pKԀC1Q+ECON:Wa\Ֆ1^m8` g==f]\}֚c`g'_I rPps` /" p~ {0 װz jb;@ A`ڨ >;drڦAPڥ}ڪ}p) s}aw2 {N_lMrs؀н 3k-\?B0@=\'-M V'3+, %`\E2 q N >1'q4 Ӣ ~O /.1N N>>; +1@- A1lpk9Pz&~ ].@7B`==A> o>o> vt@~j:y0 B  'u" ^ ۑ |y `}?`~j+w| P볞%Di #y8%P;͍ #0 > 1` P0у=\L].9 >3o_F}`⒞ q a,08`# ?mA+Ǥ@<=`>߻+&<'2o4OP#?^&3^ 9" w+9_ F/Pv{G[VQr&݇}|P%_ e/TI-<=6P/^߸^6cR/~Pb ,.^ . [ \OwaP._ٹK~/v@x!O4J ?1ǿ01Z1@`P(  |Y@:`_-_=po=p0xxX)9IYiyyH dz* +;KkʪS ,1( Y 홧G<-.>N^n}Lhx.n|>O_oo 2Ϡm\1D'ZTpT‹;zWHKm2PKR˙4kkyҦȘy bȠ;s&'ҥL:} 5ԩTZ5+юCr5Ȃرd˚=6ڵlۺ} 7. f ic̼| GlČwBȔ+k̜-7$$3ϢK4;Ҫ[SZش®,^Qp/m<9A!o}>-ܻ{~55R˛?>u_.3?η^H`` .`>aNHa^ana&\tHb&/xa _pɊA\TPb FA%<@"1#`$$)0$I jрΐ ?@_( ="!? "\`‡#A#oT@THY` #^t@ @P0ch  "@0Q%@Yk 胄~ADD`FL@F[ g%5 k  :' j B64묝} nKnw:8_x=F"b{{IԚ .i"-p? qODK AC0H 1* t@& R6Gq;A@J ["\~\ y: Ph.rj@{re/f @ZD@!|Ht+I@K@qw5f2= dk# 6 ^ 4 . +;zꪯ:.ĚN{:Jox{WG^{9HP83##_:Jr<0t 7@) ԅ3Қ~$[x13'rlq)D|X+'d.lC]]Bͼ4AH!DԳ1y#(ZN^8o ~!X0 @lz&F]E@h -|KB+q5 ܯGĥx!a/~E(1H&Z!lhC*w/@!!h]C@!} Ȉ-@`0 @("0Z1: д)/[Q^@3~* "r;Z#&бaPBAC6Ela$A,r[`A|s6n0!'NP"\ |y>x`G 50 L89/W8(Ovl/~GoPz| J 'pw& @y w|| __/{п"8Q@gx(@ @@@D%@A ~x&s'vsv'~ q8(tx$' PGu7'"!-xqGqWNyX{|~XQ0"M ЃUO0T$/' uty#ftuRPohs!{t w}#} Pu X`r77Nxr`o8& ]p舄Xwxs ڀUgr xEt F 'hQ( @GP'sN`wq lȍ#8hP(|rX$@xxhh0fW/{IGN8Շ)88n'sLN @ Hv`p' gr08Ȋc$r9>aL)Wtj㈇昔^WɸЌuSW9PP=h6rPgmik  |[W)w^&*/RyXfy ȑcZg$nyFrx[AbnXt(|I)v5\Ɉxf9w(nסҪ 0 pPૈp ;Z0lOu Bz <0 Jz: 7 j B P7j#j *B@ժ ۚ޺ 9@`ʚ; 6{Jqz% )1+, 7C*\>K8 . `W`VPJCK1+'&X:਎ ^kDdZ zj{&˶IG00>wJ|=pmkiI(v{t\a{APO| n  @5 `[s`s0Px&]@p+XV0PKmpr0*~Tny ʫ2:0+:{0PTǛo*>CD11 yۭz[ :  P ;8zP {˛ 5)b {zj (Z,*0<,P"LŒ?AXp n`a@yĉث?bʮܪ K _Li 9s=PLS  J\7и)CYY@ ; ۱Ȉ"zR7L|̿{˅s<]pHbʄʯ|24NKbXz@~0b{ɷ:>~̇` ͮ:pA㷋J͘ <B *΁ H{q J{PO;,jP 0`Ma[` jhjg-;]2+=F &;ѣi;<-K۫B-- '-x\"}ƜOm @͠Ԙ| 2ڮ-6A)PБQŖ@ ť!wݿ[kTl`n ML K8`j ;UW ,@~BFC< MX0@,] ah=br~V`=@a #`(H0&RP}W|/>̋x.0p#CD}E@a ؕ }[9\<}l 1<<Mn 2~޾* 虀`L"&N֌A >   nܝ O3, qPۢ{~ F n4Qk$mܧ1ݪf0 ӭľ!@? rC 8]=O.-^V %#/m>0,eeWL,I}p.v amvx3k] ? I żWBy׎~mQט`Ȑ nks .M`ȑȬd s 1& =@ ?` L3KNo-GJO ͽ&yn~rq}l~vlw~̈r]v9lvnr ӎ7ꅺ҉7]xmv p຃ 1Ut1΅ X,*<@<8YN~xť!$B8KٴB\r+۹c  < ButX(*2m| ב΃=F8rgb < t_Hlޮlڶ_ ;82mBu:A 6:nHJDacRdqԳxDh R.Zrv}'`xB"i6aHR ,*B4 5J!CVŸSe#@#> ;>dLcFf<"3@+҄$xHM$ x6]Ip VśxU*-S6naJ`V !vQh"HrA- ¨ XQzX *鬴JETǨ !0kf:p@B<??@ q2Ib<(T)Vވ$^R⊣p$lfEuOrb!,~ 'gpYu؏|hKm`#!(DEau,v7嘝1d̔ h(=cUX"xLipH{MţټK$x\.!) 椘2h^HNnDI(^,N!XR∧Vrڣ|pe 'c 兩RF+ϦE VzBĶDpD ʷI i8i `c}))c -?phrQ$BsX'9 7] hd@tVlX TYىϦhc ,̣( 4\^"쬱x@mHwtPG-IWm21LX-ؒl |vd-}i6x筷Hw-tڅ7Z^?ny] ڊSNpF袓>餇~騫^zꩯzˎ_^z#{/oCzO^GO{ݫN=R_NW/~c_G H?-0~d ι}Ü788 lG6P# WX5Oc g3Mw0< 1MCLWCqqPbw'JXl a,z721b6-qHAkxGnqb@2F!&2l$YHXDra%MqLzr,E'?I|H,*7JRp*KR jIVn3)Tm4g\ d: ٶhb[6()+Y8VQDg6A9ZX@ XO O A>Q'b3 OC N4NXɗL < $u8m dçZLA 0g* 2+V@*UR HLv H!<*K= (AYRtl be$A*LH!Q-jͿ̪V)FP@eqm}.K Nv Nh VPᴨMjWֺlgKnz@ T%R~e,  bbS)8 ׽mzEfh ` Z|Kͯ~m"xoFa]Z[XƮ f,t V&R E7 p Ǜ9w',!@0Ts HpT*x O",WYul]$u QBXAK]Ba +" f(H~)@aGvp4{:JW`4@ffl,P eOESle$6'г4\3ZiQδ >"E=S׈t`]2,,7M9:yԵeh^K׵^m%bc 0׆ 'q"g_x;s_my[P;/b@搃 a77)L+ ;vkK( Xr>!8C>3 氇# ~8^ψW)7q@}p FO2/;n+UO@;xqFpPN)N6ZؿaQօP2o?{_6I~?(~wz}'qۗlkvG~H4؀9Gx0xBlllP-048:1^34Ph'8|)Ca|s2P2Y(LV;TXHȁ(CihF8 a(;R($23GfhƂ{xiP#P̳u;:(h(B|H9VN8OAxH:T('Hs:@2>pzhehgƷ֊pp0`-@UFȌ8ȀL ࠍH:ix[X ޘ3@͘WoPP)8 ّ@pj,:+-("iؒ☎b} u4f46֨G9/ٔN9H?Yn@YsBIGD97a̓b)PIjYgydٖHV =ME{GE)~r~,99WgMC|Px\F$ I`)ɚ\%yK |ADDЊiZv )9PGu9yI9{(5K0K848iw5Y`s )5uv tΧRK'P4'0Z9K @z95 5N jԆ@yX;95Kp YB`(%L*Z%zB3d13Zz};7ovo֟4 8npu5LL-*;4`YQa9 5p_vӁ(\`qtr#`rp{ץQs' p^s %`^#KxZJkc\r;sut@t=3r@y:^S U*JC#9$MZSZcڬ ʪ9[^''dghX57jP4fʮDj\\p稽Uq+yKkG{~Yd~3VyEJ6ߝ9~nGy~؆ۜ.y[]ꪾ]^k!6^~^T;뿞+`Dnlzy޷~eӾkE^vl>nqi~+j>.[Y.Ho_>U  o?_ mS)_ ^PP(y(B50P.dJ$h3@U5[E[2py3567=Z`0F01UD1YHEHpPU iCc-pkQ47o|`6 fe`KDa)ZPVXhk/` kPؘVSg`hPp!0Z5JqdYp Pq Pr/5XiП~5!EHwS9"a ?_bD1@Xhx)XEv#&pWw  7@ P@1);K[k{{ Y3f@HVD`6B(1Afa3{ȶI::/?O_o_,Ll\c& 3 W#9|ب\&8+́4k6ڂ)M%<{ym%/6$iD%} 5Sh(5$\z 6رd˚5ڔ{:}{ܹt놅7?~< $,p|oF^!k 憤勠k.I C^|~Y\\hs!;".%Kֱ f4y(EkeP~GgE!{ @y("1f31М`wȅ_|1^jZL?^nԌ-Q8Y#$6h(-" 菊`vCȐGu!xrWvV.B!#ZT5^)2'a<_0ۉ!hBxڠEej6E1@~'g)w(Pj"dq(z ơ0 0^pکsZJaq3a]q9QGO֛T!"LPwvBqkFRF1iR(\ ȫ@ 'YSAQ[};Ȟha1Vt Aȱ jځCR ĹhF Gm".l.47 fK0 gM gq?5FPQu:Oʩ}=7v]o]Pzt6Dt5G U܀),ooLc¶.r:܆v! ~>UsQPM^:=ɘҹQ霬 iٝ(t2P9u; |`)K&蹞; AF0tz&8]sؑVItttps8, '!Wp+J]0eSP ɑI=J@B H1]ppx0&c0s;đ_ 5.5qv:r"/`,Rwi)K}]3 `UJ)ʩR)|*@y!p i 1y CA )ɫf)wJ2yګK 1*j_xBA2;=PŗI|٦@ zu&zJ9`:;H%ActZ]I yly Jڰ{HK%Jd0(S;X2bTyq0ɦȺ{ IF{L2˛ fJXhy_JKk oy0୦d-xiKJ<`A) $$K-+-ia\&H) Zk L,Aϙ%J <J}.!k[A58=,Q? 'q¶`ąQ BYdt;~{đɺMG9V|XKv`7:`Y̛Kw Q yp\B(ʋ8zpT;V [mJJL\&n@eRg2`Ŀpû;ܻ4glbhhÉ0zʌu!è𨑚@Щ2ʛB\l@ͦ \kr`Gp,zͲ@6)\Zdz˘~`ʨέ­~0Zʷ`pċzmJtIС"M4{Tಅ$۠ c77,stK]Kl֋ ,K=t> u@,=/Ʉ ԲhkjMp V[UGxq0Pr4N{ʼn 0 uKĊ20@+|K^ٓYo=ƣêP\ҭ{ %۳åP۱ĵ@s}&ܤ]+ݔlHJ۾ ?|˼l|}(ܶ L,ጩ e0 c<]\.=5 }ܺ@Fz޴ Q14Ͷ+ޓ^`MqLx1ܴ8~%?~I&`$1L6ð2HlV~]㧄H`;g. jQn柪_^Hq^KbwH9:|P:M~.ƆPxny腞qa} B9Nz ^6q|&^o,9ϓȍrN> Az8Gzx .^纠S^FǼ9@m045A M ޏ -S+̬\pr0`̰l#\oyP&Ҍz$_{ߏQ^rM?j(f%/I1@эp,p,χ)/(Jǚ8˒] 2*-0\pnb< !TK"@+@| ҆ɿe3mgs-ARЯn=xA}1_< Y.@FR&I}^> Nh-1 1<_<&z}h},~ }떟 B+7oCE&_FppmWKº/GK> µIM"|ѿA> oٶY4oP岜Fe~I !d,  )$(6&4 ' 8 -= ./#9 ' '(0-***44541RrH4 G* p#U*R;%l7K*Q-y(mU+o$#x#jLJsTfPqgMO(eS:Vj%ls%Qa2K":U.:K(UcX2KW"WfKOPrZIfhg &+.9((9(qy!"T)jfSzhj2(*).2727.2s.v./}2(+32-gsj)_s'{83=B=ɛ9üBBɓm/2._sz;/ºǺôĘƱtuuv-1uH usu,t~yHwInvs~us9utv3uysq z|H3:~92~-8nyz88 AH0h}@K/Yx9~fG#%$%#=>\O9BŌ J(5}4ʴu3l'6xMgUQ- {M;9<NE~4cuT*j۷Rkɉ׺l!ΑS"~JeH^ٲ{T@=0uNZA_Ss3ǂl GR})M@ E@DWcc#!̽QqZ+o3#Gmyo;rؓ~)\m\)eG31H馜4Oz'TuJ o De6R,((VK=9 `_L`XBp ) EH4 IF!U8(!bUpG)dh`A9-c5]P%փ ? .TG_QWI[4*q݅@驙iO}V0 ZN)JG :k)@ G^/oƛwK' wp@ 0# R7 6 !=(', H0. g@ll.vlvCpw9D\HDAbDI\[HŪ=Q\BE]Y/v#dLYdFnqpcgڸ7xd atD dڀA'~ B LKAlQO0@N <*JVA3Q<r§O]& hB@'x5tJe*: & c'5/OiPE LȊݫ@JV, ]@,Ru^U &vJņKMr:xCìžhB8M[zաr}rWw'lAWύ|KAdmXALZN',a 'vh ތ. E3`OgL_~kG !}xH5Z#P}jfJN2@ r ж o{EVGip:t0٭"OJ߂x-loX 8xQD_oDtPޤy,q`A..BANh+V / 5%' /@Vx ĂlW " D\/ CXo搃4i7RN@Ĺ>un< xAS (Gҵ@0BCJnQ0=T?XӇ@ Mg?OІ@CIް%CsvtvJXY@Pw=hG_|OɧoUyr RVX!`mWphFw!Cdyzy0D7Ql~Luyp|K0WmBA( p;mنq!|(>w.7{vrxpIyM-5q/WG~TXN'@8 Y|ЅcZcC7 G H`'[fH+*){)_"hKxHldllǖ0y' Xy0, {MXrǷ3,@OXH;H-011n`4W 0R.4XՐ|Z@ YZ RV|%QPVWyMXW_H֘m9|( !cu@*%X SKk7aqMP@'@ X Ui kRk2@dplm(mH 0P|spTXmno pw 9ExIIzifGٗ4l6ڠ9jp9O~֙f|ig)yOsjԚ|C1mvmx۰8fY鏮 8qys98(hQ(@' ' hDYrkTAlFɞS8 [ >,w 8 lkiU0kf ʠil@U4ũDnewVptht[`uyR`"FtԹV`z④vݹ9>@?I1PJbM:>cyǙsyi@ H 8z'T(@m_ Up(D .`mpU z)q)# I >3W>+2X*<iʘzY%Y%!Ȁ]T Y yhʨg >iՀJBɚc*;7`ډLJVB7&CÈqY9Adjs;TAP2K{<601@1p£z:ڥ +Y ۈx)GtHqWP@Wo˳YYo)yƱp@ ڪdza[X{Pk)***`kZ4Ihs 9nhEدH6rYis Y0Syֈ |&OkP` iqpql`K+;+eȉkrI?X&x栄XK(0WVpXz8q9ɗ ͛ 및 P+0P0pk꛾V{[$븃ꋥ,K\^j횶j&wPym": 0rY ҸY: Pľk z$U ӿۺ4, m+.78$\AL=;\ęz+/- 5;.5\C;B'rdsdZXyhHB}q@v6׍ Ҽ< 6 .ʭڭ7 <إPG(܀JWZw rcs" ж׉ ˉݛ2kxph Ml8z-RlJ^R~>:P;ޥPR\'<УzMY܎ݻ'Fnɏ7T~Y܈ $XZVn} ;N'xpTuyhmoσ;،y}⊢P G.}+ҋ7+mܙؕR JJ0" {U`؋ɛ ls>W0(R0Py8o ;Xp6 _ h ~O|ݮ̾LLK#- 0zcθެIʣ<9POn^AmP{nJaʓiOSgD8q\;P3lCO_s˰m)Z 55,:o4xqo/pgoޑ߰ ݝ9GEY0x`p @/ܠy@/1 п  _ֲ O@( t=ۋ /4A?Sh(##y!  JH`e6Wu jJwǷ%eavˊ@cQ {Mw[3 (X.#ceΰ@**Xyȅ"=xaED/ ,&A԰ љ< ԢSy Jv.a$thv63h6ڔ)4H*eHBP ( Q?@u4vnlmSFzZT+PK!@?pG Uh"\hY yRRO|9lSVPϹaɣKg5jxFKS֎IENȘS'(yE=#y&>}Q7xJ p_Y*zqjS#.0`(4 daE`mX_08 +(XO`QV%#!% dL3&J8"$` zwkr+.+l2WsLU2|)^XUW&y gtp^E pGxYIꖛXt"+<1T`#. ZH(HN Ah1ģ-t M1GGLⳳZJYH/Cy/,I5V{aI{`&JpPK'R9|{s8,FbY>BϣY 0 a=ZxcZZnYDP,ƳQA@y<w$4]I{6$Pq L4n`A" R%yG݀W8X/6/!VvUX>dca- @h f Y"plZ`X)R^!8dlP(y< n¢ s:KvI> ƸE+ RFae"HQR( 6R(v&yIjRX9方3d @5MȚf5IJ"v,9iN@ RNǙO̦MPI{sLiPs-AZ=0 (E/ьjۨG P~t% $t= K_ː4m` ylBy@ӚHB-TP*u9ٰReCԫjU2VY⩁@: #Y (HG`0 `/pz!#$AQ vtHUc E+8k,S!|Ju9*p3lg? XF/p e0T:V]mlZxѨ8&ªpۅ*1;R'CڡUjmDW޾*V(->+"|%C#Le dTl]VW x.+ؿa ';nD_ ݰ{Uh_ cS#4oX3ޱc3bpۋ VX"T]lm"Z30A wŪx^m&䨞4RY A|~;2~*@,|wW-3+ y|"1:T{TDgvLҍ^Q|Tsòa0Xꪚe#ݸSToZ{ĞDWRb;njggTҮ,k_xr;5]q{݌>7w˻m|[7mM |/G"| oo3=rŁ︍9Fx|$/OK|,o_|4o|< }D/я/a Tx(ҟ,A$a(I4L@_Կ.#e::`v32Ht :bpkHCu3B"] @鉈  1H4|N3`V?@ j7:F(8q  `{& (@ G3!.45P:ؐy{phA(Hphz`XW~ ~su4}y^|Ǘy/(r')+Ȃ->Vk  (R`sTkpP7G$wGk`|5Ws18v Xg f 57kly^'k^|ktnR8sTHvVG^(gp7{sзh:uD4o}xuy|Pg}J}RLی2L/Aݗ݊Dv}NىVDMm`y_YLckϠ7h fQt,-Bt^ĕYƄ^jMوN]*_cP[]N`x!x8<蜞0ې'鏍 nuާ $|ӊ0O|lṋy܎nvNAȞ*-L0&<%2.OA륽< NNPz.!#y #<| _E,{ų!?=_"D ?~Ƌ0Ē --G\Ł&PNߊP/eRofXm$klodoǺtneABI9t\1ftJxBִ@0đ`b|ߋ P;0`/5B.CUwx8 05B9"7b|tP oLxs MP&>0Loz68rêƭoċ!,ׅ۫ś܃YW YVZ coU7᫅ #r(ŋ GB?O#V!IaHubd?s*,zc@ϟMbLuZ NEovϖDphG7sxs SӪ%&T;t"ʺφaǖ=g6,Wd!`wm.2g_7$X&ʞusmxprPވ'"G ?dcB<z ϣ O;]ַg^Dx4}l #O=QY~v?ط́%h 0(H蠅BZ}P_"6Jm "g}{!"n$!Hr Z}`5MyC !$x xmȉ!B8*C͇6:H@x@@Jݶ A?'eət 0'|!HP2R&5hYs0M޺CeEB]T: Sx݅EsU,n*Lz)FB^G5*#z zcV T){G3ʈ8JVO k*WUteX/+ _WJlfQ!bWXIG;D^Ѵ#Z6O|bZY4Ĉ>tNL$-12G5 bGWpBa.~!M*r Ost biqp"UŖFo) z7=m#hğ@b55 s"pM sp! _GKpI;bxR;Uc1t\cxe' #,URbk>hq Q\ÝE_6 Տ\՝~tG)vۿUNOR0)XT*iV^U~@TEX(j_5<".5~]Ku´_> ; wx@dWyTd@!d,  )%(6'5 ' 8 -= -/$241 '(0-***346W LtqC#H4 L4r#U*Q9%u&*HN*Ql-y(mU)o$#x#4W%LJsTePrgMO(fR8Xj$ls%Qa#:T"RcX&TaJOPjd] &*.1$8'4%9(qy!"T)jeSzgk2(*).2727.2s.t./}2)+3-2eqj)(|at83=B=ɛ9üBBɓm/2._sz;/ºǺôĘƱ=BB>??B=B@B =AA=@A!B?B B= @! @?s z@A DD8 `  @BX=| k4Cćh=U!^1\dzϟ@cp.]УHfmZ"~((4 to!64455@ քH#%Aj Pu*@AwKu@ H2ѣG߷*]̸qСEKܸh~ȅ5].8h\ל)4VY;d0/W_͛u)&}3!|[K^jlk.RBwKGa%IP!j@@ŗ_FI1~.qg?AQva)yp,gipb@=b6~;eK2]qۍ@@Prӆ9Fit#WhU,GJaL\)KߡHʎ 3]8T f!Hk])E6JL4o݂ b92GW9(5 gX"@M`(!X9w֊dg=|WDKč챧Qb,mꌂ^1lP5{4A K2Z^@;*0N IdY鴥dy@ |6HJ0;VhW|(>pWw(b0(ث0,!c M<<;=-DMu%;P;-ڱlP3Mu8Y/`+5q8wtݘ}6lݎե`6ng37u6Szw{΁n c׫M>;.p"Cn礇{~z颏뭯.{îz>N;멿^z'o'߼{Ǯ?_ƿ{H+^6k^觯>ÿ~o ? 3 6۸2ǸYprw.s=H¼Pm"` Wȶ-!d ®3̡p3q?ԡgVìCLb̊UPPLDFת8B,z1bZU2+kF[gca87MVdG᱋{ dH8P!svD&YdHFHZr2l/IdҐH(pA*}IJQ*yK4E-såR$0 L0@ xB@Lu HpQ SDGMR,J׸ 'XG Y )TsX_ LN S'8alc1 SC`pM I <?< fDznsF`f@Sh)T ep` (dP`H0ܔxM|@DTRZ8{t%J qQRX&GM(@ &_O PkX)BaQe@`%®ut)=)L.T O8#ҚMjWֺlg C VxB0jPW*XejM2# ͮvUkۖa[U`D|Kͯ7|S=@{ʒe,BUD0Zx7^ of9[ 6Q ꪧ2Q9 GAѭ&7 "ͯuu ǨNp@|H_4Q2e)LRaIݴBF!2:X'䕮OBq#@B@L*/]@ŪeMɖcr(Yn,m(Pj6l1QuoT@Vz[GX M ^͎X=uWYJ:ķn9jQ֪^r'{ `k< X67~ii[.4*0B +p`Z 0{FA8 ॷA-hG"wQu3| W({@ V qAP8 #[M<6vp[Ah?3 .^ xA0 .Be"סI1 01*@`%P n'}@ \lE8wkAМ49>n?'fZ3@oWVQs< =kz|D n؝/}CrҦ5E j[.@0| yFRjtzI0 t5W 00x`x~#sߗp"Xh G o&W/$qP*xE4p~ $o' '(V{vxxxoPqz2}-D/'nW v~\-'8:(w8y~\ P'A?ׂayqCȄ7 &gwT8 rv`Ab8l7 GlWsk(yW Z0pn+`+Go`qwFȆP\|pwxRj(xyP H p`(w |ٷ}4> !X0Z\PsH X$vq40\mXP )q-/`^[WpʸF|u``]x }P H1rPRdH TX T q@F)`vJɔh0P8C舓T%^E/.@zoFg$Y1@3pzgn9k3k9l9*Skn|)~Ȗb4KGpij9_i494T`;x z2Ez,@AW ekZ3Zz4FPwG'u |ўS0A(Q36ZH_ 8h~8 J U09 h! ;rkpjڬcͺ̊9kZ;+ $8zjn`pxq m7_ 0e>\H> ڭc4 :?S눥ڗKH1욃q[n( *q @}gpK:E;EB ˜%o9:I:Ys3ІfQנ=9C(#u٥A`00 ׏xH7AkiԘ D k ܺk+;`;`N Ԋx G@~Hs+luZm8W٥u[Zp[Q;-'YPҞ+P[:kуz98P[ `J P{]b ;: CIPF H<ڣPQT٫۽ {۾U+W+{/ 4P](F8, A,*I 18\۹! WA/0pB). فkl0=\RU=̾jxEܠ[C EZ K@ )p`p/ r,Sw,0ؠ/Au$sporbi2]7T+`<)ğ[ ĘVܛ_vlÉ 0Lɐ=LY˻L3˧KyI햿k8ڐpqcs'O 3%W|L%pιPLÖP΂ KK{`[ 2@``qҜB |p|p\B3L#:93S=S\4L0|L 3]!}6'=ʥO(pG'4uRgVRWv ':AZ m02 bwjvno wI`x 9 =5'30H,<-4Z zm B#m"r-̊H9J`y,@hz'/+,{Ypu7:y#{P=UٳvP|ǷwuGZ7C{`i-2ߜ3q8|K}p= ]v}ö=AiWz@Y{tCp̨ `Zw1€+&( ܣ)MҐt}$e3#`~Lۍ|L KMl}] gK]жn;G@l<H SJp !yw)yY 0+} βd%׶P4:-3mآj6/~)wwoLJ~xs;{?7V<;y8HP >܊pPsmDnC,MlNц38;rz Yp0?gYo8HHx[8Ȍx{јB{X> 9"Y)(.ﺌ'&xҶ.7Y0p] 0M[`hP¹IȻm3F;Fl Y-#Y+Un gZ`X:SMf~-Se>"!DO kEG^ cZ0KYoFٿeo ?cNl[Vɤ-2lH|O"m̍4f mma|̦e I3(\dƨ&˴^ *ߖ;=?I_o=߹l |-~S7^}McQ .?D~9,6y" h h$TxQ *Xt@Ɓ$1X:Ƞ' FfD6QA8}.T*(OzuNj ֦2@oҽ#gH81xm(pcD|`PeZ8`1 Zp^eZ:|͂=NW`[iX[ʋq"庼ks禆GVIGf.!]};aC`!ߝ?P'aU? < 3P2jE,Z@ig.4rŅu tn FԭKp0*s1TR17&C=(DctD"YT {cf)"u0yP`6xЇg1%[if#p$A~sya0d}P@ PE X)\|hpRzM!'$l8 iM(+p]p (*I.'ų*w>K}@d$h';qx%a7؎T]nsP@3Sj4+{%D=x xwr1>x Btx' Bᒇ'* s0A8)72ov{쨫N60z?gAyHՒhZ` rEqM[$Yf9휾1%fSRv`$k`zYnJ@'/Tt+hPo.)P&{(SD)MIL#ĵ4rk_WWV:E4Z0vuk `ȂJ 5쳐`.p7dwmLsk[z RɶEɆ (F60 HBd/%!FP zs5άhn} kx? x$.q "T̰V.e\)!X.DBkQoAbI#+LQV`T,ky\0yd2ՍzT{շ* u$ 7Gjmh$ঢnU* e6ҷ%T'Mb҂&J۞M;5#W’7G8lL+}Lo:K}T͍.[/׿>它}Y#Ajo۽u}t}| ~/+~o 6 ~c1|c 7` L b2 G`H le j݃a khMշ7g$8o0ԀoJO9Pn 8h%`hh A@yW}~G8@r } hr0r 8gi0#Gi gz_ }!8,x8rWzsЀ58nyPuKȄMOQhu|b0Xz6@pwс 8@nhHoGuhwy{(fVzp7bHxV}8PX؈(:s`x3w~Xz!H_@d7}@~mXm؈x+$Xzw z6b(PHoF҉b&ĘwEW!*'W(GZsg }88x wxw;"+i ;Be )Ii9lW'ɑ祑$ %(y*3IIQS `'&wv|pd F̨K( _00W]IQq :^^py%a I AM b @_&p}_ɘRl/yxQ 6H `7Oɗb mO)hWOye8| 94r·^ ,i}Icp(ИY,x^PVlv 8) _0Oi\hHvdf@ h@ (c#8'Gpc+ h0x!x)XI" \IAڑ+{6J&9qP:~Ǚ8 *x7!*( ,xI70H!+zfzm* vj f9o@ǧx&g Z54o:Pk1vq`"I·!:\ _}ʠG2p]wXةPηͨ"j٪gZ!_ 'J Qz.~Qr' 7Ūї>,تG(*j 'Xa:گX .(V ypjDԶHip*}Iy鰯)} ! :bȡ#; 6ᩳB8CipH?[}z' `|[+aal:XBFxfii;*IZ|hPW dbڌ8(gPbз Yd>:/x}KoP+Bʵ7 *5 P$ @hio88Ի{[ {!Rx˵Ћ[-g{ثݛxkw+uks諾s;r+qٽR˿#xvpv\ef| Kl !,#L%73(  X'̺&$ ¬« p3* E!,- {YQĪ# (`E1RN`|PIJ0 RŲŎNY=ԢL.}#؎:1)MRM>H-X lu ֍R* M" Z Q%Ո\ "0SP ~ ٜܐL @ȭ= Pij:^ Q`i ЭѤ=quhǝܸlֻ"=9M@] Pݝ l]ގ,ȝ-M x<qP--N`#X^r}ٳUlNᔜ!}2n5fh<&{CK!MF2n=>>XNt&~Lܕ8 (gn$@܇ےՔ0ۍܝa &Ln{[ 0ě:G^[əQż]l1܏pP >Nzy\tlێew}ߞ e۳čh|$  .nlIꖾ"[Q5Y q뺽Н{ ^-:. M⽺Pe֝DRV-=UT\ڽ]hT|U]h\hČư2!^M*WVaұ73/PQk-2QKL?B?]Ghk AaOWX@: /Ñp A,ᕟ[Ģ M wĠlh[{Jثmܻ;|Mοп[Oq ;Ex9Pop?39j, _wDD Az9?DAǾ˻З9ۯ♋A=熈DӤ:  C,)!!;x BKPSj7-ɓ(SDRIm-WԖC̛8sY:BSKѣHe $8`@ԤΘbUҗٴreK.X+.n5m=p8\Xz6LeR.w1ǐU 6rһ3k>5ͭrI©Fӧa  MUg9du[yhmgwtT G :(O0wlfzw1IM͝G@:vr%K.F<3auNw$8%S!F!W\ v '  U'M zB5n`, 3 ٠EBH:&5vrc;`Ǐ Ke@ y|9_^S1(#L=o23'1qFYK`nC ىlD[7RбѨ mS#aN$42!Z;zC%Ёy\i'Œ)YBbZ8NSUF>\`FJlW}k ^2l}K(%4$:@1y ?- z;*+YX0~V%Lu wLXgaP\q!|x\HƗl\Gh6T1[ |"%B0-EKc``ɇ SK\- T*u@Su&CX^׌=myͨ0KGB ' \ ;Q0#ySE$E%>ܩ.CPyO$Vi0O3q M28W;H!EVcmaZYqAvj!ZN!G ёN0g0v1)_* D%)|P+Kl!b0nj c颀^Sk"$X QTTc*s֤%5.͂Uv@Z\axX%?dYV"~͡]L ԞkD-X!MNvoq6\pAnE$ HTukXIh0ʊZ^:i :rg;+RϾ睼)y_Bx' ߈ W;XrwUQ=V,НHLۇtΒLU k%Hqq-;c bQ54o!P[|)5 FYX,em@}u=n mH$ggsbIz[hA @4,ej֙\]C,5AF(CۓRl*$]S18fb?Ok8A R\Hw1$eG֛p=("LOРu=іI X2睰/mF@2)ѝX;J6J}s1B3N7N~ћ\~aP$(㓸0\a 1;(o{ފHjU.)^(`{D7z-D25TQ)ї=e 2݊\FQcL"?+1^B,-nɸecZ{Aꇎ+x!IGZ5  0M=j$]辻|êR Z&OK2V{JU]SnY1!/`V|MHp6 {'kyB̴c9{6Z/~?hX%ҠO il;c5UƇY\DZ@ l T2 !K,  )$(6&4 ' 8 -= -.#1 '(0,***44541SrH4 G+ p#U*R;%l7HN+Ql-y(mU)o$#x#3W&MIsSfQshOM*eS:Yi#ls%Sb2K":U.9J(UcX1KV#WgKOPqZIfhf &+.9('9(qy!"T)jfSzhj2(*).2727.2s.u./}2)+3-2gsj)(}`s83=B=ɛ9üBBɓm2/_sz;/ºǺôĘƱtuuv:2uJ usu-t~yIw]nvsuunv4u.ysqz|I4;}:3y.9nyz99 I# `Q ?9Ï HT9t@=~:hIE;Jѣ, ӧPxag;|4'#HQ2Tol5`S+@T먮U J[Qƪ)׫$xE׌Vϗ"t:$mVn XH>IH9Vn$zG=9PpËOJS!ǹ9;hΞz_ƞLŐb)E|G\Ri(bɡUXIqފ,y-(c)6G:G1"h(([EIDǃJV`K!eWpG}W3i6Jx暠PAVQW=`GcoFR[WS X~:ρ}1D>)[]SJc])ݠlfzfKq֔>%]|Pvqm) ɶ * C"G%GsB[EPe(YbƗFJG/zE'@~S(.(vDquHhҤ\RQ HLz u[MgwBA 2rԌ,5Ѐr,-4/ @7754{gyЙ"P`JL157-5X7XYw`!1ahvl-hύ|= | 7}G56e'vH>9g4FnyޚWyڡnz6[-蠟> g.-4f>L{蜓B8^ p|+;|+?}3_O=c}oOOo׫߃o_?Ko=_x$@A L̠7H rG(< SB0L cAڐGw:dw".!sJLb'Z.PgEa[ ߺ/zQh g2_[c"C8jr4x>2-Hȟnb!C^Nwd$ȵ%2̤*IKQ<'[GH-e1P~zn( * ` `'hB>MF6 Nʹ # R D%JQt&`0{Jo` Ѐ@T3-ZHP p3|k`DAծ=[ &DWэ|Ka[4_s)BQY N;'LѤ `VrMDJؼ@fQP0l]T x{^ B)AXc 5)iެb)o6pލ1c hf](H/!2^Awڿ8oc5 *x[V2dܥŬ_ u,"U"3e|:N̘?Yf2_ v/S-z6g2٭q<#iz/B+\!kt {Ɔ7mm ,h +JB" ¥uLx -Uhܾ YZh2'oZsA`)>V+x1 k M9X/S X0[0x@W TP^ ` WѭtEWO 8!pA2up x9h$A20NrH܇4w7m|ރ9)B=H n3!),* XX%k#'@ ` |3 E K] Oe҆9@d!P`sL9P ( -IiyƧ񋏼!,P} 8/T xAdaV'V~W W@7#F}7Aסz`Di߆#G~ yo`fnWqpPp(0h| PyW&6{vxd}r$ֲnm*q8xk0Xu6@H 6h A؆B7{?s>  t ~q|`,ͱGxnwg~т4 ixu{m 8()p {7BAggp {[PHu(@|`h(%! s 3(b8~x)Ȃgi0([[48 /X h3VX 8py( ؋YPEx?v[B8o(|r*X bXD'ȉd&G0ZP ? @[`)6q4&253Eym>ypM9--`1uX ylb@Hgr] jlіlFlRse7u yfk~)v7;dH(nfn-W `gВny)CNiz oUoM7`zKЙ! *[(되(r) / aHI|y k%ٝީٝ =IGQ$ T (X )1pov@u vyvWPXGW։yivV"Y y>7 202+C> ioJȒ8@wu~s0X/7~)X V@`Z|rxi #* ' 3S4ʔY7*K9J0P@J /2`/`p L TYP'~a: zR i m= 糪3F:v =tCzڧ: iJJWN;hnX1yB@!D!@@| ͪ|kڗ~S4-<گxPA)*3Я ê&;:S6:*<*J8pH VB׭*08t)  H|&;jnj =;\ rگ ;# ڳJ kckk Z 0@[,B~Gu 8@7VNjxxԠ.VvP3jK7ʤ:Ф۫oLNڤLjS/(#[؋XpBuhYFwZhn쫋:; +Tr$Y 1p?tHۿ^My~⣮h^@HM ,#$rb9zimP8B: `P`i*ɢ]F:2' haJ Л@@nF,HþdL)VzΠ덹[g|K%(( }(ZYd СV8DCЇ<6)(lj;N BƔdmn$p H-Pm @A[o0CNˍW53N;\4vT-}BpC5fBX" 850r'B1)6m7n=TH]rx #x"b`Ma20laE&זMB2Wh!HʦY\ AxŴX3 lELG}Vz CH a+>g6}g~pY+C흤H+C."ruȃ&abTb @B\/"AʠBR_?$02 ӌ VȁP&8 k1ńkĪ:9,z!J/^=H'vKSbܵ rM?Jc*iFcX7E@h :E]#[(d!Ɩ( dV 1zMV" \`$Md)7ɷ Jl+_ KzhG4 ,XA.c@bhœb'Hc*30 hJs,#(J\ Epvs,9sSE; X8{!k `FS> EHf;WЄr -! 7ِh4 (\_^cKJZ32)D@L*5=-* Z\A !# l[xb"Jv 5rGhrtd]ˊkgPAA0"y<1$#XHA t l 1:z"; P.`mriL}SϖFl (:| AkrPYF(gy@l3,7M줔hC:kՊm_,ю>#|;L&BDWA]'49;ݝB x.+MDrqX\zJ6p-jZ"XNTwox< dW D.gD[ %qU%}09GPvDɴbMh@j4rFb7ȣ( P-Vw8I,6{*2֡@ N?^uxf,R:ڳ6 ً"!(@Kc4 85 VBKmNzH@霟>Fh+liS({~Qa^}l;ܞ-{DzV;6 v#|>8 K|8|nW.x?C2$/O^gp^ G"X1PMn_{ڗ}3q p}7xUrHhhr*ywv bN7v~Wqpw}Ѐ3<9;ȃ=6kH)G Ϙt@[4*niN'YІ`J|lJ:c'J3"9t K:`kGqϪ8\?2 tP zHZ y{{Z,ڮnw! J 9GzI{`)vk oʱbm{p?2P`A^e/)hPgjj b@li8]cp׈诣MfWJJ;|Z{|G3k[b7B ΐ Wg0@> yk ~`@R򎛸 H;| o_x۱Sw KuzPne'˺aȹ MKB7w˻ ZkNjɫ駃>+K.ի˽ +Kk狾髾QJX`"ȾU! `=K .L/P0 'P# P< |< %P&$' lkQU.  & N0 % 7UA! Q\ V ZF H V*P0żpM1R 0M'p^ PN@&',,(mN ďƯٗ&N+r#P[`#',LtlxT O@N lmL M@ʉ&@.ŋ{0OPȏ1L Oɼ| ٜɲ@یN ˥'@w<zН"+cN2]`c-\mW p KQ "M盬]>acgў@ښx]w.,-Ͻ4*,^m,ȡnm_>Ę5\ Hڰ ˝I,lx]P钮+(V ľMyMߏ M뼐C O@}IC^Q֟Px}rm;\ 6©%(U2$ O3l{%,Kly}m=d+/_*;<z [O\Tm b lYl\]{6XϿQBka^u/u/Sto vS7|\gHOtOτ߼O޺ ?\u;m 9@[Bw Ip'op0@w+>C?? /=?pJq9 Ywgp0lBֿڏܟH~;ZŸʠg! L_Nw> ?zƥ9Ѕàٯoӱo)!Խ[0r-KȰCf E51؀`p8sC bE[z4\$˗0Ce̘6onf?<尃D:Hi -(R.7BÇ9͜`(tM߿βsLpQiM96#K-E8aN`4қrf`G\Ö kCoORp=qҚTJ@ yw@3(1G@WcK0B g߳:SDt8h*K|ߡ&/}BC\̅)Ζ.a7>YRc- 4'G.bCaN2NZW=-)tۂN~.C"QbhB3ZҺ[ë]M '? :'{ԗ1* p^L rǞGܾ/4?xmt%pdiS&Т?7 & P@PsP &x;ԡK p4&/p0l#NL4p0iX( {`iI|x7?t;")U+7XЅ;uAvP nIkzhz69eqh2ފQGU cv#$Hd K7]|{&G W)2ddF4224H?ȁHt%,!CN(`qU*W9򖺄(2$@K`%un{gR׹fTODG[Դw@r5tn$vД!@,CTyH ~tSٳ:aU\AGR8mTkX*эK\|AdB7jHֲ4oeL"}R+n5R7@M 9t῞-etUozNT]5 Bu|g%ӎ`lU\:̷a kڊՙkʊ2UijDEcTd +EԕweYNMrgR9Y;m;.x ®%pIem97毶2l0g:22am_RtɮJkpyfsUuqoBb4~\7:0i`E Z={zM3A&;l%СE :UKfn#"ai~?b*->O8qqr> =w es2 3 P 4S-WO06r6T x'ͤg^D&GcVŪG^beE`R̊OÔM0Vt-!9x8&(P7!fW$Ѣqi6ӹ$6Ka zN 5Q a>㴥x%6+Di R.sB'm&[Cvg'Q |\)Zʊs/ 5`W"H7I`c%|'O'LhaHbqMh JR[Mܚ*ye+8K(pzkrKzBmB catlzj[3ALT7V!$ 3柦B^;DW5.`TDOJ)I/at@*++\NfL,Dfߋ5A^YNC z0[$;iʸ(@#/U_x,VĮD !,v$(8&2  ' 8 -= .-$: &*' '($(1.**)544RrC#H4 K2p#U*S<#E5-l7K*R+tV(n##x#iMImRshQK,fS9Yi#ls%Sb1J":U+5I(UcX)PaKNOsZIdfd &+9(+9(qy!%_dRoTyfj2(*).2727.2s.u.Do2)32-+rij)j|Th'{5=|=ɛ9üBBA1/z_t;0ûƹ—öķǯƱ ē(MKɐ P֭OҘ#ONK MӒ׸ ##N%&%N)#)N N$d #_- XE8X%ZXRBʌʜy4'(Gx8$,Ԅi#Vd(0|UiE Œ:;T ( cZ\O, 0]]eݥ:)%Z5u؏ %!8lH:D;t,xfmj5,.„=!@A X!.(Zk"Q4,x/ G,)o@l[q!gǝt B ʔ! L̈*8?9r i %Dǂȅ1. s pP=G X { A_qfq u~-"NS@qhݶ!S }?݂tE=IVN8G3pqzh  `EA; :P aȱE^!y4QQ_co w[8$ G;\^B΃ 嬋`ҭC!|*[!-$T}k~^; Ѐ@ w* "wBu}VCOWBrཙ"Gjbi}tp]E%$z W.ۙ @Eʻ+[*[ ? {X-/ÆڇW W&Npb| ,q(B 8i>>09c<2gd=/ޑUJ(<(O˒H2d. Ou4ϧn0Y?ss8&XJF;ѐA[17= Ke4G Ogȗ2W S՟V5g Xh>4w W̿5݉>,נIfʱualg[1-k{َohоА-sݶ~w잷{;&ww|G8 t{Ӯ7 ~p ' +ds7Õ;8=Nn*o0Y/Vm_;5y{7M %'C M^JRui>]3WQuC,gW_N \؊ ӳ/Y&'z#BhJ\%Rpg  F'ْOVR`G#Xo@R:y^w|X0(mAPhߨ|8/ 1< V@Ѥ ? 2Fp'2띜vþFyzP`yzBPD)L (!G';ZNwz}B^'72Wd7<'!)8H"ք( *n$ eR}]G!} ɐI0 rT9s3]rvWA#p(HJ؄Nh'L1zЃRu%!{b,e2(( 0y hj'rg@('1EB{!6oA),`P!(-m8ru8uSw. 8`XdFćK ,.!]W1,  /21xs9'x0SPwM[8AQ0/S=8ʨB/9ƍPfhuh_踎2Xs?sh IHD8j_SV7 9tr/PCPUE:}gM )eH7XQVpUЇ`- 7y aA$VNr)r)Ip0p)AC*@*CV`+, Vyi)ةeIW7Q0Yp@m@3OT(uS3~TTKiɩ4 ١G ҩ*xU-'*'#ژ.*W; pɣ@ U.Zs83sxpHĖgj.W@RpϩIs*8 9J>j@XA*$ Yw@3L7`ttbPW Q\^ `}bکzy)?*Zujڞ79d,:0S0c>p]ME!B!%;DQ2 z:: PE0e*:j)sUPRpHj$) `Xj*Z*5w7$8Rjyy2 ʡa *d*W!ZjXTyST0ʪVW0PC`yXPW:Pd^TC: +sq1Q 3  ]i` s)\ U_G%ٖȔHY0ۥKŐ`0p36Ny et9i 9h h*kPseȋv0P80pI3zq{ v͋ϻоkzJ0wtPl 3+)+[ {Kk :VWjj< l H; lϛjP6 p6[4C<8 n[k+p -RI/ V9WNRiEYU`Ƃ) d/;PBзÅоq@>ܻ&`2& ` |SĚę ;(Xpk*m1[๭\ Y }00. lB+2'ں~W tQa% \lIL Yжv& CZ PC*T&Zb<0:<7 <#,b7LZRDb P6Lgq̕VK)yܺrdX EӡO{KP@&sG\mLlRT,8El+r,?FU `Yb6}RX6"V3N8WTQEPCcU5Y֎<Ch ~"y1!^$7a%ElYk0@ј(gh,规hF" -Jg)`nD{u,%56}'48 үWUaB_ &5-rlVنrm&(P%+[#"_1AC,ޑ< 4';2)-G+L0\_MЕu^-uL+qe# 5g6۝Ȝ_3ztIx xAm/`wO>C_,+@)c^$דE7W^޼E!DQPZC bO7L9RˈH%r<6,# HAA*@q́D'HzW1sZ$ *#xpRWp(e/XREl Y!#:pZ";PLa[Qjg?QG V% ^`NH9;НxPp~9i9Zrea "Y>(I{[ ֬[i`r5QGGk #:݄&ĉ&5M!_~aKhMQVwY 5(2rkE;s *Îvzl 0g'` _8җ^[yҜR x HT!%}&!K} Y4AQu!;v",G(N{`.P h:?md}Ѓd+d{@ {0rvpdW!vǍ3_8l`&*C2?m% 4-=5,t%$FK`Epo@[m%0U/ݙx#"@ԝLaaԍDQU:U T@\)AJ}*T$p* @U̳멋mk T7@ 9MeNf{~;Oz_W:u]>͹׬Mw=a49ADbҟ)A}a?>~pca 7Pr3`:+%?F<q8Way;5G: tg~0 QZtgG)mFgE/M0H-3 Ts8o? 35@H=|'сDh{JXHSw 0:}{QNH.cx",p01`XRr`T(dsqH_H(yr}Yz$~Cj@]ox;xȇc iA NV C#|UV5(ٰOzs}dqTV*B V"f& rS]OSUGrhdˆO3yr$ *)U&4%酽xMO Q)SIUiWYR9tv, ddmuF EV*YG,äM;I 9aUzAA[J^ T:(eZqhʥWʦJq*sJujVy+w{ʧ}nCWD* *:Ujʩ꩟ *Jjʪꪯ *PJ_i3_"_@_`ʬ Qgc@Ph#n0j_@JQj 70 ڬ:``50P673 _ n`n0 j06e0\`e`pd zh 7p: n0 ;#jp D [f 0dڲkpip i:ۭаBޚ~Jukwyzzy@Okjg\@Yz)7jh۳8K @iKk9k0J +W[Z  2 ;{9k&:pucH6T eвa4c j[@h0*qțP .ke)˶PջP`k[ <D ڥ%Tf_zk\ `T;Qu!L%<6p8R|èf= A,C,1|3lfDMOI܈+7L[]LR:&0Œ@ƛ g\jR[XQ^Luů#?>@z*I-w}z`db8]0: }K| Ֆ,ӌZh0O.+V²c W 0)+T [֊lRkӨ} `@۲+ ɷxMP<)òq9=tp@/fȗf`)kӚ2M \@eɫ];ePR2T cm ۿep]%۹,{ڴ\@ޱaS xo- -|W}Ҙ؝?ܬa0 ؝ؑ2n m Ӆf,9.D~4@k˒C @"[= c  sMf۲`U- 綵05 M ,\ꭽ?V/(bQ:7i~uPpbԪ>:fz^%`ߖ|%N dP*a`*ζMng0͑NJg6ڰn-ӯ.<î 5}b\ R nbrN3 R  ə orpZƐHC<%.s|%o)wJ$/'3Ohл9=@X*ZCGϫ֌kUsryՀ Iʃ<I 0B52# UPG-56"k/R6b= \}LB< ɅJu%ٞ4Q,8=M!ttKt6UOSe 2^NxG3`}D\AڸO4q&*K8!=!~vl"C6~_#S}co7I\ayMnwY =T01<* ؀{hՃ~@pW$ XAjh)q<0'l W)__H05̡w= nVBL"ˉO;"y(x.Ί]62Qdݸ3ṭsG4g x5OL$5$I#I6&8%3`$$BOVVZOiI򇒬b@K @bh] B/I 2( ULdfQ! &-kfsA&nRdd!IVBxA ۩Ds=!a]ꓗ'54̓> "P*xC#J),N +~RmT?lLPđ3;Ɯ4*Տ{@DQPMbPjҼ Xh02u X8UV5/TvJ`,JW.pquj^D\5| kzViJsB5 z @␀.vK]rK ^CrZP+ }]  ܽY>@~l5JZpeww` c#Hb^؂$ֈ7T PőZ @? `vCB'fr^T)qye>35t7 [6 ae iZܰ'iaY05-<_7{!}.>tsHDq\Ɓp 5usceEc@F/JQMt>PvE1j 75k .p$bC v8EffQhvs*WiDw aFq2l(Β=]Edb! $xjHsk,.*(gGպ(Vҡnxi]PQI}wMNmcĻi {]uכ2/ [$؇u3>O׋{|0]h@$(L?Sa"Fqԃ:|JNsug$r֕ls"^1O|>fI|?&"IOO\'qFyz'WB~YV׀r'uBGw瀊w}vAs xsp}@,VZRXƁ:5XrE|2e|L P3?؃w}e< Gtw?e5Vw'UyZ1u[>CW jQUi@i8Zmj&Iٕ U% u0k ]pHw]e xu[fEVP8hFe_ b__X `Gz5zSCPx b*BV SvcqcS3Ќ'y`{KH(dW~{g aƌf[xp PH f t '}}|t5FjfgUvӠpx|/Rvk0iEii i&PNG~c5~u{ 5&vw@ h7C&.` n,ǎ IPlul6D—kDmG~0l{ ` XF B] FpwGky~uqI"|ny0poٖE@p5"rr+ hvui~Ŧ'A@$`s@ItusPm5}Hpt3T\&TXW9U\YNbSkӐ)XPdb%swv`G@ %7y~ B%`XדOD t0wafSs\g sz{Xig)Id߈dب~](9c9u}HtfNY}Rcw&\͇N 0Uv)S"<Z5,c*y (ԸR)S mT95V9mҦ vcJZC@MYpLU%Ŝ7K:AZ`*GIIW\:|<ڛ4v 䅡*S*RCith ٪w)GvR šS j}[ZzpkZ ٬wjvȭ`P[[cTU:.]pJa) >EP: 0P\؈_EHkx jY]`@Hz`hz ƱP +j2Xpb VF k`px#@֡tQŪڴwҩP@ke:˳>(oFR+^xfh_FfZۈ:gpF0Zf9'g: mPizcf*qhD p:}g/Sj9y SaF9;hvKH~גPvim[}Kf8+HY *˕Kk Hƻ3Wˤ׬eʼFxw{`8^LTP^G_x@o[iI*e+ʯujH';e՗liDU ÙhYl2P#jY(rوK-EP &|N :$[sJ@ +s.98KթܰJ.K)KW7I 0dC,s VIvQn ِd|a U3Pbi@52;-L*ẵ4|pk[܅,YvMǍLGmvfnm.x¸z[ ݻ~lƲ Ҭ_( ͚ط0嘵V)C_s>H&6u]!-f}KҾZ{[=ȤNU\U_ՖZ@bFiˌ;7YxMS; 3Z0}wkR+z f| ،=ّٕ}ِى=ڒ]ڛؔ}٫yڬ]۪آ+ؙڻMژ ؞ܹ٩}ٖ]ܬ=˭ mڶM̝ݪݨ mMWD^י]c Pr` MkmޮM۶- ֍-=.z(}Ƌk|&qmK9nPU.0./u6N85>5:?>I~JnA>CKL^<+4D;9N>b1>^RNkHN`.ۛ_nq~H_K[ r&} ,.jn^nc~L^rN>AaMӀ)\p b-H'l]gsLg{ nq9ʶ.+x, ĎpƞO7,:%xvy޹˫Y^خ~ Ӫ`p\|0p@4`L] ~ ]P2 ]qN{x  4nu Jw0v  z(yk ߈sn[ _5Պ1k!k_?LsA^ c щ ΅ hZр v`_pe8 Z k` pcZ p@pT[\l ~ѵzP __xy_| bOf噍ϳ`yv+ĨL X- k1 Vk݀7_ =k "ռ0w0 8Pa(&O`Ԁ0HXh`gp s3G8sgrȶuxsw(:9gXj1'kXz*1 WKj ІTUw:X(J: ,<`[>^}>9Y[ ~8-FeBGΜt GX6{h-/ߴtÏfA3YQq-L1&Lj4!s. .Cp@RhF…mKWӕT9ləNž\4IJPCû=Payê \nLG) 1XO2:kM:ni'A`AO(dyNA4ud Ƅ ;nQ&X>4I =sBú[ M c<6;~ G IZ~l0V$@N!kj!Z?1B`imAGSGA^&)pH:2^ up(!^r`HQ ,80%8 (I"nᏋhHVE"IH0<x|%R 3Kҁ[!nsa}/a }ebQiȚmv@|S+ހ@9LBI!ޣB ',{JZi!!xBa#9+!1GͨGEB'G]0 (("W\M[m㺅Lrm`bTSZ P{Akd\GD??B=@B@>B =AA=@A!B?? !B@?=> @sz@A DDx0PQ!aHχA  H"*H0"d0޽yǍ JSmq[wӧxxiR#yLPAF삆, kawP_SbZU>kIP#K\4:3kr5TflڄTXecj#/W'u^$=]ν{9LH_ "9+"A`YoӫMSVX~UJ`0z=(`?\CF݆eRu(∥oE2\. u>F( Ÿmfۀ. #[a.JQ?DT~Ueu\4H'At,!GJ$׋U6-[4dp*r`$]]8@[yշUj"r_$ɒ8@5Hקp~',I.U#)>dlg [҅^% QW`!AijDJ5mHgȤvm>0+zVM[/Ю 66X  TA٭JA}^*4F=gmb9 pjkqYюHr:ЍtKZr[R4PgEzTxֳ%\OZv5j)KVsm[z._3 ٥{L,Q^QȞjkVr{ahyBE(8 RUm.`a UtY'Hἧgc,D-~qjZrx,Q40˓y+kؒ,*E6/pV`Db1sHRV +2z%.E G?o& m< XQ'!+aZG P d VplC- G_aL @S^@P!؂` .@dس kdȎ@1a,(j)S6){ MP/\+` 6 03 h-dxpc2Pč@G3CYāqCjA楀@@7A:AVÚJ x .7[ Y "A 8[ )2$hmlaZ X+S,lS39A:$!q,;~@ $B ~<^m-E}r֦A~l{ A3TpA.-0xʭ}7 >Y,$g:(!}0 `ނp>_h:B>qg  xp{mYpj Y40| WnSP P|lqgX{ͧu$pwqn@X0pmrP{vt0txPt_0 gzzopWZw ·|Lǂp#WnV(v}pgqWZ}o@,`qdƅx|}nBt}{P{0Xv5WrDRgOG7&J6Zg0n$ Zp|`q+Y`{çL H}ZoLwxMv@}/(iIpvn=xzYE3uMY\Nk6F`{Q}V8X hx#̇|\0ZP6騍qȋyP~~˜~p~0{Ȍwжh!X7Vp(H}|6'&  k-I"&]@lϘk0,@xA.Grh7B!Di7Cw' @3YI%G{z4iF lЖnn90X(y)U 8: m)6c 9Зsi8 qL@p9Q I9w|+)Xp|FOefMə )~␘Ӹ)=i.H S`(Dinp9nnڹg⹞ٝ f9hTGEۨ  G 30 ,q@Wq 0T : &)' 0 =8Cr9(Z<=ѣQi@56` x$8ld]^ 4AJHʔFw_yFxvpY$;&fz0;2jYiC4E6nٟ~|IwvZn,p߇JH,nуکFzʪlЦ338ʘCSyJ \8u$SJJ ꂋ*ѩ # kHk$@*U>Stٺ9ej@#>5@* 4hvAȉm0\Я\i{؅ g:p6p, f8gWcz ڮJۦ5I/*+ Kqjx:Y4KZHu7K [ w}AsvȬ|WlA 00 Pgf`il9++ʩa욦nɮnP]*еj˩g+XY[[aж|3oJzx[ @˹ VpXnp ZZk{[pxMh熂V;# mI8  E( k/|3kZB;kzk̙Iv2i^Pf}7Y,bI4Ksۦ2ZIQ{ itǔzl0H8Kmj 0a@k,i5uٻ4ɕp1j+r_$|(92jc7 0PI p{!D{GĆ6O02{c|jG+,K}kiETK0YklGmLq<5K;GSL`neȍLFSȒ,YGhؚ;&UWMz4KDI%ː\4KPIP˓|Eʶ SS0Lȴp/$;|+Itpu7T##`nj4KPV| `ȫڻ,ν,IcĬ6ڻ\ϧoL,i xLq pnIq./PpI4`F'`/*;qWrs3WsV90ysE1tSoP 6Kͼ|Ȳ\B Qm\NɃW` HjLȔWwpV$Hx@x_j#(}/0[xZ"פK3Хow}rvy'tu7XAyI oʆqX=LP%@I5C٥}4T9j4M͟4P v}6x}@S|LiF 8~Zypi}:~Q~G ~}|٤@Ӿ@iK%}4#ƌTC̞Smήm#I HˆXTJ vj #:צLWr[n: |+8+r&yPh> +>5M={߉ " վ|2<=GS\ 0c=͊THv'Fᧀ۰Pl(ll uxx(X{k~ -;66N9A޻E|VH ~m8ڠcA:kR@@y \гwH00@N˳ wh˸@ W5╌L\m貼hͷX<DԤ`͎O4md"o睄 !9#HȻ`@. P-zI@)ԑ Y.MLOV_zkygйM9H\G]1,QiȿB*fyו G,|o?;mÓD;c`7io/[Hw쬠fjBa?C4]+גQ) 00ͿcM$/ ?@t{n~/~0ϟүliw0q0vހP> @u 0@(pBHxű @i(xI89JZjzWj*$#;c{Ip(++; ,tћLP!fG!vP=]Mwu-MC7 MNSWMzH?j(ɑ)AlĩX:P` ň"W\PTrF>Zq 0x-ij" xdX1SN5E SԺeKVr[ ֒ԪdNE6mT>QꭾE9)gJ#]"KJ%êF)~ yBC b1LܜE hl&:IZ +[:sYn[-.X`:cQRkaٲԫx?=mѦW}*_Xx>GE裄}uJP` b6  aԑdSuE í0&t&J) Z82s\1E%B pX11H$rE)Bb5."PI^2މ >|cE-cR _b%(^~ LpyAhf'pmT {Gfo%6h ~\u~8{䁗)8‘J8q(-%$lta4X$D4EЯfLJ#rC@j py%IU;Yu$` Hu&.ajLqi [ 5aJW|(t|K6/H|W w$ d1'rt[Ĥ"8mfBE 2I!4!:WóL,$@rJ`"6]X5z6x ]4@Q 8=&0$-IOz̀Di*z+(mSBm[־#/o(HamUWȀacdpG@n Ѐ6`px #H 5lP @a $n1m "hpc0cH@`50($s @ 8nBn08 ԃhjpic`7 i'h0ЄG8 `mT "`r0r 8 ne8gHGj``gy({8rs (5rbȅ_狿(H|xxg8dЌhehGm8(荶Xr懎騎Ȏmd_1(A`ipW8Pb8xh(80 ?( 6 >HZeHyj!h`6)8ЕaYVi Bx-6Dtuy{ɗ{ɖcI}IiYYWnɀ阏 }tŘdrI P pLəiU yКsDX$u" cYb `@$x p p!_Csg7@ﱛj%ț 9@i MDW0c [ ~A 6Hٜ>0Θ> ``hp=BH) wȄ"id'I8ȢPΈx> j0A"i)VPq7QvII9Ah `hjȞfVgPjj @k^=`=s8Gpg i=`@zꨝu(~ 7רx;9IJ)Vp@(xq(u(QbudXH\*CHmUʛЄ؉%:s (z﹏ hpw(`k خIW}7{7522[$Gآ `{ Y;:Q{ ˲ pb;h_vjʠ 9+UD 3y@tisGj Zy`G{H(zl"ЉZ 0[$蹅(ʬj*Kj;eP IBszUD(AH䯶jj{@KHCۆKDFɌCY.ʌ>i=9)hj&;@Ʉ +h [cZdӛlB8r7 aNJNԁ;/gZc 0/@Ÿ!_A l  | җM }݄(,^אޔ/ݯ. '@m ?-4ߡl| | # <ND d^W =]̌㖀Q?шnRmv允ƫj|; |810-\Ϝ>mޅMM۸#.̝^ ­C*@^\Ip #]L PSp.NP ` pm|?~~|ɾ쟠0L~6^mom(@B" "ܟ,?/Q0ׂ^=RM $ZO>NfQ}WQ?-$u?>Y-9eΦ.$ޚPp|쎿u7,Ӱ/Q_f ]#}mݛ2զn4?lM.l aݺ Fo"nuч(~4"ۯcOQ?ٺ.N/sʳ}a!ɑ z  ŧډzqݦ(R8‡HEG e$ŏrI4ɲx`(A̬3gOB:q!C "M:/>%S@`ԭL34l0!OKD.]OXi9( uqcVBV0r3y  ><u!FF7~δQLe˛ߓ(:u 9&'- t =D Ϸc`@~.4G)`6Nha@!ޅldx@GPNJ+pP(*2+qGxt#"w#C:qx$ )ݒ`BёO*9TpAyy0\\aI}~Faǐ~gOBYuOV[ OFhbh(,t Mjyj {Y5G=e[W`Q#lUfU,H: ClyTC&bXV[ou%W5Ů|w*lm:tZla!߮"R0(fYeG#kzo:BNr.9,pmX"**$FHb|Zekm=f7l+>3 3 qpJ~ MxƍFֆlv ta7oj~w!6QGۖ|&T~9}ԑ xPA %^័k0,lW7d [d1XǼGHU~I멏X/y߫ωvNv13v) $=.5!DL*.~רYs2ZATLȒdkX'46;.:p Zyd Pq;s2cPfB10 qMҦEE$ZkX-Nsh47b{X\9:ώ#;X@<ÏdD i"C1(s& HӤطPÔt$(?O2\%ZX&D;d3"rx"\F}8!|9#`I)CD/,iK2;b0GID:@sqz$-\, S4E*~p&A ?Y@9MUO}O"\o! WteL8ʅ%#thV< qZbKaKUq%_(:딈ˇ~Z| hS!խJ11RYê" D(tWved%W*Tb^ԏt 4jȒ4EOZCڋ ZdAWj0ni3LG5#бK^c̠\E=[*dWqgpVg`xp&f֐q.Usj$BcYAeF5)@V*b0y(H`M-9~4˙eZ<4OwlHרҖUZ Ҧ:Fb} ^C8 q u# $3WrCD-i1[ U"\V}-Qz0zP^n3[682%ECDu]7s+~@^Es ,bP0n0l2wuCVX8l0^dpr!K,  )$(6&4 ' 8 -= //#1 '(0-***44541SqC#H4 L5p#U*R;&l7HN+Pl-y(mV(o$#x#4W&MJlRsgQN*eS:[h"mt%Vb2K":U.:K(UcX1KV"XfKOPjc\ &+.9('9(qz!"T)jdRpXzhj2(*).2727.2s.u./}2(3-2)fsj)(}_r83=B=ɛ9üBBɓm2/_sz;/ºǺôĘƱtuus.2uI vt~yIwInuvuu-s:utv4u~wq zzH`}G3~.9nyzI9 Ѽ$F>H Wt(Zm .y=a'^5jjgU& 1Ah_GE(yTHTgj-qLD gs%+ok(Yh ՚]pc.ؘ|Š](, d 6YdY< fO=KǂtSF!_jfgKΥK5/ pYPmŢ; ?gP 6b͔bnA@-8RkUeDH{)nRn<`Ż![à_`0&`\P-7cؚWmTLoJJܰD[X8@WLA6p,h@' 6p,lA1Q x\*8(@@ 4OekLr#yV+$gsȅä;MLRn4{( `Ax7.{XH7SO@E G~M$U pɡύyZ`Pr(w$}(lh>W?gX4 xmБo+"}v}DyyZY0W@u gQ~z~z *qwPzn-!l {05 x'1C'pn‣zw 7z&(-p'oPWY( Tu͖xQ|h pvoP3+Q(t t.tMXp7 VxZ ZkȈ02f8Y@k2f$/g }hExyh"slY [KqQ,^8 "2&+>LH}h}1){tM⨋E145Jy(j imSjA3 HbؐɐF߈3"Y /kkg1 Y*I: h86,Tk&x@.FM3619됓) ˈ?I.3lpe sS U9wy)=q2WY9`i' f 0ƈvw(w&h3s+1pL7q@rC ,P+r9rE2ɑsH>k02М̩_>u9yɓ蕏)[1Wz*`YvWi' Vwu',Vw}O(`vq}C`wSxuaِԩ+ ԓJ=:c?q 6ٓdn1@[~( Qȗx)^G~{7m*Po4lzw{8~k{rz1 >p zI=q[Bb *<6!ɢ@wʝXiv:NhLhzRXqJG{0xX{Ѷ @g8QS z3" TT :}Cz?A6dn+ q)i)lHmwxp0{" oʭlRJcimٮO;qY_г\@۳D[j!DF+ <={9W+6HS{X@GV" #K{ض(X@9jഌ<  Z+2P:[ 3nz)[[DM$r' nt`Y0Ӹ9cWzBN{{3ǻZLD9D+z"YP8v$kS8о꫹_[8 1a[}zyr`'wQ軸q|,1QpXJ׻֖3q_3:)ɗQD<|y$::¤qP;&̗>p5 %ʿ+Q,:)S ^`k& z%, k P l89KyܕT6K| K8}ǀMlv[Po{ 8ߠojK 9` nw6 #pV6KKܘ [ʬ7#0 P˲ʬl~.xm̆.yɫ|p| !8k#!lj}<yY׌T ;\Ȼ,\Lp6ܜ~nk; *yS`0Pfmv2wj!ٙЃply໣,7qkPsvps9g:P?tm (j#l@]K7mϭӱLӨ\8ˊ|~EͰhf&lGugvv/\wv{ gwvǀwkxu,W~ \m`-@u0rxq#ְҡpF83ө\<˃<7 kN#`k3=m +6$.1)ε!l7>㐙nnX{j qkdy-Z n.҂@j]qh(;,C('/=4w5'.@2^} @A?PnB?tp yPf6Gv@yHTJweY!WQ7IJSA[j:z z K驗S 0shs5 rur‚3 E;{`ն^mMHLI1wsU9tMeTZw]L@`%$bzݓN"r~qS Q2w܅{Ǟ|yrf )qM+9`"0_8l "tFW j; 8N1DiZb*J41w@fBМfR@+D؞⮼pG 㽁D%R2~VJ| ]T0VpGU@J,ızHXij#H/Ḋ\H 20","¯Q:P QDd @By\^DWeJ 5vP\kJnBSG@jv T#Uw"rT)?azI-Z`=Y\ "]1bU9Ԃ=vxnwn /&r-"v 5j)f$i0d439tGERΤ1"[')PAe|’E`@C 2)Bzibm(rdm-IOҔe!0Iywi&SMoS1l!%qҠ uD->;Fm1:uTU ӫjU"EV ְmMX:~lm[շ'j]\׼~)z v)a[׺v*_;ĚlSkY&gφ5,iOԪvmk_ ִkhkպR`o w-pa*w.nzZUҭu^vwUb-y•mwހR7-T["4t_.}j`7 +|]Bxx0 k0P2k"&kOV+>k0x4lx<!)7m$+9Ev" (KyTm,ky\ 0yd.ό4yln 8_X951`Bf" yъRBk#Bf"f6|!ϛ:h6Ϟ F{ѐ&4l1t54mMi.A&!ha8 ذ^qx\4`g `A8!`\z  uB2X& 6["D  /cXF[\5un ޗ֍LW$/O򔫼Ah!փfx1|@ oE!!"u?LoӟK v1bEg5p}[38`'nh冃,fp_H ;o3!Wl;7vf˾+zk`=wo5038ϰp! ;""zO@H02N6{/{l p-ūm7gܧiU ||o?&_>%n & ǀ Ȁ`Er 'v9`s9evm3ݳOepx?Qz ^]&$*8z:w9~8 Ő ar{sfm}Hw"z@WZM8k:(c}g5@|abVPbFw5`Txux~sE`**hdP1k^G^`WHw6ewu|p}wF8b5 a7bo`eqȂ6VcmFs8ozm`u6|˷*6pmaP5|Wٶ 'kxP8x>qdP f50|x(V"2 (oP:p(r `Ϙwh{5k6pwPmr pF'dgm IF }p0u1iesK |ʳuYYg@v`9v e1^3Gp6p$h/l9xjГ-iuаm2kH`^3Gh,ȒduiGyIxs}~he^YNp@ f-I/Wz%(۸w IW96  QؗЌ>QfxgvpF0pչ_%f1yF)ІeFV'gm@pr蓴6zYhsv{w K韗 mh 7@5^p _HQx^g~`%#'H~4G:`fVhh5em60x 3:h3zhzy s=Z1,9=:kڊzIf?aУ 'sI;Z o}aE$g %cYz  jtAP}zijGUvfj:U穡*d*Zbj^Z^ꪱ]*\jYJYZw+'JjjfNdS jd*M׊٪ʭ *JjjR PR 0ڱӡ SJi2 Nn`$0&P &P'0& k " [!Q?(*J NP! `i70˱ϰ@ L{V%TN®J$KഌM ˲M $M(` `Bk!&m˵ൊ%p 'N% iP"  K %0Mwk庹N a [M'P{(k MNM𵈰(kPW˹{kKN`Ӌ$@zNNKQட+ KQж%@ (@U;NO w >[l{Pp(  `< ;|J{ P PW!PQC۸ 0ܻ  ;;*cMJX[p{`ѾW #DՃ,Ԕ Ɯ%V׎0o|\0' [ Ɋ \Imya<_;͐,,~k: ̘̚ձ{،M"ܶp}ʩܱ_ڌԟ y\Pْ]Y9;ψ6MܮrlȽP|ۏ# İPǴ[6  0 am^+(; L 4@qɔ0VN"+.d>aWü$k aS"fwEo{n[_mƫ^RyaʪeGE^~/eo [n?m&?aPf` P p~%N6躃gfZFB !20_.頞^vs?pfյSNf:]Bn쇠iDn,vO.b  o@p$_b.rf]~Of!>"0*@CR`&5%53o5?`"TA p BY;RWT/fO|N8Fe2 i8xK`_Dothup y0ooPא`T$/ttp?pנ~`> IPusN`* `"` SoOM7Qs(aˮ_R2NWoz nqO|M`.?BB@Bzm\7y3y z|G9XYnJiIz:ҽ B=Սӻ @ҢtȪyiHˉ** %Ѝ aP?x 3j$B?ra#YҤCPp  =l B;(T^* :~a8$aׯ`Êu9̚ @.nٚ,Z݋ɏ_?"Є=ufʑǐ#/k*Dx }KMؗSTBMo語MmF=~uػh`mǔ+_drBZK,d\lË} 8`˟OP|O]_( mz` 6Q|o0%ae\$NKAMH4b8(cw'j@ p'N#"N:"y%VfC%Bץt_RF& \Rf'ii ɛ\:hwkJp''QN*h72WIgjSHֲR$5`G-q= !xg2hu(jF)Jvsd񪬃f"- ւV[;إuY,Fѩ.Yrqu^ˍ;PNm~e@Ԃ(B~AgrQ<ď=z'rp A w"n̑@r`Ha&,"eCL-uEe#* 9Ꮕ5`R6dE@υT5@}5~!i8] ~ 0Ȑ"YvOӸ6^e:eބ s(yHb>*\ |!"" NgOlgΊ"Khgx<26rֈ6_/MhyVO*oDz!9\uo5 5nUJ6 P#9҃Q|N q$ e' SX8dz(@B{\*`e[9W2 f3@LFFV[4a( ]8W"ZC:B@&(1FvEo q4*bbȿ4-$-Vxa^FnbW)Zd-eLFғ$CIJJ>,%TJQEbI%&^pqV$Og5$$%4FSF9v+#8eQK4N!\I\s`K_rȝ%ܤȓ#=۩AVcxy#-j{ B %:oiR"IRyL=JH"rJ$%SZ~N4<6D`c|!ISRt<= .\?t=Qkhp;47NubUW!3IvT2W#jPԭdZ)_JcE]J>#B89vI¨| 18 KO RATm@D+3x1n,RІfഩbF&^2 dnK39d.7|.l;!$97ۉ|6?~0n2\wo{+ <sc*Mōcrh8[%to:ǡ׮\fDPsxo_8KP) Ё5VLt&b|S5q>IwÉF v)/" k.79P@Rĩqʡ㲘)'M^f#M&Ƹ"Eyf(Ʀg9M_D a ?".UhS@j4JXfMDGBHq/mUCtrN5%Ԋf o aV. JKH8<jDhm8JoE' fB8TD?I̜|i)Z·cK\ SNp &P qsYcN|`c1qzwrLu /z=f!+yjɲ#;termui-3.1.0/_examples/000077500000000000000000000000001351315327100147765ustar00rootroot00000000000000termui-3.1.0/_examples/barchart.go000066400000000000000000000016121351315327100171130ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() bc := widgets.NewBarChart() bc.Data = []float64{3, 2, 5, 3, 9, 3} bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"} bc.Title = "Bar Chart" bc.SetRect(5, 5, 100, 25) bc.BarWidth = 5 bc.BarColors = []ui.Color{ui.ColorRed, ui.ColorGreen} bc.LabelStyles = []ui.Style{ui.NewStyle(ui.ColorBlue)} bc.NumStyles = []ui.Style{ui.NewStyle(ui.ColorYellow)} ui.Render(bc) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/canvas.go000066400000000000000000000006521351315327100166030ustar00rootroot00000000000000// +build ignore package main import ( "image" "log" ui "github.com/gizak/termui/v3" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() c := ui.NewCanvas() c.SetRect(0, 0, 50, 50) c.SetLine(image.Pt(0, 0), image.Pt(10, 20), ui.ColorWhite) ui.Render(c) for e := range ui.PollEvents() { if e.Type == ui.KeyboardEvent { break } } } termui-3.1.0/_examples/demo.go000066400000000000000000000074401351315327100162560ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" "math" "time" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() p := widgets.NewParagraph() p.Title = "Text Box" p.Text = "PRESS q TO QUIT DEMO" p.SetRect(0, 0, 50, 5) p.TextStyle.Fg = ui.ColorWhite p.BorderStyle.Fg = ui.ColorCyan updateParagraph := func(count int) { if count%2 == 0 { p.TextStyle.Fg = ui.ColorRed } else { p.TextStyle.Fg = ui.ColorWhite } } listData := []string{ "[0] gizak/termui", "[1] editbox.go", "[2] interrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go", } l := widgets.NewList() l.Title = "List" l.Rows = listData l.SetRect(0, 5, 25, 12) l.TextStyle.Fg = ui.ColorYellow g := widgets.NewGauge() g.Title = "Gauge" g.Percent = 50 g.SetRect(0, 12, 50, 15) g.BarColor = ui.ColorRed g.BorderStyle.Fg = ui.ColorWhite g.TitleStyle.Fg = ui.ColorCyan sparklineData := []float64{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} sl := widgets.NewSparkline() sl.Title = "srv 0:" sl.Data = sparklineData sl.LineColor = ui.ColorCyan sl.TitleStyle.Fg = ui.ColorWhite sl2 := widgets.NewSparkline() sl2.Title = "srv 1:" sl2.Data = sparklineData sl2.TitleStyle.Fg = ui.ColorWhite sl2.LineColor = ui.ColorRed slg := widgets.NewSparklineGroup(sl, sl2) slg.Title = "Sparkline" slg.SetRect(25, 5, 50, 12) sinData := (func() []float64 { n := 220 ps := make([]float64, n) for i := range ps { ps[i] = 1 + math.Sin(float64(i)/5) } return ps })() lc := widgets.NewPlot() lc.Title = "dot-marker Line Chart" lc.Data = make([][]float64, 1) lc.Data[0] = sinData lc.SetRect(0, 15, 50, 25) lc.AxesColor = ui.ColorWhite lc.LineColors[0] = ui.ColorRed lc.Marker = widgets.MarkerDot barchartData := []float64{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} bc := widgets.NewBarChart() bc.Title = "Bar Chart" bc.SetRect(50, 0, 75, 10) bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"} bc.BarColors[0] = ui.ColorGreen bc.NumStyles[0] = ui.NewStyle(ui.ColorBlack) lc2 := widgets.NewPlot() lc2.Title = "braille-mode Line Chart" lc2.Data = make([][]float64, 1) lc2.Data[0] = sinData lc2.SetRect(50, 15, 75, 25) lc2.AxesColor = ui.ColorWhite lc2.LineColors[0] = ui.ColorYellow p2 := widgets.NewParagraph() p2.Text = "Hey!\nI am a borderless block!" p2.Border = false p2.SetRect(50, 10, 75, 10) p2.TextStyle.Fg = ui.ColorMagenta draw := func(count int) { g.Percent = count % 101 l.Rows = listData[count%9:] slg.Sparklines[0].Data = sparklineData[:30+count%50] slg.Sparklines[1].Data = sparklineData[:35+count%50] lc.Data[0] = sinData[count/2%220:] lc2.Data[0] = sinData[2*count%220:] bc.Data = barchartData[count/2%10:] ui.Render(p, l, g, slg, lc, bc, lc2, p2) } tickerCount := 1 draw(tickerCount) tickerCount++ uiEvents := ui.PollEvents() ticker := time.NewTicker(time.Second).C for { select { case e := <-uiEvents: switch e.ID { case "q", "": return } case <-ticker: updateParagraph(tickerCount) draw(tickerCount) tickerCount++ } } } termui-3.1.0/_examples/gauge.go000066400000000000000000000030561351315327100164210ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "fmt" "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() g0 := widgets.NewGauge() g0.Title = "Slim Gauge" g0.SetRect(20, 20, 30, 30) g0.Percent = 75 g0.BarColor = ui.ColorRed g0.BorderStyle.Fg = ui.ColorWhite g0.TitleStyle.Fg = ui.ColorCyan g2 := widgets.NewGauge() g2.Title = "Slim Gauge" g2.SetRect(0, 3, 50, 6) g2.Percent = 60 g2.BarColor = ui.ColorYellow g2.LabelStyle = ui.NewStyle(ui.ColorBlue) g2.BorderStyle.Fg = ui.ColorWhite g1 := widgets.NewGauge() g1.Title = "Big Gauge" g1.SetRect(0, 6, 50, 11) g1.Percent = 30 g1.BarColor = ui.ColorGreen g1.LabelStyle = ui.NewStyle(ui.ColorYellow) g1.TitleStyle.Fg = ui.ColorMagenta g1.BorderStyle.Fg = ui.ColorWhite g3 := widgets.NewGauge() g3.Title = "Gauge with custom label" g3.SetRect(0, 11, 50, 14) g3.Percent = 50 g3.Label = fmt.Sprintf("%v%% (100MBs free)", g3.Percent) g4 := widgets.NewGauge() g4.Title = "Gauge" g4.SetRect(0, 14, 50, 17) g4.Percent = 50 g4.Label = "Gauge with custom highlighted label" g4.BarColor = ui.ColorGreen g4.LabelStyle = ui.NewStyle(ui.ColorYellow) ui.Render(g0, g1, g2, g3, g4) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/grid.go000066400000000000000000000047031351315327100162560ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" "math" "time" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() sinFloat64 := (func() []float64 { n := 400 data := make([]float64, n) for i := range data { data[i] = 1 + math.Sin(float64(i)/5) } return data })() sl := widgets.NewSparkline() sl.Data = sinFloat64[:100] sl.LineColor = ui.ColorCyan sl.TitleStyle.Fg = ui.ColorWhite slg := widgets.NewSparklineGroup(sl) slg.Title = "Sparkline" lc := widgets.NewPlot() lc.Title = "braille-mode Line Chart" lc.Data = append(lc.Data, sinFloat64) lc.AxesColor = ui.ColorWhite lc.LineColors[0] = ui.ColorYellow gs := make([]*widgets.Gauge, 3) for i := range gs { gs[i] = widgets.NewGauge() gs[i].Percent = i * 10 gs[i].BarColor = ui.ColorRed } ls := widgets.NewList() ls.Rows = []string{ "[1] Downloading File 1", "", "", "", "[2] Downloading File 2", "", "", "", "[3] Uploading File 3", } ls.Border = false p := widgets.NewParagraph() p.Text = "<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget" p.Title = "Demonstration" grid := ui.NewGrid() termWidth, termHeight := ui.TerminalDimensions() grid.SetRect(0, 0, termWidth, termHeight) grid.Set( ui.NewRow(1.0/2, ui.NewCol(1.0/2, slg), ui.NewCol(1.0/2, lc), ), ui.NewRow(1.0/2, ui.NewCol(1.0/4, ls), ui.NewCol(1.0/4, ui.NewRow(.9/3, gs[0]), ui.NewRow(.9/3, gs[1]), ui.NewRow(1.2/3, gs[2]), ), ui.NewCol(1.0/2, p), ), ) ui.Render(grid) tickerCount := 1 uiEvents := ui.PollEvents() ticker := time.NewTicker(time.Second).C for { select { case e := <-uiEvents: switch e.ID { case "q", "": return case "": payload := e.Payload.(ui.Resize) grid.SetRect(0, 0, payload.Width, payload.Height) ui.Clear() ui.Render(grid) } case <-ticker: if tickerCount == 100 { return } for _, g := range gs { g.Percent = (g.Percent + 3) % 100 } slg.Sparklines[0].Data = sinFloat64[tickerCount : tickerCount+100] lc.Data[0] = sinFloat64[2*tickerCount:] ui.Render(grid) tickerCount++ } } } termui-3.1.0/_examples/hello_world.go000066400000000000000000000006531351315327100176430ustar00rootroot00000000000000// +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() p := widgets.NewParagraph() p.Text = "Hello World!" p.SetRect(0, 0, 25, 5) ui.Render(p) for e := range ui.PollEvents() { if e.Type == ui.KeyboardEvent { break } } } termui-3.1.0/_examples/image.go000066400000000000000000000076731351315327100164240ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "encoding/base64" "fmt" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "log" "net/http" "os" "strings" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { var images []image.Image for _, arg := range os.Args[1:] { resp, err := http.Get(arg) if err != nil { log.Fatalf("failed to fetch image: %v", err) } image, _, err := image.Decode(resp.Body) if err != nil { log.Fatalf("failed to decode fetched image: %v", err) } images = append(images, image) } if len(images) == 0 { image, _, err := image.Decode(base64.NewDecoder(base64.StdEncoding, strings.NewReader(GOPHER_IMAGE))) if err != nil { log.Fatalf("failed to decode gopher image: %v", err) } images = append(images, image) } if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() img := widgets.NewImage(nil) img.SetRect(0, 0, 100, 50) index := 0 render := func() { img.Image = images[index] if !img.Monochrome { img.Title = fmt.Sprintf("Color %d/%d", index+1, len(images)) } else if !img.MonochromeInvert { img.Title = fmt.Sprintf("Monochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) } else { img.Title = fmt.Sprintf("InverseMonochrome(%d) %d/%d", img.MonochromeThreshold, index+1, len(images)) } ui.Render(img) } render() uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return case "", "h": index = (index + len(images) - 1) % len(images) case "", "l": index = (index + 1) % len(images) case "", "k": img.MonochromeThreshold++ case "", "j": img.MonochromeThreshold-- case "": img.Monochrome = !img.Monochrome case "": img.MonochromeInvert = !img.MonochromeInvert } render() } } const GOPHER_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==` termui-3.1.0/_examples/list.go000066400000000000000000000026431351315327100163050ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() l := widgets.NewList() l.Title = "List" l.Rows = []string{ "[0] github.com/gizak/termui/v3", "[1] [你好,世界](fg:blue)", "[2] [こんにちは世界](fg:red)", "[3] [color](fg:white,bg:green) output", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] foo", "[8] bar", "[9] baz", } l.TextStyle = ui.NewStyle(ui.ColorYellow) l.WrapText = false l.SetRect(0, 0, 25, 8) ui.Render(l) previousKey := "" uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return case "j", "": l.ScrollDown() case "k", "": l.ScrollUp() case "": l.ScrollHalfPageDown() case "": l.ScrollHalfPageUp() case "": l.ScrollPageDown() case "": l.ScrollPageUp() case "g": if previousKey == "g" { l.ScrollTop() } case "": l.ScrollTop() case "G", "": l.ScrollBottom() } if previousKey == "g" { previousKey = "" } else { previousKey = e.ID } ui.Render(l) } } termui-3.1.0/_examples/paragraph.go000066400000000000000000000034411351315327100172740ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() p0 := widgets.NewParagraph() p0.Text = "Borderless Text" p0.SetRect(0, 0, 20, 5) p0.Border = false p1 := widgets.NewParagraph() p1.Title = "标签" p1.Text = "你好,世界。" p1.SetRect(20, 0, 35, 5) p2 := widgets.NewParagraph() p2.Title = "Multiline" p2.Text = "Simple colored text\nwith label. It [can be](fg:red) multilined with \\n or [break automatically](fg:red,fg:bold)" p2.SetRect(0, 5, 35, 10) p2.BorderStyle.Fg = ui.ColorYellow p3 := widgets.NewParagraph() p3.Title = "Auto Trim" p3.Text = "Long text with label and it is auto trimmed." p3.SetRect(0, 10, 40, 15) p4 := widgets.NewParagraph() p4.Title = "Text Box with Wrapping" p4.Text = "Press q to QUIT THE DEMO. [There](fg:blue,mod:bold) are other things [that](fg:red) are going to fit in here I think. What do you think? Now is the time for all good [men to](bg:blue) come to the aid of their country. [This is going to be one really really really long line](fg:green) that is going to go together and stuffs and things. Let's see how this thing renders out.\n Here is a new paragraph and stuffs and things. There should be a tab indent at the beginning of the paragraph. Let's see if that worked as well." p4.SetRect(40, 0, 70, 20) p4.BorderStyle.Fg = ui.ColorBlue ui.Render(p0, p1, p2, p3, p4) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/piechart.go000066400000000000000000000024051351315327100171250ustar00rootroot00000000000000// +build ignore package main import ( "fmt" "log" "math" "math/rand" "time" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) var run = true func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() rand.Seed(time.Now().UTC().UnixNano()) randomDataAndOffset := func() (data []float64, offset float64) { noSlices := 1 + rand.Intn(5) data = make([]float64, noSlices) for i := range data { data[i] = rand.Float64() } offset = 2.0 * math.Pi * rand.Float64() return } pc := widgets.NewPieChart() pc.Title = "Pie Chart" pc.SetRect(5, 5, 70, 36) pc.Data = []float64{.25, .25, .25, .25} pc.AngleOffset = -.5 * math.Pi pc.LabelFormatter = func(i int, v float64) string { return fmt.Sprintf("%.02f", v) } pause := func() { run = !run if run { pc.Title = "Pie Chart" } else { pc.Title = "Pie Chart (Stopped)" } ui.Render(pc) } ui.Render(pc) uiEvents := ui.PollEvents() ticker := time.NewTicker(time.Second).C for { select { case e := <-uiEvents: switch e.ID { case "q", "": return case "s": pause() } case <-ticker: if run { pc.Data, pc.AngleOffset = randomDataAndOffset() ui.Render(pc) } } } } termui-3.1.0/_examples/plot.go000066400000000000000000000036351351315327100163120ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" "math" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() sinData := func() [][]float64 { n := 220 data := make([][]float64, 2) data[0] = make([]float64, n) data[1] = make([]float64, n) for i := 0; i < n; i++ { data[0][i] = 1 + math.Sin(float64(i)/5) data[1][i] = 1 + math.Cos(float64(i)/5) } return data }() p0 := widgets.NewPlot() p0.Title = "braille-mode Line Chart" p0.Data = sinData p0.SetRect(0, 0, 50, 15) p0.AxesColor = ui.ColorWhite p0.LineColors[0] = ui.ColorGreen p1 := widgets.NewPlot() p1.Title = "dot-mode line Chart" p1.Marker = widgets.MarkerDot p1.Data = [][]float64{[]float64{1, 2, 3, 4, 5}} p1.SetRect(50, 0, 75, 10) p1.DotMarkerRune = '+' p1.AxesColor = ui.ColorWhite p1.LineColors[0] = ui.ColorYellow p1.DrawDirection = widgets.DrawLeft p2 := widgets.NewPlot() p2.Title = "dot-mode Scatter Plot" p2.Marker = widgets.MarkerDot p2.Data = make([][]float64, 2) p2.Data[0] = []float64{1, 2, 3, 4, 5} p2.Data[1] = sinData[1][4:] p2.SetRect(0, 15, 50, 30) p2.AxesColor = ui.ColorWhite p2.LineColors[0] = ui.ColorCyan p2.PlotType = widgets.ScatterPlot p3 := widgets.NewPlot() p3.Title = "braille-mode Scatter Plot" p3.Data = make([][]float64, 2) p3.Data[0] = []float64{1, 2, 3, 4, 5} p3.Data[1] = sinData[1][4:] p3.SetRect(45, 15, 80, 30) p3.AxesColor = ui.ColorWhite p3.LineColors[0] = ui.ColorCyan p3.Marker = widgets.MarkerBraille p3.PlotType = widgets.ScatterPlot ui.Render(p0, p1, p2, p3) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/sparkline.go000066400000000000000000000026561351315327100173260ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() data := []float64{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} sl0 := widgets.NewSparkline() sl0.Data = data[3:] sl0.LineColor = ui.ColorGreen // single slg0 := widgets.NewSparklineGroup(sl0) slg0.Title = "Sparkline 0" slg0.SetRect(0, 0, 20, 10) sl1 := widgets.NewSparkline() sl1.Title = "Sparkline 1" sl1.Data = data sl1.LineColor = ui.ColorRed sl2 := widgets.NewSparkline() sl2.Title = "Sparkline 2" sl2.Data = data[5:] sl2.LineColor = ui.ColorMagenta slg1 := widgets.NewSparklineGroup(sl0, sl1, sl2) slg1.Title = "Group Sparklines" slg1.SetRect(0, 10, 25, 25) sl3 := widgets.NewSparkline() sl3.Title = "Enlarged Sparkline" sl3.Data = data sl3.LineColor = ui.ColorYellow slg2 := widgets.NewSparklineGroup(sl3) slg2.Title = "Tweeked Sparkline" slg2.SetRect(20, 0, 50, 10) slg2.BorderStyle.Fg = ui.ColorCyan ui.Render(slg0, slg1, slg2) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/stacked_barchart.go000066400000000000000000000017351351315327100206170ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() sbc := widgets.NewStackedBarChart() sbc.Title = "Student's Marks: X-Axis=Name, Y-Axis=Grade% (Math, English, Science, Computer Science)" sbc.Labels = []string{"Ken", "Rob", "Dennis", "Linus"} sbc.Data = make([][]float64, 4) sbc.Data[0] = []float64{90, 85, 90, 80} sbc.Data[1] = []float64{70, 85, 75, 60} sbc.Data[2] = []float64{75, 60, 80, 85} sbc.Data[3] = []float64{100, 100, 100, 100} sbc.SetRect(5, 5, 100, 30) sbc.BarWidth = 5 ui.Render(sbc) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/table.go000066400000000000000000000034371351315327100164230ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() table1 := widgets.NewTable() table1.Rows = [][]string{ []string{"header1", "header2", "header3"}, []string{"你好吗", "Go-lang is so cool", "Im working on Ruby"}, []string{"2016", "10", "11"}, } table1.TextStyle = ui.NewStyle(ui.ColorWhite) table1.SetRect(0, 0, 60, 10) ui.Render(table1) table2 := widgets.NewTable() table2.Rows = [][]string{ []string{"header1", "header2", "header3"}, []string{"Foundations", "Go-lang is so cool", "Im working on Ruby"}, []string{"2016", "11", "11"}, } table2.TextStyle = ui.NewStyle(ui.ColorWhite) table2.TextAlignment = ui.AlignCenter table2.RowSeparator = false table2.SetRect(0, 10, 20, 20) ui.Render(table2) table3 := widgets.NewTable() table3.Rows = [][]string{ []string{"header1", "header2", "header3"}, []string{"AAA", "BBB", "CCC"}, []string{"DDD", "EEE", "FFF"}, []string{"GGG", "HHH", "III"}, } table3.TextStyle = ui.NewStyle(ui.ColorWhite) table3.RowSeparator = true table3.BorderStyle = ui.NewStyle(ui.ColorGreen) table3.SetRect(0, 30, 70, 20) table3.FillRow = true table3.RowStyles[0] = ui.NewStyle(ui.ColorWhite, ui.ColorBlack, ui.ModifierBold) table3.RowStyles[2] = ui.NewStyle(ui.ColorWhite, ui.ColorRed, ui.ModifierBold) table3.RowStyles[3] = ui.NewStyle(ui.ColorYellow) ui.Render(table3) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return } } } termui-3.1.0/_examples/tabs.go000066400000000000000000000032061351315327100162570ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() header := widgets.NewParagraph() header.Text = "Press q to quit, Press h or l to switch tabs" header.SetRect(0, 0, 50, 1) header.Border = false header.TextStyle.Bg = ui.ColorBlue p2 := widgets.NewParagraph() p2.Text = "Press q to quit\nPress h or l to switch tabs\n" p2.Title = "Keys" p2.SetRect(5, 5, 40, 15) p2.BorderStyle.Fg = ui.ColorYellow bc := widgets.NewBarChart() bc.Title = "Bar Chart" bc.Data = []float64{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} bc.SetRect(5, 5, 35, 10) bc.Labels = []string{"S0", "S1", "S2", "S3", "S4", "S5"} tabpane := widgets.NewTabPane("pierwszy", "drugi", "trzeci", "żółw", "four", "five") tabpane.SetRect(0, 1, 50, 4) tabpane.Border = true renderTab := func() { switch tabpane.ActiveTabIndex { case 0: ui.Render(p2) case 1: ui.Render(bc) } } ui.Render(header, tabpane, p2) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return case "h": tabpane.FocusLeft() ui.Clear() ui.Render(header, tabpane) renderTab() case "l": tabpane.FocusRight() ui.Clear() ui.Render(header, tabpane) renderTab() } } } termui-3.1.0/_examples/tree.go000066400000000000000000000037431351315327100162730ustar00rootroot00000000000000/// +build ignore package main import ( "log" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) type nodeValue string func (nv nodeValue) String() string { return string(nv) } func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() nodes := []*widgets.TreeNode{ { Value: nodeValue("Key 1"), Nodes: []*widgets.TreeNode{ { Value: nodeValue("Key 1.1"), Nodes: []*widgets.TreeNode{ { Value: nodeValue("Key 1.1.1"), Nodes: nil, }, { Value: nodeValue("Key 1.1.2"), Nodes: nil, }, }, }, { Value: nodeValue("Key 1.2"), Nodes: nil, }, }, }, { Value: nodeValue("Key 2"), Nodes: []*widgets.TreeNode{ { Value: nodeValue("Key 2.1"), Nodes: nil, }, { Value: nodeValue("Key 2.2"), Nodes: nil, }, { Value: nodeValue("Key 2.3"), Nodes: nil, }, }, }, { Value: nodeValue("Key 3"), Nodes: nil, }, } l := widgets.NewTree() l.TextStyle = ui.NewStyle(ui.ColorYellow) l.WrapText = false l.SetNodes(nodes) x, y := ui.TerminalDimensions() l.SetRect(0, 0, x, y) ui.Render(l) previousKey := "" uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", "": return case "j", "": l.ScrollDown() case "k", "": l.ScrollUp() case "": l.ScrollHalfPageDown() case "": l.ScrollHalfPageUp() case "": l.ScrollPageDown() case "": l.ScrollPageUp() case "g": if previousKey == "g" { l.ScrollTop() } case "": l.ScrollTop() case "": l.ToggleExpand() case "G", "": l.ScrollBottom() case "E": l.ExpandAll() case "C": l.CollapseAll() case "": x, y := ui.TerminalDimensions() l.SetRect(0, 0, x, y) } if previousKey == "g" { previousKey = "" } else { previousKey = e.ID } ui.Render(l) } } termui-3.1.0/_scripts/000077500000000000000000000000001351315327100146475ustar00rootroot00000000000000termui-3.1.0/_scripts/copyright_header.py000077500000000000000000000022161351315327100205450ustar00rootroot00000000000000#!/usr/bin/env python3 import re import os import io copyright = """// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. """ exclude_dirs = [".git", "_docs"] exclude_files = [] include_dirs = [".", "debug", "extra", "test", "_example"] def is_target(fpath): if os.path.splitext(fpath)[-1] == ".go": return True return False def update_copyright(fpath): print("processing " + fpath) f = io.open(fpath, 'r', encoding='utf-8') fstr = f.read() f.close() # remove old m = re.search('^// Copyright .+?\r?\n\r?\n', fstr, re.MULTILINE|re.DOTALL) if m: fstr = fstr[m.end():] # add new fstr = copyright + fstr f = io.open(fpath, 'w',encoding='utf-8') f.write(fstr) f.close() def main(): for d in include_dirs: files = [ os.path.join(d, f) for f in os.listdir(d) if os.path.isfile(os.path.join(d, f)) ] for f in files: if is_target(f): update_copyright(f) if __name__ == '__main__': main() termui-3.1.0/_test/000077500000000000000000000000001351315327100141375ustar00rootroot00000000000000termui-3.1.0/_test/log_events.go000066400000000000000000000012161351315327100166330ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build ignore package main import ( "fmt" "log" ui "github.com/gizak/termui/v3" ) // logs all events to the termui window // stdout can also be redirected to a file and read with `tail -f` func main() { if err := ui.Init(); err != nil { log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() events := ui.PollEvents() for { e := <-events fmt.Printf("%v", e) switch e.ID { case "q", "": return case "": return } } } termui-3.1.0/v3/000077500000000000000000000000001351315327100133515ustar00rootroot00000000000000termui-3.1.0/v3/alignment.go000066400000000000000000000001441351315327100156550ustar00rootroot00000000000000package termui type Alignment uint const ( AlignLeft Alignment = iota AlignCenter AlignRight ) termui-3.1.0/v3/backend.go000066400000000000000000000014021351315327100152640ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( tb "github.com/nsf/termbox-go" ) // Init initializes termbox-go and is required to render anything. // After initialization, the library must be finalized with `Close`. func Init() error { if err := tb.Init(); err != nil { return err } tb.SetInputMode(tb.InputEsc | tb.InputMouse) tb.SetOutputMode(tb.Output256) return nil } // Close closes termbox-go. func Close() { tb.Close() } func TerminalDimensions() (int, int) { tb.Sync() width, height := tb.Size() return width, height } func Clear() { tb.Clear(tb.ColorDefault, tb.Attribute(Theme.Default.Bg+1)) } termui-3.1.0/v3/block.go000066400000000000000000000052461351315327100150010ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "image" "sync" ) // Block is the base struct inherited by most widgets. // Block manages size, position, border, and title. // It implements all 3 of the methods needed for the `Drawable` interface. // Custom widgets will override the Draw method. type Block struct { Border bool BorderStyle Style BorderLeft, BorderRight, BorderTop, BorderBottom bool PaddingLeft, PaddingRight, PaddingTop, PaddingBottom int image.Rectangle Inner image.Rectangle Title string TitleStyle Style sync.Mutex } func NewBlock() *Block { return &Block{ Border: true, BorderStyle: Theme.Block.Border, BorderLeft: true, BorderRight: true, BorderTop: true, BorderBottom: true, TitleStyle: Theme.Block.Title, } } func (self *Block) drawBorder(buf *Buffer) { verticalCell := Cell{VERTICAL_LINE, self.BorderStyle} horizontalCell := Cell{HORIZONTAL_LINE, self.BorderStyle} // draw lines if self.BorderTop { buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Min.Y, self.Max.X, self.Min.Y+1)) } if self.BorderBottom { buf.Fill(horizontalCell, image.Rect(self.Min.X, self.Max.Y-1, self.Max.X, self.Max.Y)) } if self.BorderLeft { buf.Fill(verticalCell, image.Rect(self.Min.X, self.Min.Y, self.Min.X+1, self.Max.Y)) } if self.BorderRight { buf.Fill(verticalCell, image.Rect(self.Max.X-1, self.Min.Y, self.Max.X, self.Max.Y)) } // draw corners if self.BorderTop && self.BorderLeft { buf.SetCell(Cell{TOP_LEFT, self.BorderStyle}, self.Min) } if self.BorderTop && self.BorderRight { buf.SetCell(Cell{TOP_RIGHT, self.BorderStyle}, image.Pt(self.Max.X-1, self.Min.Y)) } if self.BorderBottom && self.BorderLeft { buf.SetCell(Cell{BOTTOM_LEFT, self.BorderStyle}, image.Pt(self.Min.X, self.Max.Y-1)) } if self.BorderBottom && self.BorderRight { buf.SetCell(Cell{BOTTOM_RIGHT, self.BorderStyle}, self.Max.Sub(image.Pt(1, 1))) } } // Draw implements the Drawable interface. func (self *Block) Draw(buf *Buffer) { if self.Border { self.drawBorder(buf) } buf.SetString( self.Title, self.TitleStyle, image.Pt(self.Min.X+2, self.Min.Y), ) } // SetRect implements the Drawable interface. func (self *Block) SetRect(x1, y1, x2, y2 int) { self.Rectangle = image.Rect(x1, y1, x2, y2) self.Inner = image.Rect( self.Min.X+1+self.PaddingLeft, self.Min.Y+1+self.PaddingTop, self.Max.X-1-self.PaddingRight, self.Max.Y-1-self.PaddingBottom, ) } // GetRect implements the Drawable interface. func (self *Block) GetRect() image.Rectangle { return self.Rectangle } termui-3.1.0/v3/buffer.go000066400000000000000000000030211351315327100151450ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "image" rw "github.com/mattn/go-runewidth" ) // Cell represents a viewable terminal cell type Cell struct { Rune rune Style Style } var CellClear = Cell{ Rune: ' ', Style: StyleClear, } // NewCell takes 1 to 2 arguments // 1st argument = rune // 2nd argument = optional style func NewCell(rune rune, args ...interface{}) Cell { style := StyleClear if len(args) == 1 { style = args[0].(Style) } return Cell{ Rune: rune, Style: style, } } // Buffer represents a section of a terminal and is a renderable rectangle of cells. type Buffer struct { image.Rectangle CellMap map[image.Point]Cell } func NewBuffer(r image.Rectangle) *Buffer { buf := &Buffer{ Rectangle: r, CellMap: make(map[image.Point]Cell), } buf.Fill(CellClear, r) // clears out area return buf } func (self *Buffer) GetCell(p image.Point) Cell { return self.CellMap[p] } func (self *Buffer) SetCell(c Cell, p image.Point) { self.CellMap[p] = c } func (self *Buffer) Fill(c Cell, rect image.Rectangle) { for x := rect.Min.X; x < rect.Max.X; x++ { for y := rect.Min.Y; y < rect.Max.Y; y++ { self.SetCell(c, image.Pt(x, y)) } } } func (self *Buffer) SetString(s string, style Style, p image.Point) { runes := []rune(s) x := 0 for _, char := range runes { self.SetCell(Cell{char, style}, image.Pt(p.X+x, p.Y)) x += rw.RuneWidth(char) } } termui-3.1.0/v3/canvas.go000066400000000000000000000013611351315327100151540ustar00rootroot00000000000000package termui import ( "image" "github.com/gizak/termui/v3/drawille" ) type Canvas struct { Block drawille.Canvas } func NewCanvas() *Canvas { return &Canvas{ Block: *NewBlock(), Canvas: *drawille.NewCanvas(), } } func (self *Canvas) SetPoint(p image.Point, color Color) { self.Canvas.SetPoint(p, drawille.Color(color)) } func (self *Canvas) SetLine(p0, p1 image.Point, color Color) { self.Canvas.SetLine(p0, p1, drawille.Color(color)) } func (self *Canvas) Draw(buf *Buffer) { for point, cell := range self.Canvas.GetCells() { if point.In(self.Rectangle) { convertedCell := Cell{ cell.Rune, Style{ Color(cell.Color), ColorClear, ModifierClear, }, } buf.SetCell(convertedCell, point) } } } termui-3.1.0/v3/doc.go000066400000000000000000000004301351315327100144420ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. /* Package termui is a library for creating terminal user interfaces (TUIs) using widgets. */ package termui termui-3.1.0/v3/drawille/000077500000000000000000000000001351315327100151545ustar00rootroot00000000000000termui-3.1.0/v3/drawille/drawille.go000066400000000000000000000034351351315327100173130ustar00rootroot00000000000000package drawille import ( "image" ) const BRAILLE_OFFSET = '\u2800' var BRAILLE = [4][2]rune{ {'\u0001', '\u0008'}, {'\u0002', '\u0010'}, {'\u0004', '\u0020'}, {'\u0040', '\u0080'}, } type Color int type Cell struct { Rune rune Color Color } type Canvas struct { CellMap map[image.Point]Cell } func NewCanvas() *Canvas { return &Canvas{ CellMap: make(map[image.Point]Cell), } } func (self *Canvas) SetPoint(p image.Point, color Color) { point := image.Pt(p.X/2, p.Y/4) self.CellMap[point] = Cell{ self.CellMap[point].Rune | BRAILLE[p.Y%4][p.X%2], color, } } func (self *Canvas) SetLine(p0, p1 image.Point, color Color) { for _, p := range line(p0, p1) { self.SetPoint(p, color) } } func (self *Canvas) GetCells() map[image.Point]Cell { cellMap := make(map[image.Point]Cell) for point, cell := range self.CellMap { cellMap[point] = Cell{cell.Rune + BRAILLE_OFFSET, cell.Color} } return cellMap } func line(p0, p1 image.Point) []image.Point { points := []image.Point{} leftPoint, rightPoint := p0, p1 if leftPoint.X > rightPoint.X { leftPoint, rightPoint = rightPoint, leftPoint } xDistance := absInt(leftPoint.X - rightPoint.X) yDistance := absInt(leftPoint.Y - rightPoint.Y) slope := float64(yDistance) / float64(xDistance) slopeSign := 1 if rightPoint.Y < leftPoint.Y { slopeSign = -1 } targetYCoordinate := float64(leftPoint.Y) currentYCoordinate := leftPoint.Y for i := leftPoint.X; i < rightPoint.X; i++ { points = append(points, image.Pt(i, currentYCoordinate)) targetYCoordinate += (slope * float64(slopeSign)) for currentYCoordinate != int(targetYCoordinate) { points = append(points, image.Pt(i, currentYCoordinate)) currentYCoordinate += slopeSign } } return points } func absInt(x int) int { if x >= 0 { return x } return -x } termui-3.1.0/v3/events.go000066400000000000000000000113231351315327100152040ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "fmt" tb "github.com/nsf/termbox-go" ) /* List of events: mouse events: keyboard events: any uppercase or lowercase letter like j or J etc etc > etc terminal events: keyboard events that do not work: */ type EventType uint const ( KeyboardEvent EventType = iota MouseEvent ResizeEvent ) type Event struct { Type EventType ID string Payload interface{} } // Mouse payload. type Mouse struct { Drag bool X int Y int } // Resize payload. type Resize struct { Width int Height int } // PollEvents gets events from termbox, converts them, then sends them to each of its channels. func PollEvents() <-chan Event { ch := make(chan Event) go func() { for { ch <- convertTermboxEvent(tb.PollEvent()) } }() return ch } var keyboardMap = map[tb.Key]string{ tb.KeyF1: "", tb.KeyF2: "", tb.KeyF3: "", tb.KeyF4: "", tb.KeyF5: "", tb.KeyF6: "", tb.KeyF7: "", tb.KeyF8: "", tb.KeyF9: "", tb.KeyF10: "", tb.KeyF11: "", tb.KeyF12: "", tb.KeyInsert: "", tb.KeyDelete: "", tb.KeyHome: "", tb.KeyEnd: "", tb.KeyPgup: "", tb.KeyPgdn: "", tb.KeyArrowUp: "", tb.KeyArrowDown: "", tb.KeyArrowLeft: "", tb.KeyArrowRight: "", tb.KeyCtrlSpace: ">", // tb.KeyCtrl2 tb.KeyCtrlTilde tb.KeyCtrlA: "", tb.KeyCtrlB: "", tb.KeyCtrlC: "", tb.KeyCtrlD: "", tb.KeyCtrlE: "", tb.KeyCtrlF: "", tb.KeyCtrlG: "", tb.KeyBackspace: ">", // tb.KeyCtrlH tb.KeyTab: "", // tb.KeyCtrlI tb.KeyCtrlJ: "", tb.KeyCtrlK: "", tb.KeyCtrlL: "", tb.KeyEnter: "", // tb.KeyCtrlM tb.KeyCtrlN: "", tb.KeyCtrlO: "", tb.KeyCtrlP: "", tb.KeyCtrlQ: "", tb.KeyCtrlR: "", tb.KeyCtrlS: "", tb.KeyCtrlT: "", tb.KeyCtrlU: "", tb.KeyCtrlV: "", tb.KeyCtrlW: "", tb.KeyCtrlX: "", tb.KeyCtrlY: "", tb.KeyCtrlZ: "", tb.KeyEsc: "", // tb.KeyCtrlLsqBracket tb.KeyCtrl3 tb.KeyCtrl4: "", // tb.KeyCtrlBackslash tb.KeyCtrl5: "", // tb.KeyCtrlRsqBracket tb.KeyCtrl6: "", tb.KeyCtrl7: "", // tb.KeyCtrlSlash tb.KeyCtrlUnderscore tb.KeySpace: "", tb.KeyBackspace2: "", // tb.KeyCtrl8: } // convertTermboxKeyboardEvent converts a termbox keyboard event to a more friendly string format. // Combines modifiers into the string instead of having them as additional fields in an event. func convertTermboxKeyboardEvent(e tb.Event) Event { ID := "%s" if e.Mod == tb.ModAlt { ID = "" } if e.Ch != 0 { ID = fmt.Sprintf(ID, string(e.Ch)) } else { converted, ok := keyboardMap[e.Key] if !ok { converted = "" } ID = fmt.Sprintf(ID, converted) } return Event{ Type: KeyboardEvent, ID: ID, } } var mouseButtonMap = map[tb.Key]string{ tb.MouseLeft: "", tb.MouseMiddle: "", tb.MouseRight: "", tb.MouseRelease: "", tb.MouseWheelUp: "", tb.MouseWheelDown: "", } func convertTermboxMouseEvent(e tb.Event) Event { converted, ok := mouseButtonMap[e.Key] if !ok { converted = "Unknown_Mouse_Button" } Drag := e.Mod == tb.ModMotion return Event{ Type: MouseEvent, ID: converted, Payload: Mouse{ X: e.MouseX, Y: e.MouseY, Drag: Drag, }, } } // convertTermboxEvent turns a termbox event into a termui event. func convertTermboxEvent(e tb.Event) Event { if e.Type == tb.EventError { panic(e.Err) } switch e.Type { case tb.EventKey: return convertTermboxKeyboardEvent(e) case tb.EventMouse: return convertTermboxMouseEvent(e) case tb.EventResize: return Event{ Type: ResizeEvent, ID: "", Payload: Resize{ Width: e.Width, Height: e.Height, }, } } return Event{} } termui-3.1.0/v3/go.mod000066400000000000000000000003301351315327100144530ustar00rootroot00000000000000module github.com/gizak/termui/v3 require ( github.com/mattn/go-runewidth v0.0.2 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d ) termui-3.1.0/v3/go.sum000066400000000000000000000012011351315327100144760ustar00rootroot00000000000000github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= termui-3.1.0/v3/grid.go000066400000000000000000000063301351315327100146270ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui type gridItemType uint const ( col gridItemType = 0 row gridItemType = 1 ) type Grid struct { Block Items []*GridItem } // GridItem represents either a Row or Column in a grid. // Holds sizing information and either an []GridItems or a widget. type GridItem struct { Type gridItemType XRatio float64 YRatio float64 WidthRatio float64 HeightRatio float64 Entry interface{} // Entry.type == GridBufferer if IsLeaf else []GridItem IsLeaf bool ratio float64 } func NewGrid() *Grid { g := &Grid{ Block: *NewBlock(), } g.Border = false return g } // NewCol takes a height percentage and either a widget or a Row or Column func NewCol(ratio float64, i ...interface{}) GridItem { _, ok := i[0].(Drawable) entry := i[0] if !ok { entry = i } return GridItem{ Type: col, Entry: entry, IsLeaf: ok, ratio: ratio, } } // NewRow takes a width percentage and either a widget or a Row or Column func NewRow(ratio float64, i ...interface{}) GridItem { _, ok := i[0].(Drawable) entry := i[0] if !ok { entry = i } return GridItem{ Type: row, Entry: entry, IsLeaf: ok, ratio: ratio, } } // Set is used to add Columns and Rows to the grid. // It recursively searches the GridItems, adding leaves to the grid and calculating the dimensions of the leaves. func (self *Grid) Set(entries ...interface{}) { entry := GridItem{ Type: row, Entry: entries, IsLeaf: false, ratio: 1.0, } self.setHelper(entry, 1.0, 1.0) } func (self *Grid) setHelper(item GridItem, parentWidthRatio, parentHeightRatio float64) { var HeightRatio float64 var WidthRatio float64 switch item.Type { case col: HeightRatio = 1.0 WidthRatio = item.ratio case row: HeightRatio = item.ratio WidthRatio = 1.0 } item.WidthRatio = parentWidthRatio * WidthRatio item.HeightRatio = parentHeightRatio * HeightRatio if item.IsLeaf { self.Items = append(self.Items, &item) } else { XRatio := 0.0 YRatio := 0.0 cols := false rows := false children := InterfaceSlice(item.Entry) for i := 0; i < len(children); i++ { if children[i] == nil { continue } child, _ := children[i].(GridItem) child.XRatio = item.XRatio + (item.WidthRatio * XRatio) child.YRatio = item.YRatio + (item.HeightRatio * YRatio) switch child.Type { case col: cols = true XRatio += child.ratio if rows { item.HeightRatio /= 2 } case row: rows = true YRatio += child.ratio if cols { item.WidthRatio /= 2 } } self.setHelper(child, item.WidthRatio, item.HeightRatio) } } } func (self *Grid) Draw(buf *Buffer) { width := float64(self.Dx()) + 1 height := float64(self.Dy()) + 1 for _, item := range self.Items { entry, _ := item.Entry.(Drawable) x := int(width*item.XRatio) + self.Min.X y := int(height*item.YRatio) + self.Min.Y w := int(width * item.WidthRatio) h := int(height * item.HeightRatio) if x+w > self.Dx() { w-- } if y+h > self.Dy() { h-- } entry.SetRect(x, y, x+w, y+h) entry.Lock() entry.Draw(buf) entry.Unlock() } } termui-3.1.0/v3/render.go000066400000000000000000000014001351315327100151520ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "image" "sync" tb "github.com/nsf/termbox-go" ) type Drawable interface { GetRect() image.Rectangle SetRect(int, int, int, int) Draw(*Buffer) sync.Locker } func Render(items ...Drawable) { for _, item := range items { buf := NewBuffer(item.GetRect()) item.Lock() item.Draw(buf) item.Unlock() for point, cell := range buf.CellMap { if point.In(buf.Rectangle) { tb.SetCell( point.X, point.Y, cell.Rune, tb.Attribute(cell.Style.Fg+1)|tb.Attribute(cell.Style.Modifier), tb.Attribute(cell.Style.Bg+1), ) } } } tb.Flush() } termui-3.1.0/v3/style.go000066400000000000000000000024211351315327100150370ustar00rootroot00000000000000package termui // Color is an integer from -1 to 255 // -1 = ColorClear // 0-255 = Xterm colors type Color int // ColorClear clears the Fg or Bg color of a Style const ColorClear Color = -1 // Basic terminal colors const ( ColorBlack Color = 0 ColorRed Color = 1 ColorGreen Color = 2 ColorYellow Color = 3 ColorBlue Color = 4 ColorMagenta Color = 5 ColorCyan Color = 6 ColorWhite Color = 7 ) type Modifier uint const ( // ModifierClear clears any modifiers ModifierClear Modifier = 0 ModifierBold Modifier = 1 << 9 ModifierUnderline Modifier = 1 << 10 ModifierReverse Modifier = 1 << 11 ) // Style represents the style of one terminal cell type Style struct { Fg Color Bg Color Modifier Modifier } // StyleClear represents a default Style, with no colors or modifiers var StyleClear = Style{ Fg: ColorClear, Bg: ColorClear, Modifier: ModifierClear, } // NewStyle takes 1 to 3 arguments // 1st argument = Fg // 2nd argument = optional Bg // 3rd argument = optional Modifier func NewStyle(fg Color, args ...interface{}) Style { bg := ColorClear modifier := ModifierClear if len(args) >= 1 { bg = args[0].(Color) } if len(args) == 2 { modifier = args[1].(Modifier) } return Style{ fg, bg, modifier, } } termui-3.1.0/v3/style_parser.go000066400000000000000000000074241351315327100164230ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "strings" ) const ( tokenFg = "fg" tokenBg = "bg" tokenModifier = "mod" tokenItemSeparator = "," tokenValueSeparator = ":" tokenBeginStyledText = '[' tokenEndStyledText = ']' tokenBeginStyle = '(' tokenEndStyle = ')' ) type parserState uint const ( parserStateDefault parserState = iota parserStateStyleItems parserStateStyledText ) // StyleParserColorMap can be modified to add custom color parsing to text var StyleParserColorMap = map[string]Color{ "red": ColorRed, "blue": ColorBlue, "black": ColorBlack, "cyan": ColorCyan, "yellow": ColorYellow, "white": ColorWhite, "clear": ColorClear, "green": ColorGreen, "magenta": ColorMagenta, } var modifierMap = map[string]Modifier{ "bold": ModifierBold, "underline": ModifierUnderline, "reverse": ModifierReverse, } // readStyle translates an []rune like `fg:red,mod:bold,bg:white` to a style func readStyle(runes []rune, defaultStyle Style) Style { style := defaultStyle split := strings.Split(string(runes), tokenItemSeparator) for _, item := range split { pair := strings.Split(item, tokenValueSeparator) if len(pair) == 2 { switch pair[0] { case tokenFg: style.Fg = StyleParserColorMap[pair[1]] case tokenBg: style.Bg = StyleParserColorMap[pair[1]] case tokenModifier: style.Modifier = modifierMap[pair[1]] } } } return style } // ParseStyles parses a string for embedded Styles and returns []Cell with the correct styling. // Uses defaultStyle for any text without an embedded style. // Syntax is of the form [text](fg:,mod:,bg:). // Ordering does not matter. All fields are optional. func ParseStyles(s string, defaultStyle Style) []Cell { cells := []Cell{} runes := []rune(s) state := parserStateDefault styledText := []rune{} styleItems := []rune{} squareCount := 0 reset := func() { styledText = []rune{} styleItems = []rune{} state = parserStateDefault squareCount = 0 } rollback := func() { cells = append(cells, RunesToStyledCells(styledText, defaultStyle)...) cells = append(cells, RunesToStyledCells(styleItems, defaultStyle)...) reset() } // chop first and last runes chop := func(s []rune) []rune { return s[1 : len(s)-1] } for i, _rune := range runes { switch state { case parserStateDefault: if _rune == tokenBeginStyledText { state = parserStateStyledText squareCount = 1 styledText = append(styledText, _rune) } else { cells = append(cells, Cell{_rune, defaultStyle}) } case parserStateStyledText: switch { case squareCount == 0: switch _rune { case tokenBeginStyle: state = parserStateStyleItems styleItems = append(styleItems, _rune) default: rollback() switch _rune { case tokenBeginStyledText: state = parserStateStyledText squareCount = 1 styleItems = append(styleItems, _rune) default: cells = append(cells, Cell{_rune, defaultStyle}) } } case len(runes) == i+1: rollback() styledText = append(styledText, _rune) case _rune == tokenBeginStyledText: squareCount++ styledText = append(styledText, _rune) case _rune == tokenEndStyledText: squareCount-- styledText = append(styledText, _rune) default: styledText = append(styledText, _rune) } case parserStateStyleItems: styleItems = append(styleItems, _rune) if _rune == tokenEndStyle { style := readStyle(chop(styleItems), defaultStyle) cells = append(cells, RunesToStyledCells(chop(styledText), style)...) reset() } else if len(runes) == i+1 { rollback() } } } return cells } termui-3.1.0/v3/symbols.go000066400000000000000000000021541351315327100153720ustar00rootroot00000000000000package termui const ( DOT = '•' ELLIPSES = '…' UP_ARROW = '▲' DOWN_ARROW = '▼' COLLAPSED = '+' EXPANDED = '−' ) var ( BARS = [...]rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} SHADED_BLOCKS = [...]rune{' ', '░', '▒', '▓', '█'} IRREGULAR_BLOCKS = [...]rune{ ' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█', } BRAILLE_OFFSET = '\u2800' BRAILLE = [4][2]rune{ {'\u0001', '\u0008'}, {'\u0002', '\u0010'}, {'\u0004', '\u0020'}, {'\u0040', '\u0080'}, } DOUBLE_BRAILLE = map[[2]int]rune{ [2]int{0, 0}: '⣀', [2]int{0, 1}: '⡠', [2]int{0, 2}: '⡐', [2]int{0, 3}: '⡈', [2]int{1, 0}: '⢄', [2]int{1, 1}: '⠤', [2]int{1, 2}: '⠔', [2]int{1, 3}: '⠌', [2]int{2, 0}: '⢂', [2]int{2, 1}: '⠢', [2]int{2, 2}: '⠒', [2]int{2, 3}: '⠊', [2]int{3, 0}: '⢁', [2]int{3, 1}: '⠡', [2]int{3, 2}: '⠑', [2]int{3, 3}: '⠉', } SINGLE_BRAILLE_LEFT = [4]rune{'\u2840', '⠄', '⠂', '⠁'} SINGLE_BRAILLE_RIGHT = [4]rune{'\u2880', '⠠', '⠐', '⠈'} ) termui-3.1.0/v3/symbols_other.go000066400000000000000000000010451351315327100165710ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build !windows package termui const ( TOP_LEFT = '┌' TOP_RIGHT = '┐' BOTTOM_LEFT = '└' BOTTOM_RIGHT = '┘' VERTICAL_LINE = '│' HORIZONTAL_LINE = '─' VERTICAL_LEFT = '┤' VERTICAL_RIGHT = '├' HORIZONTAL_UP = '┴' HORIZONTAL_DOWN = '┬' QUOTA_LEFT = '«' QUOTA_RIGHT = '»' VERTICAL_DASH = '┊' HORIZONTAL_DASH = '┈' ) termui-3.1.0/v3/symbols_windows.go000066400000000000000000000010121351315327100171340ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. // +build windows package termui const ( TOP_LEFT = '+' TOP_RIGHT = '+' BOTTOM_LEFT = '+' BOTTOM_RIGHT = '+' VERTICAL_LINE = '|' HORIZONTAL_LINE = '-' VERTICAL_LEFT = '+' VERTICAL_RIGHT = '+' HORIZONTAL_UP = '+' HORIZONTAL_DOWN = '+' QUOTA_LEFT = '<' QUOTA_RIGHT = '>' VERTICAL_DASH = '|' HORIZONTAL_DASH = '-' ) termui-3.1.0/v3/theme.go000066400000000000000000000053141351315327100150050ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui var StandardColors = []Color{ ColorRed, ColorGreen, ColorYellow, ColorBlue, ColorMagenta, ColorCyan, ColorWhite, } var StandardStyles = []Style{ NewStyle(ColorRed), NewStyle(ColorGreen), NewStyle(ColorYellow), NewStyle(ColorBlue), NewStyle(ColorMagenta), NewStyle(ColorCyan), NewStyle(ColorWhite), } type RootTheme struct { Default Style Block BlockTheme BarChart BarChartTheme Gauge GaugeTheme Plot PlotTheme List ListTheme Tree TreeTheme Paragraph ParagraphTheme PieChart PieChartTheme Sparkline SparklineTheme StackedBarChart StackedBarChartTheme Tab TabTheme Table TableTheme } type BlockTheme struct { Title Style Border Style } type BarChartTheme struct { Bars []Color Nums []Style Labels []Style } type GaugeTheme struct { Bar Color Label Style } type PlotTheme struct { Lines []Color Axes Color } type ListTheme struct { Text Style } type TreeTheme struct { Text Style Collapsed rune Expanded rune } type ParagraphTheme struct { Text Style } type PieChartTheme struct { Slices []Color } type SparklineTheme struct { Title Style Line Color } type StackedBarChartTheme struct { Bars []Color Nums []Style Labels []Style } type TabTheme struct { Active Style Inactive Style } type TableTheme struct { Text Style } // Theme holds the default Styles and Colors for all widgets. // You can set default widget Styles by modifying the Theme before creating the widgets. var Theme = RootTheme{ Default: NewStyle(ColorWhite), Block: BlockTheme{ Title: NewStyle(ColorWhite), Border: NewStyle(ColorWhite), }, BarChart: BarChartTheme{ Bars: StandardColors, Nums: StandardStyles, Labels: StandardStyles, }, Paragraph: ParagraphTheme{ Text: NewStyle(ColorWhite), }, PieChart: PieChartTheme{ Slices: StandardColors, }, List: ListTheme{ Text: NewStyle(ColorWhite), }, Tree: TreeTheme{ Text: NewStyle(ColorWhite), Collapsed: COLLAPSED, Expanded: EXPANDED, }, StackedBarChart: StackedBarChartTheme{ Bars: StandardColors, Nums: StandardStyles, Labels: StandardStyles, }, Gauge: GaugeTheme{ Bar: ColorWhite, Label: NewStyle(ColorWhite), }, Sparkline: SparklineTheme{ Title: NewStyle(ColorWhite), Line: ColorWhite, }, Plot: PlotTheme{ Lines: StandardColors, Axes: ColorWhite, }, Table: TableTheme{ Text: NewStyle(ColorWhite), }, Tab: TabTheme{ Active: NewStyle(ColorRed), Inactive: NewStyle(ColorWhite), }, } termui-3.1.0/v3/utils.go000066400000000000000000000107461351315327100150500ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "fmt" "math" "reflect" rw "github.com/mattn/go-runewidth" wordwrap "github.com/mitchellh/go-wordwrap" ) // InterfaceSlice takes an []interface{} represented as an interface{} and converts it // https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces-in-go func InterfaceSlice(slice interface{}) []interface{} { s := reflect.ValueOf(slice) if s.Kind() != reflect.Slice { panic("InterfaceSlice() given a non-slice type") } ret := make([]interface{}, s.Len()) for i := 0; i < s.Len(); i++ { ret[i] = s.Index(i).Interface() } return ret } // TrimString trims a string to a max length and adds '…' to the end if it was trimmed. func TrimString(s string, w int) string { if w <= 0 { return "" } if rw.StringWidth(s) > w { return rw.Truncate(s, w, string(ELLIPSES)) } return s } func SelectColor(colors []Color, index int) Color { return colors[index%len(colors)] } func SelectStyle(styles []Style, index int) Style { return styles[index%len(styles)] } // Math ------------------------------------------------------------------------ func SumIntSlice(slice []int) int { sum := 0 for _, val := range slice { sum += val } return sum } func SumFloat64Slice(data []float64) float64 { sum := 0.0 for _, v := range data { sum += v } return sum } func GetMaxIntFromSlice(slice []int) (int, error) { if len(slice) == 0 { return 0, fmt.Errorf("cannot get max value from empty slice") } var max int for _, val := range slice { if val > max { max = val } } return max, nil } func GetMaxFloat64FromSlice(slice []float64) (float64, error) { if len(slice) == 0 { return 0, fmt.Errorf("cannot get max value from empty slice") } var max float64 for _, val := range slice { if val > max { max = val } } return max, nil } func GetMaxFloat64From2dSlice(slices [][]float64) (float64, error) { if len(slices) == 0 { return 0, fmt.Errorf("cannot get max value from empty slice") } var max float64 for _, slice := range slices { for _, val := range slice { if val > max { max = val } } } return max, nil } func RoundFloat64(x float64) float64 { return math.Floor(x + 0.5) } func FloorFloat64(x float64) float64 { return math.Floor(x) } func AbsInt(x int) int { if x >= 0 { return x } return -x } func MinFloat64(x, y float64) float64 { if x < y { return x } return y } func MaxFloat64(x, y float64) float64 { if x > y { return x } return y } func MaxInt(x, y int) int { if x > y { return x } return y } func MinInt(x, y int) int { if x < y { return x } return y } // []Cell ---------------------------------------------------------------------- // WrapCells takes []Cell and inserts Cells containing '\n' wherever a linebreak should go. func WrapCells(cells []Cell, width uint) []Cell { str := CellsToString(cells) wrapped := wordwrap.WrapString(str, width) wrappedCells := []Cell{} i := 0 for _, _rune := range wrapped { if _rune == '\n' { wrappedCells = append(wrappedCells, Cell{_rune, StyleClear}) } else { wrappedCells = append(wrappedCells, Cell{_rune, cells[i].Style}) } i++ } return wrappedCells } func RunesToStyledCells(runes []rune, style Style) []Cell { cells := []Cell{} for _, _rune := range runes { cells = append(cells, Cell{_rune, style}) } return cells } func CellsToString(cells []Cell) string { runes := make([]rune, len(cells)) for i, cell := range cells { runes[i] = cell.Rune } return string(runes) } func TrimCells(cells []Cell, w int) []Cell { s := CellsToString(cells) s = TrimString(s, w) runes := []rune(s) newCells := []Cell{} for i, r := range runes { newCells = append(newCells, Cell{r, cells[i].Style}) } return newCells } func SplitCells(cells []Cell, r rune) [][]Cell { splitCells := [][]Cell{} temp := []Cell{} for _, cell := range cells { if cell.Rune == r { splitCells = append(splitCells, temp) temp = []Cell{} } else { temp = append(temp, cell) } } if len(temp) > 0 { splitCells = append(splitCells, temp) } return splitCells } type CellWithX struct { X int Cell Cell } func BuildCellWithXArray(cells []Cell) []CellWithX { cellWithXArray := make([]CellWithX, len(cells)) index := 0 for i, cell := range cells { cellWithXArray[i] = CellWithX{X: index, Cell: cell} index += rw.RuneWidth(cell.Rune) } return cellWithXArray } termui-3.1.0/v3/widgets/000077500000000000000000000000001351315327100150175ustar00rootroot00000000000000termui-3.1.0/v3/widgets/barchart.go000066400000000000000000000042511351315327100171360ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "fmt" "image" rw "github.com/mattn/go-runewidth" . "github.com/gizak/termui/v3" ) type BarChart struct { Block BarColors []Color LabelStyles []Style NumStyles []Style // only Fg and Modifier are used NumFormatter func(float64) string Data []float64 Labels []string BarWidth int BarGap int MaxVal float64 } func NewBarChart() *BarChart { return &BarChart{ Block: *NewBlock(), BarColors: Theme.BarChart.Bars, NumStyles: Theme.BarChart.Nums, LabelStyles: Theme.BarChart.Labels, NumFormatter: func(n float64) string { return fmt.Sprint(n) }, BarGap: 1, BarWidth: 3, } } func (self *BarChart) Draw(buf *Buffer) { self.Block.Draw(buf) maxVal := self.MaxVal if maxVal == 0 { maxVal, _ = GetMaxFloat64FromSlice(self.Data) } barXCoordinate := self.Inner.Min.X for i, data := range self.Data { // draw bar height := int((data / maxVal) * float64(self.Inner.Dy()-1)) for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ { for y := self.Inner.Max.Y - 2; y > (self.Inner.Max.Y-2)-height; y-- { c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, i))) buf.SetCell(c, image.Pt(x, y)) } } // draw label if i < len(self.Labels) { labelXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) - int((float64(rw.StringWidth(self.Labels[i])) / 2)) buf.SetString( self.Labels[i], SelectStyle(self.LabelStyles, i), image.Pt(labelXCoordinate, self.Inner.Max.Y-1), ) } // draw number numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) if numberXCoordinate <= self.Inner.Max.X { buf.SetString( self.NumFormatter(data), NewStyle( SelectStyle(self.NumStyles, i+1).Fg, SelectColor(self.BarColors, i), SelectStyle(self.NumStyles, i+1).Modifier, ), image.Pt(numberXCoordinate, self.Inner.Max.Y-2), ) } barXCoordinate += (self.BarWidth + self.BarGap) } } termui-3.1.0/v3/widgets/gauge.go000066400000000000000000000025511351315327100164410ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "fmt" "image" . "github.com/gizak/termui/v3" ) type Gauge struct { Block Percent int BarColor Color Label string LabelStyle Style } func NewGauge() *Gauge { return &Gauge{ Block: *NewBlock(), BarColor: Theme.Gauge.Bar, LabelStyle: Theme.Gauge.Label, } } func (self *Gauge) Draw(buf *Buffer) { self.Block.Draw(buf) label := self.Label if label == "" { label = fmt.Sprintf("%d%%", self.Percent) } // plot bar barWidth := int((float64(self.Percent) / 100) * float64(self.Inner.Dx())) buf.Fill( NewCell(' ', NewStyle(ColorClear, self.BarColor)), image.Rect(self.Inner.Min.X, self.Inner.Min.Y, self.Inner.Min.X+barWidth, self.Inner.Max.Y), ) // plot label labelXCoordinate := self.Inner.Min.X + (self.Inner.Dx() / 2) - int(float64(len(label))/2) labelYCoordinate := self.Inner.Min.Y + ((self.Inner.Dy() - 1) / 2) if labelYCoordinate < self.Inner.Max.Y { for i, char := range label { style := self.LabelStyle if labelXCoordinate+i+1 <= self.Inner.Min.X+barWidth { style = NewStyle(self.BarColor, ColorClear, ModifierReverse) } buf.SetCell(NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate)) } } } termui-3.1.0/v3/widgets/image.go000066400000000000000000000115401351315327100164310ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" "image/color" . "github.com/gizak/termui/v3" ) type Image struct { Block Image image.Image Monochrome bool MonochromeThreshold uint8 MonochromeInvert bool } func NewImage(img image.Image) *Image { return &Image{ Block: *NewBlock(), MonochromeThreshold: 128, Image: img, } } func (self *Image) Draw(buf *Buffer) { self.Block.Draw(buf) if self.Image == nil { return } bufWidth := self.Inner.Dx() bufHeight := self.Inner.Dy() imageWidth := self.Image.Bounds().Dx() imageHeight := self.Image.Bounds().Dy() if self.Monochrome { if bufWidth > imageWidth/2 { bufWidth = imageWidth / 2 } if bufHeight > imageHeight/2 { bufHeight = imageHeight / 2 } for bx := 0; bx < bufWidth; bx++ { for by := 0; by < bufHeight; by++ { ul := self.colorAverage( 2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2, ) ur := self.colorAverage( (2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, 2*by*imageHeight/bufHeight/2, (2*by+1)*imageHeight/bufHeight/2, ) ll := self.colorAverage( 2*bx*imageWidth/bufWidth/2, (2*bx+1)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2, ) lr := self.colorAverage( (2*bx+1)*imageWidth/bufWidth/2, (2*bx+2)*imageWidth/bufWidth/2, (2*by+1)*imageHeight/bufHeight/2, (2*by+2)*imageHeight/bufHeight/2, ) buf.SetCell( NewCell(blocksChar(ul, ur, ll, lr, self.MonochromeThreshold, self.MonochromeInvert)), image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by), ) } } } else { if bufWidth > imageWidth { bufWidth = imageWidth } if bufHeight > imageHeight { bufHeight = imageHeight } for bx := 0; bx < bufWidth; bx++ { for by := 0; by < bufHeight; by++ { c := self.colorAverage( bx*imageWidth/bufWidth, (bx+1)*imageWidth/bufWidth, by*imageHeight/bufHeight, (by+1)*imageHeight/bufHeight, ) buf.SetCell( NewCell(c.ch(), NewStyle(c.fgColor(), ColorBlack)), image.Pt(self.Inner.Min.X+bx, self.Inner.Min.Y+by), ) } } } } func (self *Image) colorAverage(x0, x1, y0, y1 int) colorAverager { var c colorAverager for x := x0; x < x1; x++ { for y := y0; y < y1; y++ { c = c.add( self.Image.At( x+self.Image.Bounds().Min.X, y+self.Image.Bounds().Min.Y, ), ) } } return c } type colorAverager struct { rsum, gsum, bsum, asum, count uint64 } func (self colorAverager) add(col color.Color) colorAverager { r, g, b, a := col.RGBA() return colorAverager{ rsum: self.rsum + uint64(r), gsum: self.gsum + uint64(g), bsum: self.bsum + uint64(b), asum: self.asum + uint64(a), count: self.count + 1, } } func (self colorAverager) RGBA() (uint32, uint32, uint32, uint32) { if self.count == 0 { return 0, 0, 0, 0 } return uint32(self.rsum/self.count) & 0xffff, uint32(self.gsum/self.count) & 0xffff, uint32(self.bsum/self.count) & 0xffff, uint32(self.asum/self.count) & 0xffff } func (self colorAverager) fgColor() Color { return palette.Convert(self).(paletteColor).attribute } func (self colorAverager) ch() rune { gray := color.GrayModel.Convert(self).(color.Gray).Y switch { case gray < 51: return SHADED_BLOCKS[0] case gray < 102: return SHADED_BLOCKS[1] case gray < 153: return SHADED_BLOCKS[2] case gray < 204: return SHADED_BLOCKS[3] default: return SHADED_BLOCKS[4] } } func (self colorAverager) monochrome(threshold uint8, invert bool) bool { return self.count != 0 && (color.GrayModel.Convert(self).(color.Gray).Y < threshold != invert) } type paletteColor struct { rgba color.RGBA attribute Color } func (self paletteColor) RGBA() (uint32, uint32, uint32, uint32) { return self.rgba.RGBA() } var palette = color.Palette([]color.Color{ paletteColor{color.RGBA{0, 0, 0, 255}, ColorBlack}, paletteColor{color.RGBA{255, 0, 0, 255}, ColorRed}, paletteColor{color.RGBA{0, 255, 0, 255}, ColorGreen}, paletteColor{color.RGBA{255, 255, 0, 255}, ColorYellow}, paletteColor{color.RGBA{0, 0, 255, 255}, ColorBlue}, paletteColor{color.RGBA{255, 0, 255, 255}, ColorMagenta}, paletteColor{color.RGBA{0, 255, 255, 255}, ColorCyan}, paletteColor{color.RGBA{255, 255, 255, 255}, ColorWhite}, }) func blocksChar(ul, ur, ll, lr colorAverager, threshold uint8, invert bool) rune { index := 0 if ul.monochrome(threshold, invert) { index |= 1 } if ur.monochrome(threshold, invert) { index |= 2 } if ll.monochrome(threshold, invert) { index |= 4 } if lr.monochrome(threshold, invert) { index |= 8 } return IRREGULAR_BLOCKS[index] } termui-3.1.0/v3/widgets/list.go000066400000000000000000000065151351315327100163300ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" rw "github.com/mattn/go-runewidth" . "github.com/gizak/termui/v3" ) type List struct { Block Rows []string WrapText bool TextStyle Style SelectedRow int topRow int SelectedRowStyle Style } func NewList() *List { return &List{ Block: *NewBlock(), TextStyle: Theme.List.Text, SelectedRowStyle: Theme.List.Text, } } func (self *List) Draw(buf *Buffer) { self.Block.Draw(buf) point := self.Inner.Min // adjusts view into widget if self.SelectedRow >= self.Inner.Dy()+self.topRow { self.topRow = self.SelectedRow - self.Inner.Dy() + 1 } else if self.SelectedRow < self.topRow { self.topRow = self.SelectedRow } // draw rows for row := self.topRow; row < len(self.Rows) && point.Y < self.Inner.Max.Y; row++ { cells := ParseStyles(self.Rows[row], self.TextStyle) if self.WrapText { cells = WrapCells(cells, uint(self.Inner.Dx())) } for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ { style := cells[j].Style if row == self.SelectedRow { style = self.SelectedRowStyle } if cells[j].Rune == '\n' { point = image.Pt(self.Inner.Min.X, point.Y+1) } else { if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() { buf.SetCell(NewCell(ELLIPSES, style), point.Add(image.Pt(-1, 0))) break } else { buf.SetCell(NewCell(cells[j].Rune, style), point) point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0)) } } } point = image.Pt(self.Inner.Min.X, point.Y+1) } // draw UP_ARROW if needed if self.topRow > 0 { buf.SetCell( NewCell(UP_ARROW, NewStyle(ColorWhite)), image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y), ) } // draw DOWN_ARROW if needed if len(self.Rows) > int(self.topRow)+self.Inner.Dy() { buf.SetCell( NewCell(DOWN_ARROW, NewStyle(ColorWhite)), image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1), ) } } // ScrollAmount scrolls by amount given. If amount is < 0, then scroll up. // There is no need to set self.topRow, as this will be set automatically when drawn, // since if the selected item is off screen then the topRow variable will change accordingly. func (self *List) ScrollAmount(amount int) { if len(self.Rows)-int(self.SelectedRow) <= amount { self.SelectedRow = len(self.Rows) - 1 } else if int(self.SelectedRow)+amount < 0 { self.SelectedRow = 0 } else { self.SelectedRow += amount } } func (self *List) ScrollUp() { self.ScrollAmount(-1) } func (self *List) ScrollDown() { self.ScrollAmount(1) } func (self *List) ScrollPageUp() { // If an item is selected below top row, then go to the top row. if self.SelectedRow > self.topRow { self.SelectedRow = self.topRow } else { self.ScrollAmount(-self.Inner.Dy()) } } func (self *List) ScrollPageDown() { self.ScrollAmount(self.Inner.Dy()) } func (self *List) ScrollHalfPageUp() { self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2))) } func (self *List) ScrollHalfPageDown() { self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2))) } func (self *List) ScrollTop() { self.SelectedRow = 0 } func (self *List) ScrollBottom() { self.SelectedRow = len(self.Rows) - 1 } termui-3.1.0/v3/widgets/paragraph.go000066400000000000000000000016761351315327100173250ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" . "github.com/gizak/termui/v3" ) type Paragraph struct { Block Text string TextStyle Style WrapText bool } func NewParagraph() *Paragraph { return &Paragraph{ Block: *NewBlock(), TextStyle: Theme.Paragraph.Text, WrapText: true, } } func (self *Paragraph) Draw(buf *Buffer) { self.Block.Draw(buf) cells := ParseStyles(self.Text, self.TextStyle) if self.WrapText { cells = WrapCells(cells, uint(self.Inner.Dx())) } rows := SplitCells(cells, '\n') for y, row := range rows { if y+self.Inner.Min.Y >= self.Inner.Max.Y { break } row = TrimCells(row, self.Inner.Dx()) for _, cx := range BuildCellWithXArray(row) { x, cell := cx.X, cx.Cell buf.SetCell(cell, image.Pt(x, y).Add(self.Inner.Min)) } } } termui-3.1.0/v3/widgets/piechart.go000066400000000000000000000073631351315327100171560ustar00rootroot00000000000000package widgets import ( "image" "math" . "github.com/gizak/termui/v3" ) const ( piechartOffsetUp = -.5 * math.Pi // the northward angle resolutionFactor = .0001 // circle resolution: precision vs. performance fullCircle = 2.0 * math.Pi // the full circle angle xStretch = 2.0 // horizontal adjustment ) // PieChartLabel callback type PieChartLabel func(dataIndex int, currentValue float64) string type PieChart struct { Block Data []float64 // list of data items Colors []Color // colors to by cycled through LabelFormatter PieChartLabel // callback function for labels AngleOffset float64 // which angle to start drawing at? (see piechartOffsetUp) } // NewPieChart Creates a new pie chart with reasonable defaults and no labels. func NewPieChart() *PieChart { return &PieChart{ Block: *NewBlock(), Colors: Theme.PieChart.Slices, AngleOffset: piechartOffsetUp, } } func (self *PieChart) Draw(buf *Buffer) { self.Block.Draw(buf) center := self.Inner.Min.Add(self.Inner.Size().Div(2)) radius := MinFloat64(float64(self.Inner.Dx()/2/xStretch), float64(self.Inner.Dy()/2)) // compute slice sizes sum := SumFloat64Slice(self.Data) sliceSizes := make([]float64, len(self.Data)) for i, v := range self.Data { sliceSizes[i] = v / sum * fullCircle } borderCircle := &circle{center, radius} middleCircle := circle{Point: center, radius: radius / 2.0} // draw sectors phi := self.AngleOffset for i, size := range sliceSizes { for j := 0.0; j < size; j += resolutionFactor { borderPoint := borderCircle.at(phi + j) line := line{P1: center, P2: borderPoint} line.draw(NewCell(SHADED_BLOCKS[1], NewStyle(SelectColor(self.Colors, i))), buf) } phi += size } // draw labels if self.LabelFormatter != nil { phi = self.AngleOffset for i, size := range sliceSizes { labelPoint := middleCircle.at(phi + size/2.0) if len(self.Data) == 1 { labelPoint = center } buf.SetString( self.LabelFormatter(i, self.Data[i]), NewStyle(SelectColor(self.Colors, i)), image.Pt(labelPoint.X, labelPoint.Y), ) phi += size } } } type circle struct { image.Point radius float64 } // computes the point at a given angle phi func (self circle) at(phi float64) image.Point { x := self.X + int(RoundFloat64(xStretch*self.radius*math.Cos(phi))) y := self.Y + int(RoundFloat64(self.radius*math.Sin(phi))) return image.Point{X: x, Y: y} } // computes the perimeter of a circle func (self circle) perimeter() float64 { return 2.0 * math.Pi * self.radius } // a line between two points type line struct { P1, P2 image.Point } // draws the line func (self line) draw(cell Cell, buf *Buffer) { isLeftOf := func(p1, p2 image.Point) bool { return p1.X <= p2.X } isTopOf := func(p1, p2 image.Point) bool { return p1.Y <= p2.Y } p1, p2 := self.P1, self.P2 buf.SetCell(NewCell('*', cell.Style), self.P2) width, height := self.size() if width > height { // paint left to right if !isLeftOf(p1, p2) { p1, p2 = p2, p1 } flip := 1.0 if !isTopOf(p1, p2) { flip = -1.0 } for x := p1.X; x <= p2.X; x++ { ratio := float64(height) / float64(width) factor := float64(x - p1.X) y := ratio * factor * flip buf.SetCell(cell, image.Pt(x, int(RoundFloat64(y))+p1.Y)) } } else { // paint top to bottom if !isTopOf(p1, p2) { p1, p2 = p2, p1 } flip := 1.0 if !isLeftOf(p1, p2) { flip = -1.0 } for y := p1.Y; y <= p2.Y; y++ { ratio := float64(width) / float64(height) factor := float64(y - p1.Y) x := ratio * factor * flip buf.SetCell(cell, image.Pt(int(RoundFloat64(x))+p1.X, y)) } } } // width and height of a line func (self line) size() (w, h int) { return AbsInt(self.P2.X - self.P1.X), AbsInt(self.P2.Y - self.P1.Y) } termui-3.1.0/v3/widgets/plot.go000066400000000000000000000126661351315327100163370ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "fmt" "image" . "github.com/gizak/termui/v3" ) // Plot has two modes: line(default) and scatter. // Plot also has two marker types: braille(default) and dot. // A single braille character is a 2x4 grid of dots, so using braille // gives 2x X resolution and 4x Y resolution over dot mode. type Plot struct { Block Data [][]float64 DataLabels []string MaxVal float64 LineColors []Color AxesColor Color // TODO ShowAxes bool Marker PlotMarker DotMarkerRune rune PlotType PlotType HorizontalScale int DrawDirection DrawDirection // TODO } const ( xAxisLabelsHeight = 1 yAxisLabelsWidth = 4 xAxisLabelsGap = 2 yAxisLabelsGap = 1 ) type PlotType uint const ( LineChart PlotType = iota ScatterPlot ) type PlotMarker uint const ( MarkerBraille PlotMarker = iota MarkerDot ) type DrawDirection uint const ( DrawLeft DrawDirection = iota DrawRight ) func NewPlot() *Plot { return &Plot{ Block: *NewBlock(), LineColors: Theme.Plot.Lines, AxesColor: Theme.Plot.Axes, Marker: MarkerBraille, DotMarkerRune: DOT, Data: [][]float64{}, HorizontalScale: 1, DrawDirection: DrawRight, ShowAxes: true, PlotType: LineChart, } } func (self *Plot) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) { canvas := NewCanvas() canvas.Rectangle = drawArea switch self.PlotType { case ScatterPlot: for i, line := range self.Data { for j, val := range line { height := int((val / maxVal) * float64(drawArea.Dy()-1)) canvas.SetPoint( image.Pt( (drawArea.Min.X+(j*self.HorizontalScale))*2, (drawArea.Max.Y-height-1)*4, ), SelectColor(self.LineColors, i), ) } } case LineChart: for i, line := range self.Data { previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1)) for j, val := range line[1:] { height := int((val / maxVal) * float64(drawArea.Dy()-1)) canvas.SetLine( image.Pt( (drawArea.Min.X+(j*self.HorizontalScale))*2, (drawArea.Max.Y-previousHeight-1)*4, ), image.Pt( (drawArea.Min.X+((j+1)*self.HorizontalScale))*2, (drawArea.Max.Y-height-1)*4, ), SelectColor(self.LineColors, i), ) previousHeight = height } } } canvas.Draw(buf) } func (self *Plot) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) { switch self.PlotType { case ScatterPlot: for i, line := range self.Data { for j, val := range line { height := int((val / maxVal) * float64(drawArea.Dy()-1)) point := image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height) if point.In(drawArea) { buf.SetCell( NewCell(self.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))), point, ) } } } case LineChart: for i, line := range self.Data { for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ { val := line[j] height := int((val / maxVal) * float64(drawArea.Dy()-1)) buf.SetCell( NewCell(self.DotMarkerRune, NewStyle(SelectColor(self.LineColors, i))), image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height), ) } } } } func (self *Plot) plotAxes(buf *Buffer, maxVal float64) { // draw origin cell buf.SetCell( NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)), image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1), ) // draw x axis line for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ { buf.SetCell( NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)), image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1), ) } // draw y axis line for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ { buf.SetCell( NewCell(VERTICAL_DASH, NewStyle(ColorWhite)), image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y), ) } // draw x axis labels // draw 0 buf.SetString( "0", NewStyle(ColorWhite), image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-1), ) // draw rest for x := self.Inner.Min.X + yAxisLabelsWidth + (xAxisLabelsGap)*self.HorizontalScale + 1; x < self.Inner.Max.X-1; { label := fmt.Sprintf( "%d", (x-(self.Inner.Min.X+yAxisLabelsWidth)-1)/(self.HorizontalScale)+1, ) buf.SetString( label, NewStyle(ColorWhite), image.Pt(x, self.Inner.Max.Y-1), ) x += (len(label) + xAxisLabelsGap) * self.HorizontalScale } // draw y axis labels verticalScale := maxVal / float64(self.Inner.Dy()-xAxisLabelsHeight-1) for i := 0; i*(yAxisLabelsGap+1) < self.Inner.Dy()-1; i++ { buf.SetString( fmt.Sprintf("%.2f", float64(i)*verticalScale*(yAxisLabelsGap+1)), NewStyle(ColorWhite), image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2), ) } } func (self *Plot) Draw(buf *Buffer) { self.Block.Draw(buf) maxVal := self.MaxVal if maxVal == 0 { maxVal, _ = GetMaxFloat64From2dSlice(self.Data) } if self.ShowAxes { self.plotAxes(buf, maxVal) } drawArea := self.Inner if self.ShowAxes { drawArea = image.Rect( self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y, self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1, ) } switch self.Marker { case MarkerBraille: self.renderBraille(buf, drawArea, maxVal) case MarkerDot: self.renderDot(buf, drawArea, maxVal) } } termui-3.1.0/v3/widgets/sparkline.go000066400000000000000000000043741351315327100173460ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" . "github.com/gizak/termui/v3" ) // Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers. type Sparkline struct { Data []float64 Title string TitleStyle Style LineColor Color MaxVal float64 MaxHeight int // TODO } // SparklineGroup is a renderable widget which groups together the given sparklines. type SparklineGroup struct { Block Sparklines []*Sparkline } // NewSparkline returns a unrenderable single sparkline that needs to be added to a SparklineGroup func NewSparkline() *Sparkline { return &Sparkline{ TitleStyle: Theme.Sparkline.Title, LineColor: Theme.Sparkline.Line, } } func NewSparklineGroup(sls ...*Sparkline) *SparklineGroup { return &SparklineGroup{ Block: *NewBlock(), Sparklines: sls, } } func (self *SparklineGroup) Draw(buf *Buffer) { self.Block.Draw(buf) sparklineHeight := self.Inner.Dy() / len(self.Sparklines) for i, sl := range self.Sparklines { heightOffset := (sparklineHeight * (i + 1)) barHeight := sparklineHeight if i == len(self.Sparklines)-1 { heightOffset = self.Inner.Dy() barHeight = self.Inner.Dy() - (sparklineHeight * i) } if sl.Title != "" { barHeight-- } maxVal := sl.MaxVal if maxVal == 0 { maxVal, _ = GetMaxFloat64FromSlice(sl.Data) } // draw line for j := 0; j < len(sl.Data) && j < self.Inner.Dx(); j++ { data := sl.Data[j] height := int((data / maxVal) * float64(barHeight)) sparkChar := BARS[len(BARS)-1] for k := 0; k < height; k++ { buf.SetCell( NewCell(sparkChar, NewStyle(sl.LineColor)), image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-k), ) } if height == 0 { sparkChar = BARS[1] buf.SetCell( NewCell(sparkChar, NewStyle(sl.LineColor)), image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset), ) } } if sl.Title != "" { // draw title buf.SetString( TrimString(sl.Title, self.Inner.Dx()), sl.TitleStyle, image.Pt(self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-barHeight), ) } } } termui-3.1.0/v3/widgets/stacked_barchart.go000066400000000000000000000047401351315327100206370ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "fmt" "image" rw "github.com/mattn/go-runewidth" . "github.com/gizak/termui/v3" ) type StackedBarChart struct { Block BarColors []Color LabelStyles []Style NumStyles []Style // only Fg and Modifier are used NumFormatter func(float64) string Data [][]float64 Labels []string BarWidth int BarGap int MaxVal float64 } func NewStackedBarChart() *StackedBarChart { return &StackedBarChart{ Block: *NewBlock(), BarColors: Theme.StackedBarChart.Bars, LabelStyles: Theme.StackedBarChart.Labels, NumStyles: Theme.StackedBarChart.Nums, NumFormatter: func(n float64) string { return fmt.Sprint(n) }, BarGap: 1, BarWidth: 3, } } func (self *StackedBarChart) Draw(buf *Buffer) { self.Block.Draw(buf) maxVal := self.MaxVal if maxVal == 0 { for _, data := range self.Data { maxVal = MaxFloat64(maxVal, SumFloat64Slice(data)) } } barXCoordinate := self.Inner.Min.X for i, bar := range self.Data { // draw stacked bars stackedBarYCoordinate := 0 for j, data := range bar { // draw each stacked bar height := int((data / maxVal) * float64(self.Inner.Dy()-1)) for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ { for y := (self.Inner.Max.Y - 2) - stackedBarYCoordinate; y > (self.Inner.Max.Y-2)-stackedBarYCoordinate-height; y-- { c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, j))) buf.SetCell(c, image.Pt(x, y)) } } // draw number numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) - 1 buf.SetString( self.NumFormatter(data), NewStyle( SelectStyle(self.NumStyles, j+1).Fg, SelectColor(self.BarColors, j), SelectStyle(self.NumStyles, j+1).Modifier, ), image.Pt(numberXCoordinate, (self.Inner.Max.Y-2)-stackedBarYCoordinate), ) stackedBarYCoordinate += height } // draw label if i < len(self.Labels) { labelXCoordinate := barXCoordinate + MaxInt( int((float64(self.BarWidth)/2))-int((float64(rw.StringWidth(self.Labels[i]))/2)), 0, ) buf.SetString( TrimString(self.Labels[i], self.BarWidth), SelectStyle(self.LabelStyles, i), image.Pt(labelXCoordinate, self.Inner.Max.Y-1), ) } barXCoordinate += (self.BarWidth + self.BarGap) } } termui-3.1.0/v3/widgets/table.go000066400000000000000000000106721351315327100164430ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" . "github.com/gizak/termui/v3" ) /*Table is like: ┌ Awesome Table ───────────────────────────────────────────────┐ │ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 | │──────────────────────────────────────────────────────────────│ │ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII | │──────────────────────────────────────────────────────────────│ │ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ | └──────────────────────────────────────────────────────────────┘ */ type Table struct { Block Rows [][]string ColumnWidths []int TextStyle Style RowSeparator bool TextAlignment Alignment RowStyles map[int]Style FillRow bool // ColumnResizer is called on each Draw. Can be used for custom column sizing. ColumnResizer func() } func NewTable() *Table { return &Table{ Block: *NewBlock(), TextStyle: Theme.Table.Text, RowSeparator: true, RowStyles: make(map[int]Style), ColumnResizer: func() {}, } } func (self *Table) Draw(buf *Buffer) { self.Block.Draw(buf) self.ColumnResizer() columnWidths := self.ColumnWidths if len(columnWidths) == 0 { columnCount := len(self.Rows[0]) columnWidth := self.Inner.Dx() / columnCount for i := 0; i < columnCount; i++ { columnWidths = append(columnWidths, columnWidth) } } yCoordinate := self.Inner.Min.Y // draw rows for i := 0; i < len(self.Rows) && yCoordinate < self.Inner.Max.Y; i++ { row := self.Rows[i] colXCoordinate := self.Inner.Min.X rowStyle := self.TextStyle // get the row style if one exists if style, ok := self.RowStyles[i]; ok { rowStyle = style } if self.FillRow { blankCell := NewCell(' ', rowStyle) buf.Fill(blankCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1)) } // draw row cells for j := 0; j < len(row); j++ { col := ParseStyles(row[j], rowStyle) // draw row cell if len(col) > columnWidths[j] || self.TextAlignment == AlignLeft { for _, cx := range BuildCellWithXArray(col) { k, cell := cx.X, cx.Cell if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X { cell.Rune = ELLIPSES buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate)) break } else { buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate)) } } } else if self.TextAlignment == AlignCenter { xCoordinateOffset := (columnWidths[j] - len(col)) / 2 stringXCoordinate := xCoordinateOffset + colXCoordinate for _, cx := range BuildCellWithXArray(col) { k, cell := cx.X, cx.Cell buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate)) } } else if self.TextAlignment == AlignRight { stringXCoordinate := MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col) for _, cx := range BuildCellWithXArray(col) { k, cell := cx.X, cx.Cell buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate)) } } colXCoordinate += columnWidths[j] + 1 } // draw vertical separators separatorStyle := self.Block.BorderStyle separatorXCoordinate := self.Inner.Min.X verticalCell := NewCell(VERTICAL_LINE, separatorStyle) for i, width := range columnWidths { if self.FillRow && i < len(columnWidths)-1 { verticalCell.Style.Bg = rowStyle.Bg } else { verticalCell.Style.Bg = self.Block.BorderStyle.Bg } separatorXCoordinate += width buf.SetCell(verticalCell, image.Pt(separatorXCoordinate, yCoordinate)) separatorXCoordinate++ } yCoordinate++ // draw horizontal separator horizontalCell := NewCell(HORIZONTAL_LINE, separatorStyle) if self.RowSeparator && yCoordinate < self.Inner.Max.Y && i != len(self.Rows)-1 { buf.Fill(horizontalCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1)) yCoordinate++ } } } termui-3.1.0/v3/widgets/tabs.go000066400000000000000000000031211351315327100162740ustar00rootroot00000000000000// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package widgets import ( "image" . "github.com/gizak/termui/v3" ) // TabPane is a renderable widget which can be used to conditionally render certain tabs/views. // TabPane shows a list of Tab names. // The currently selected tab can be found through the `ActiveTabIndex` field. type TabPane struct { Block TabNames []string ActiveTabIndex int ActiveTabStyle Style InactiveTabStyle Style } func NewTabPane(names ...string) *TabPane { return &TabPane{ Block: *NewBlock(), TabNames: names, ActiveTabStyle: Theme.Tab.Active, InactiveTabStyle: Theme.Tab.Inactive, } } func (self *TabPane) FocusLeft() { if self.ActiveTabIndex > 0 { self.ActiveTabIndex-- } } func (self *TabPane) FocusRight() { if self.ActiveTabIndex < len(self.TabNames)-1 { self.ActiveTabIndex++ } } func (self *TabPane) Draw(buf *Buffer) { self.Block.Draw(buf) xCoordinate := self.Inner.Min.X for i, name := range self.TabNames { ColorPair := self.InactiveTabStyle if i == self.ActiveTabIndex { ColorPair = self.ActiveTabStyle } buf.SetString( TrimString(name, self.Inner.Max.X-xCoordinate), ColorPair, image.Pt(xCoordinate, self.Inner.Min.Y), ) xCoordinate += 1 + len(name) if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X { buf.SetCell( NewCell(VERTICAL_LINE, NewStyle(ColorWhite)), image.Pt(xCoordinate, self.Inner.Min.Y), ) } xCoordinate += 2 } } termui-3.1.0/v3/widgets/tree.go000066400000000000000000000130331351315327100163050ustar00rootroot00000000000000package widgets import ( "fmt" "image" "strings" . "github.com/gizak/termui/v3" rw "github.com/mattn/go-runewidth" ) const treeIndent = " " // TreeNode is a tree node. type TreeNode struct { Value fmt.Stringer Expanded bool Nodes []*TreeNode // level stores the node level in the tree. level int } // TreeWalkFn is a function used for walking a Tree. // To interrupt the walking process function should return false. type TreeWalkFn func(*TreeNode) bool func (self *TreeNode) parseStyles(style Style) []Cell { var sb strings.Builder if len(self.Nodes) == 0 { sb.WriteString(strings.Repeat(treeIndent, self.level+1)) } else { sb.WriteString(strings.Repeat(treeIndent, self.level)) if self.Expanded { sb.WriteRune(Theme.Tree.Expanded) } else { sb.WriteRune(Theme.Tree.Collapsed) } sb.WriteByte(' ') } sb.WriteString(self.Value.String()) return ParseStyles(sb.String(), style) } // Tree is a tree widget. type Tree struct { Block TextStyle Style SelectedRowStyle Style WrapText bool SelectedRow int nodes []*TreeNode // rows is flatten nodes for rendering. rows []*TreeNode topRow int } // NewTree creates a new Tree widget. func NewTree() *Tree { return &Tree{ Block: *NewBlock(), TextStyle: Theme.Tree.Text, SelectedRowStyle: Theme.Tree.Text, WrapText: true, } } func (self *Tree) SetNodes(nodes []*TreeNode) { self.nodes = nodes self.prepareNodes() } func (self *Tree) prepareNodes() { self.rows = make([]*TreeNode, 0) for _, node := range self.nodes { self.prepareNode(node, 0) } } func (self *Tree) prepareNode(node *TreeNode, level int) { self.rows = append(self.rows, node) node.level = level if node.Expanded { for _, n := range node.Nodes { self.prepareNode(n, level+1) } } } func (self *Tree) Walk(fn TreeWalkFn) { for _, n := range self.nodes { if !self.walk(n, fn) { break } } } func (self *Tree) walk(n *TreeNode, fn TreeWalkFn) bool { if !fn(n) { return false } for _, node := range n.Nodes { if !self.walk(node, fn) { return false } } return true } func (self *Tree) Draw(buf *Buffer) { self.Block.Draw(buf) point := self.Inner.Min // adjusts view into widget if self.SelectedRow >= self.Inner.Dy()+self.topRow { self.topRow = self.SelectedRow - self.Inner.Dy() + 1 } else if self.SelectedRow < self.topRow { self.topRow = self.SelectedRow } // draw rows for row := self.topRow; row < len(self.rows) && point.Y < self.Inner.Max.Y; row++ { cells := self.rows[row].parseStyles(self.TextStyle) if self.WrapText { cells = WrapCells(cells, uint(self.Inner.Dx())) } for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ { style := cells[j].Style if row == self.SelectedRow { style = self.SelectedRowStyle } if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() { buf.SetCell(NewCell(ELLIPSES, style), point.Add(image.Pt(-1, 0))) } else { buf.SetCell(NewCell(cells[j].Rune, style), point) point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0)) } } point = image.Pt(self.Inner.Min.X, point.Y+1) } // draw UP_ARROW if needed if self.topRow > 0 { buf.SetCell( NewCell(UP_ARROW, NewStyle(ColorWhite)), image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y), ) } // draw DOWN_ARROW if needed if len(self.rows) > int(self.topRow)+self.Inner.Dy() { buf.SetCell( NewCell(DOWN_ARROW, NewStyle(ColorWhite)), image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1), ) } } // ScrollAmount scrolls by amount given. If amount is < 0, then scroll up. // There is no need to set self.topRow, as this will be set automatically when drawn, // since if the selected item is off screen then the topRow variable will change accordingly. func (self *Tree) ScrollAmount(amount int) { if len(self.rows)-int(self.SelectedRow) <= amount { self.SelectedRow = len(self.rows) - 1 } else if int(self.SelectedRow)+amount < 0 { self.SelectedRow = 0 } else { self.SelectedRow += amount } } func (self *Tree) SelectedNode() *TreeNode { if len(self.rows) == 0 { return nil } return self.rows[self.SelectedRow] } func (self *Tree) ScrollUp() { self.ScrollAmount(-1) } func (self *Tree) ScrollDown() { self.ScrollAmount(1) } func (self *Tree) ScrollPageUp() { // If an item is selected below top row, then go to the top row. if self.SelectedRow > self.topRow { self.SelectedRow = self.topRow } else { self.ScrollAmount(-self.Inner.Dy()) } } func (self *Tree) ScrollPageDown() { self.ScrollAmount(self.Inner.Dy()) } func (self *Tree) ScrollHalfPageUp() { self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2))) } func (self *Tree) ScrollHalfPageDown() { self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2))) } func (self *Tree) ScrollTop() { self.SelectedRow = 0 } func (self *Tree) ScrollBottom() { self.SelectedRow = len(self.rows) - 1 } func (self *Tree) Collapse() { self.rows[self.SelectedRow].Expanded = false self.prepareNodes() } func (self *Tree) Expand() { node := self.rows[self.SelectedRow] if len(node.Nodes) > 0 { self.rows[self.SelectedRow].Expanded = true } self.prepareNodes() } func (self *Tree) ToggleExpand() { node := self.rows[self.SelectedRow] if len(node.Nodes) > 0 { node.Expanded = !node.Expanded } self.prepareNodes() } func (self *Tree) ExpandAll() { self.Walk(func(n *TreeNode) bool { if len(n.Nodes) > 0 { n.Expanded = true } return true }) self.prepareNodes() } func (self *Tree) CollapseAll() { self.Walk(func(n *TreeNode) bool { n.Expanded = false return true }) self.prepareNodes() }