├── README.md ├── examples ├── demo.nim └── simple.nim ├── illwillWidgets.nimble ├── src └── illwillWidgets.nim └── tests ├── table.nim ├── tdebug.nim └── utils └── tWrapText.nim /README.md: -------------------------------------------------------------------------------- 1 | # illwillWidgets 2 | mouse enabled widget library for [illwill](https://github.com/johnnovak/illwill)! 3 | 4 | [![asciicast](https://asciinema.org/a/GO4nhu01AItIrS1ihBoPWlMeH.svg)](https://asciinema.org/a/GO4nhu01AItIrS1ihBoPWlMeH) 5 | 6 | OS Support 7 | ========== 8 | 9 | linux, windows, macos 10 | 11 | Widgets 12 | ======= 13 | 14 | - buttons 15 | - checkbox 16 | - radiobox 17 | - listview 18 | - (simple) textbox 19 | - "info" box (like a status bar) 20 | - progressbar / slider 21 | - table w/ fixed header 22 | example 23 | ======= 24 | 25 | ```nim 26 | import illwill 27 | import illwillWidgets 28 | import os 29 | 30 | 31 | # default illwill boilerplate 32 | proc exitProc() {.noconv.} = 33 | illwillDeinit() 34 | showCursor() 35 | quit(0) 36 | 37 | illwillInit(fullscreen=true, mouse=true) # <- enable mouse support 38 | setControlCHook(exitProc) 39 | hideCursor() 40 | 41 | var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) 42 | 43 | # Create widgets 44 | var infoBox = newInfoBox("",0 ,0, terminalWidth()) 45 | var btnFill = newButton("fill", 38, 3, 15, 2) 46 | var btnClear = newButton("clear", 38, 6, 15, 2) 47 | 48 | while true: 49 | 50 | var key = getKey() 51 | 52 | # Test if the key signals a mouse event. 53 | if key == Key.Mouse: 54 | 55 | # get the mouse information from illwill 56 | var mi: MouseInfo = getMouse() 57 | 58 | # to catch the widgets events 59 | var ev: Events 60 | 61 | # we must call dispatch on every widget. 62 | # dispatch returns events that the widget fires. 63 | ev = tb.dispatch(btnFill, mi) 64 | if ev.contains MouseUp: 65 | # do stuff when mouse button releases 66 | tb.clear("A") 67 | if ev.contains MouseHover: 68 | # do stuff when mouse hover over element 69 | infoBox.text = "Fills the screen!!" 70 | 71 | # Another button to clear the screen again 72 | ev = tb.dispatch(btnClear, mi) 73 | if ev.contains MouseUp: 74 | # do stuff when mouse button releases 75 | tb.clear() 76 | if ev.contains MouseHover: 77 | # do stuff when mouse hover over element 78 | infoBox.text = "clear the screen" 79 | 80 | 81 | # After dispatching we need to actually render the element 82 | # into the terminal buffer 83 | tb.render(btnFill) 84 | tb.render(btnClear) 85 | tb.render(infoBox) 86 | 87 | # Draw the terminal buffer to screen. 88 | tb.display() 89 | 90 | sleep(25) 91 | 92 | ``` 93 | 94 | more examples 95 | ============= 96 | 97 | for a complete example see [examples/demo.nim](examples/demo.nim) 98 | 99 | 100 | changelog 101 | ========= 102 | - 0.1.9 103 | - Added `TableBox` to render a table of data w/ a fixed header 104 | - 0.1.8 105 | - Elements of a radio group box are pointer now. Before they where copied into the element. 106 | - radio group box: `uncheckAll` 107 | - `positionHelper(coords: MouseInfo): string` returns the mouse position absolute and from the edges 108 | - progress bar does hightlight text background 109 | - colorText: fgColor # text color when not done 110 | - colorTextDone: fgColor # text color when done 111 | -------------------------------------------------------------------------------- /examples/demo.nim: -------------------------------------------------------------------------------- 1 | import ../src/illwillWidgets 2 | import illwill 3 | import strformat, strutils 4 | import asyncdispatch, httpclient # for async demonstration 5 | 6 | proc exitProc() {.noconv.} = 7 | illwillDeinit() 8 | showCursor() 9 | quit(0) 10 | 11 | illwillInit(fullscreen=true, mouse=true) 12 | setControlCHook(exitProc) 13 | hideCursor() 14 | 15 | # Button 16 | var btnAsyncHttp = newButton("async http", 21, 3, 15, 2) 17 | var btnTest = newButton("fill", 38, 3, 15, 2) 18 | var btnClear = newButton("clear", 38, 6, 15, 2) 19 | var btnNoBorder = newButton("no border", 21, 7, 15, 1, border = false) 20 | 21 | # Checkbox 22 | var chkTest = newCheckbox("Some content", 38, 9) 23 | var chkTest2 = newCheckbox("Fill Color", 38, 10) 24 | chkTest2.textUnchecked = ":) " 25 | chkTest2.textChecked = ":( " 26 | 27 | var chkDraw = newCheckbox("Draw?", 38, 11) 28 | 29 | # Info box 30 | var infoBox = newInfoBox("", 0 ,0, terminalWidth()) 31 | var infoBoxMouse = newInfoBox("", 0 ,0, terminalWidth()) 32 | var infoBoxAsync = newInfoBox("", 0 ,1, terminalWidth()) 33 | var infoBoxMulti = newInfoBox("", 93 ,3, 25, 10) 34 | infoBoxMulti.bgcolor = bgYellow 35 | 36 | # Radio buttons 37 | var chkRadA = newRadioBox("Radio Box Option A", 56, 4) 38 | chkRadA.checked = true 39 | var chkRadB = newRadioBox("Radio Box Option B", 56, 5) 40 | var chkRadC = newRadioBox("Radio Box Option C", 56, 6) 41 | var radioBoxGroup = newRadioBoxGroup(@[ 42 | addr chkRadA, addr chkRadB, addr chkRadC 43 | ]) 44 | 45 | var chooseBox = newChooseBox(@[" ", "#", "@", "§"], 81, 3, 10, 5, choosenidx=2) 46 | 47 | var textBox = newTextBox("foo", 38, 13, 42, placeholder = "Some placeholder") 48 | 49 | var progressBarAsync = newProgressBar("some text", 18, 15, 100, 0.0, 50.0) 50 | var infoProgressBarAsync = newInfoBox("", 18, 16, 100) 51 | var progressBarInteract = newProgressBar("some text", 18, 18, 50, 0.0, 50.0, bgDone = bgBlue, bgTodo = bgWhite) 52 | 53 | # TODO not working properly yet 54 | var progressBarVertical = newProgressBar("some text", 2, 5, 10, 0.0, 50.0, Vertical) 55 | progressBarVertical.value = 50.0 56 | 57 | var infoProgressBarInteract = newInfoBox("", 18, 19, 50) 58 | var labelProgressBarInteract = newInfoBox("<-- interact with me! (left, right click, scroll)", 70, 18, 50, bgcolor = bgBlack, color = fgWhite) 59 | 60 | var table = newTableBox(1, 20, 40, 6) 61 | table.addCol("#", 2) 62 | table.addCol("Name", 10) 63 | table.addCol("Age", 4) 64 | table.addCol("Notes", 40) 65 | table.addRow(@["1", "John Doe", "40", "Most Wanted"]) 66 | table.addRow(@["2", "Jane Doe", "38", "Missing"]) 67 | 68 | proc asyncDemo(): Future[void] {.async.} = 69 | var idx = 0 70 | while true: 71 | idx.inc 72 | infoBoxAsync.text = "Async Demo: " & $idx 73 | progressBarAsync.value = (idx mod progressBarAsync.maxValue.int).float 74 | progressBarVertical.value = (idx mod progressBarAsync.maxValue.int).float 75 | infoProgressBarAsync.text = 76 | fmt"{progressBarAsync.value}/{progressBarAsync.maxValue} percent: {progressBarAsync.percent}" 77 | # echo progressBar.value 78 | await sleepAsync(1000) 79 | asyncCheck asyncDemo() 80 | 81 | proc httpCall(tb: ptr TerminalBuffer): Future[void] {.async.} = 82 | var client = newHttpClient() 83 | tb[].write(22, 6, $client.getContent("http://ip.code0.xyz").strip()) 84 | 85 | proc dumpMi(tb: var TerminalBuffer, mi: MouseInfo) = 86 | infoBoxMulti.text = (repr mi) #.split("\n") 87 | # var idx = 0 88 | # for line in (repr mi).split("\n"): 89 | # tb.write 93, 3 + idx, bgYellow, fgBlack, $line.alignLeft(25), resetStyle 90 | # idx.inc 91 | 92 | proc funDraw(tb: var TerminalBuffer, mi: MouseInfo) = 93 | tb.write resetStyle 94 | if mi.action == mbaPressed: 95 | case mi.button 96 | of mbLeft: 97 | tb.write mi.x, mi.y, fgRed, "♥" 98 | of mbMiddle: 99 | tb.write mi.x, mi.y, fgBlue, "◉" 100 | of mbRight: 101 | tb.write mi.x, mi.y, fgCyan, "#" 102 | else: discard 103 | elif mi.action == mbaReleased: 104 | tb.write mi.x, mi.y, fgGreen, "⌀" 105 | 106 | var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) 107 | 108 | while true: 109 | ## When terminal size change create new buffer to reflect size change 110 | if tb.width != terminalWidth() or tb.height != terminalHeight(): 111 | tb = newTerminalBuffer(terminalWidth(), terminalHeight()) 112 | 113 | var key = getKey() 114 | 115 | # Must be done for every textbox 116 | if textBox.focus: 117 | if tb.handleKey(textBox, key): 118 | tb.write(0,2, bgYellow, fgBlue, textBox.text) 119 | chooseBox.add(textBox.text) 120 | key.setKeyAsHandled() # If the key input was handled by the textbox 121 | 122 | case key 123 | of Key.None: discard 124 | of Key.Escape, Key.Q: exitProc() 125 | of Key.Mouse: #, Key.None: # TODO Key.None here does not work with checkbox 126 | let coords = getMouse() 127 | infoBoxMouse.text = $coords 128 | var ev: Events 129 | 130 | ev = tb.dispatch(btnAsyncHttp, coords) 131 | if ev.contains MouseUp: 132 | asyncCheck httpCall(addr tb) # the `addr` here is just for demo purpose, not recommend 133 | if ev.contains MouseHover: 134 | infoBox.text = "http call to http://ip.code0.xyz to get your public ip!" 135 | 136 | ev = tb.dispatch(btnNoBorder, coords) 137 | # if ev.contains MouseUp: 138 | # asyncCheck httpCall(addr tb) # the `addr` here is just for demo purpose, not recommend 139 | # if ev.contains MouseHover: 140 | # infoBox.text = "http call to http://ip.code0.xyz to get your public ip!" 141 | 142 | 143 | ev = tb.dispatch(btnClear, coords) 144 | if ev.contains MouseUp: 145 | tb.clear() 146 | tb.write resetStyle 147 | if ev.contains MouseHover: 148 | infoBox.text = "CLEARS THE SCREEN!" 149 | 150 | ev = tb.dispatch(btnTest, coords) 151 | if ev.contains MouseUp: 152 | tb.clear(chooseBox.element) 153 | tb.write resetStyle 154 | if ev.contains MouseHover: 155 | infoBox.text = "Fills the screen!!" 156 | 157 | ev = tb.dispatch(chkTest, coords) 158 | if ev.contains MouseUp: 159 | infoBox.text = "chk test is: " & $chkTest.checked 160 | 161 | ev = tb.dispatch(chkTest2, coords) 162 | if ev.contains MouseUp: 163 | if chkTest2.checked: 164 | infoBox.text = "red" 165 | tb.setForegroundColor fgRed 166 | chkTest2.color = fgRed 167 | else: 168 | infoBox.text = "green" 169 | chkTest2.color = fgGreen 170 | tb.setForegroundColor fgGreen 171 | 172 | ev = tb.dispatch(chkDraw, coords) 173 | if ev.contains MouseHover: 174 | infoBox.text = "Enables/Disables drawing" 175 | # We enable / disable drawing based on checkbox value 176 | if chkDraw.checked: 177 | tb.funDraw(coords) 178 | 179 | ev = tb.dispatch(infoBox, coords) 180 | if ev.contains MouseDown: 181 | infoBox.text &= "#" 182 | if ev.contains MouseUp: 183 | infoBox.text &= "^" 184 | 185 | ev = tb.dispatch(chooseBox, coords) 186 | if ev.contains MouseUp: 187 | infoBox.text = fmt"Choose box choosenidx: {chooseBox.choosenidx} -> {chooseBox.element()}" 188 | 189 | # Textbox is special! (see above for `handleKey`) 190 | ev = tb.dispatch(textBox, coords) 191 | 192 | # RadioBoxGroup dispatches to all group members 193 | ev = tb.dispatch(radioBoxGroup, coords) 194 | if ev.contains MouseUp: 195 | infoBox.text = fmt"Radio button with content '{radioBoxGroup.element().text}' selected." 196 | 197 | ev = tb.dispatch(progressBarAsync, coords) 198 | if ev.contains MouseHover: 199 | infoBox.text = "i get filled by the async code!" 200 | 201 | ev = tb.dispatch(progressBarVertical, coords) # does nothing; is stupid 202 | if ev.contains MouseDown: 203 | if coords.button == mbLeft: 204 | progressBarVertical.value = progressBarVertical.valueOnPos(coords) 205 | infoBox.text = $progressBarVertical.value 206 | 207 | 208 | ev = tb.dispatch(progressBarInteract, coords) 209 | if ev.contains MouseHover: 210 | infoBox.text = "Interactive progress bar! (left, right click or scroll)" 211 | if coords.scroll: 212 | if coords.scrollDir == sdUp: 213 | progressBarInteract.percent = progressBarInteract.percent - 5.0 214 | if coords.scrollDir == sdDown: 215 | progressBarInteract.percent = progressBarInteract.percent + 5.0 216 | if ev.contains MouseDown: 217 | if coords.button == mbLeft: 218 | progressBarInteract.value = progressBarInteract.valueOnPos(coords) 219 | if coords.button == mbRight: 220 | progressBarInteract.percent = 50.0 221 | infoProgressBarInteract.text = 222 | fmt"{progressBarInteract.value}/{progressBarInteract.maxValue} percent: {progressBarInteract.percent}" 223 | # tb.write(70, 17, "<-- interact with me! (left, right click, scroll)") 224 | tb.dumpMi(coords) # to print mouse debug infos 225 | 226 | ev = tb.dispatch(table, coords) 227 | 228 | else: 229 | infoBox.text = $key 230 | discard 231 | 232 | tb.render(btnAsyncHttp) 233 | tb.render(btnNoBorder) 234 | tb.render(btnClear) 235 | tb.render(btnTest) 236 | 237 | tb.render(chkTest) 238 | tb.render(chkTest2) 239 | tb.render(chkDraw) 240 | 241 | # RadioBoxGroup renders all its members 242 | tb.render(radioBoxGroup) 243 | 244 | # Update the info box position to always be on the bottom 245 | infoBox.w = terminalWidth() 246 | infoBox.y = terminalHeight()-1 247 | tb.render(infoBox) 248 | 249 | tb.render(infoBoxMouse) # No need to update position (is at the top) 250 | tb.render(infoBoxMulti) 251 | tb.render(infoBoxAsync) 252 | tb.render(chooseBox) 253 | tb.render(textBox) 254 | 255 | tb.render(progressBarAsync) 256 | tb.render(progressBarVertical) # TODO not working properly yet 257 | tb.render(infoProgressBarAsync) 258 | 259 | tb.render(progressBarInteract) 260 | tb.render(infoProgressBarInteract) 261 | tb.render(labelProgressBarInteract) 262 | 263 | tb.render(table) 264 | 265 | tb.display() 266 | 267 | # poll(100) # for the async demo code (keep poll low), use sleep below for non async. 268 | poll(25) # for the async demo code (keep poll low), use sleep below for non async. 269 | # sleep(50) # when no async code used, just call sleep(50) 270 | -------------------------------------------------------------------------------- /examples/simple.nim: -------------------------------------------------------------------------------- 1 | import illwill 2 | import illwillWidgets 3 | import os 4 | 5 | 6 | # default illwill boilerplate 7 | proc exitProc() {.noconv.} = 8 | illwillDeinit() 9 | showCursor() 10 | quit(0) 11 | 12 | illwillInit(fullscreen=true, mouse=true) # <- enable mouse support 13 | setControlCHook(exitProc) 14 | hideCursor() 15 | 16 | var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) 17 | 18 | # Create widgets 19 | var infoBox = newInfoBox("",0 ,0, terminalWidth()) 20 | var btnFill = newButton("fill", 38, 3, 15, 2) 21 | var btnClear = newButton("clear", 38, 6, 15, 2) 22 | 23 | while true: 24 | 25 | var key = getKey() 26 | 27 | # Test if the key signals a mouse event. 28 | if key == Key.Mouse: 29 | 30 | # get the mouse information from illwill 31 | var mi: MouseInfo = getMouse() 32 | 33 | # to catch the widgets events 34 | var ev: Events 35 | 36 | # we must call dispatch on every widget. 37 | # dispatch returns events that the widget fires. 38 | ev = tb.dispatch(btnFill, mi) 39 | if ev.contains MouseUp: 40 | # do stuff when mouse button releases 41 | tb.clear("A") 42 | if ev.contains MouseHover: 43 | # do stuff when mouse hover over element 44 | infoBox.text = "Fills the screen!!" 45 | 46 | # Another button to clear the screen again 47 | ev = tb.dispatch(btnClear, mi) 48 | if ev.contains MouseUp: 49 | # do stuff when mouse button releases 50 | tb.clear() 51 | if ev.contains MouseHover: 52 | # do stuff when mouse hover over element 53 | infoBox.text = "clear the screen" 54 | 55 | 56 | # After dispatching we need to actually render the element 57 | # into the terminal buffer 58 | tb.render(btnFill) 59 | tb.render(btnClear) 60 | tb.render(infoBox) 61 | 62 | # Draw the terminal buffer to screen. 63 | tb.display() 64 | 65 | sleep(25) -------------------------------------------------------------------------------- /illwillWidgets.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.11" 4 | author = "David Krause" 5 | description = "widgets for illwill" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.0.4" 14 | requires "https://github.com/johnnovak/illwill.git" 15 | -------------------------------------------------------------------------------- /src/illwillWidgets.nim: -------------------------------------------------------------------------------- 1 | ## A small widget library for illwill, 2 | 3 | import illwill, macros, strutils 4 | import strformat, math 5 | import std/wordwrap 6 | 7 | macro preserveColor(pr: untyped) = 8 | ## this pragma saves the style before a render proc, 9 | ## and resets the style after a render proc 10 | result = newProc() 11 | result[0] = pr[0] 12 | let oldbody = pr.body 13 | result.params = pr.params 14 | result.body = quote do: 15 | let oldFg = tb.getForegroundColor() 16 | let oldBg = tb.getBackgroundColor() 17 | let oldStyle = tb.getStyle() 18 | tb.setForegroundColor(wid.color) 19 | tb.setBackgroundColor(wid.bgcolor) 20 | `oldbody` 21 | tb.setForegroundColor oldFg 22 | tb.setBackgroundColor oldBg 23 | tb.setStyle oldStyle 24 | 25 | type 26 | WrapMode* {.pure.} = enum 27 | None, Char, Word 28 | Percent* = range[0.0..100.0] 29 | Event* = enum 30 | MouseHover, MouseUp, MouseDown 31 | Orientation* = enum 32 | Horizontal, Vertical 33 | Events* = set[Event] 34 | Widget* = object of RootObj 35 | x*: int 36 | y*: int 37 | color*: ForegroundColor 38 | bgcolor*: BackgroundColor 39 | style*: Style 40 | highlight*: bool 41 | autoClear*: bool 42 | shouldBeCleared: bool 43 | Button* = object of Widget 44 | text*: string 45 | w*: int 46 | h*: int 47 | border*: bool 48 | Checkbox* = object of Widget 49 | text*: string 50 | checked*: bool 51 | textChecked*: string 52 | textUnchecked*: string 53 | RadioBoxGroup* = object of Widget 54 | radioButtons*: seq[ptr Checkbox] 55 | InfoBox* = object of Widget 56 | text*: string 57 | w*: int 58 | h*: int 59 | wrapMode*: WrapMode 60 | ChooseBox* = object of Widget 61 | bgcolorChoosen*: BackgroundColor 62 | choosenidx*: int 63 | w*: int 64 | h*: int 65 | elements*: seq[string] 66 | highlightIdx*: int 67 | chooseEnabled*: bool 68 | title*: string 69 | shouldGrow*: bool 70 | filter*: string 71 | TextBox* = object of Widget 72 | text*: string 73 | placeholder*: string 74 | focus*: bool 75 | w*: int 76 | caretIdx*: int 77 | ProgressBar* = object of Widget 78 | text*: string 79 | l*: int ## the length (length instead of width for vertical) 80 | maxValue*: float 81 | value*: float 82 | orientation*: Orientation 83 | bgTodo*: BackgroundColor 84 | bgDone*: BackgroundColor 85 | colorText*: ForegroundColor 86 | colorTextDone*: ForegroundColor 87 | TableBox* = object of Widget 88 | headers*: seq[(int, string)] 89 | rows*: seq[seq[string]] 90 | highlightIdx*: int 91 | w*: int 92 | h*: int 93 | bdColor: ForegroundColor 94 | colorHighlight: ForegroundColor 95 | bgHighlight: BackgroundColor 96 | 97 | # Cannot do this atm because of layering! 98 | # ComboBox[Key] = object of Widget 99 | # open: bool 100 | # current: Key 101 | # elements: Table[Key, string] 102 | # w: int 103 | # color: ForegroundColor 104 | # proc newComboBox[Key](x,y: int, w = 10): ComboBox = 105 | # result = ComboBox[Key]( 106 | # open: false, 107 | # ) 108 | 109 | # ######################################################################################################## 110 | # Utils 111 | # ######################################################################################################## 112 | proc wrapText*(str: string, maxLen: int, wrapMode: WrapMode): string = 113 | case wrapMode 114 | of WrapMode.None: return str 115 | of WrapMode.Char: 116 | return wrapWords(str, maxLen, splitLongWords = true) 117 | of WrapMode.Word: 118 | return wrapWords(str, maxLen, splitLongWords = false) 119 | 120 | proc positionHelper*(coords: MouseInfo): string = 121 | result = fmt"x:{coords.x} y:{coords.y} bot:{coords.y - terminalHeight()} right:{coords.x - terminalWidth()}" 122 | 123 | # ######################################################################################################## 124 | # Widget 125 | # ######################################################################################################## 126 | proc clear*(wid: var Widget) {.inline.} = 127 | wid.shouldBeCleared = true 128 | 129 | # ######################################################################################################## 130 | # InfoBox 131 | # ######################################################################################################## 132 | proc newInfoBox*(text: string, x, y: int, w = 10, h = 1, color = fgBlack, bgcolor = bgWhite): InfoBox = 133 | result = InfoBox( 134 | text: text, 135 | x: x, 136 | y: y, 137 | w: w, 138 | h: h, 139 | color: color, 140 | bgcolor: bgcolor, 141 | wrapMode: WrapMode.None 142 | ) 143 | 144 | proc render*(tb: var TerminalBuffer, wid: InfoBox) {.preserveColor.} = 145 | # TODO save old text to only overwrite the len of the old text 146 | let lines = wid.text.wrapText(wid.w, wid.wrapMode).splitLines() 147 | for idx in 0..lines.len-1: 148 | tb.write(wid.x, wid.y+idx, lines[idx].alignLeft(wid.w)) 149 | 150 | proc inside(wid: InfoBox, mi: MouseInfo): bool = 151 | return (mi.x in wid.x .. wid.x+wid.w) and (mi.y == wid.y) 152 | 153 | proc dispatch*(tb: var TerminalBuffer, wid: InfoBox, mi: MouseInfo): Events {.discardable.} = 154 | if not wid.inside(mi): return 155 | case mi.action 156 | of mbaPressed: result.incl MouseDown 157 | of mbaReleased: result.incl MouseUp 158 | of mbaNone: result.incl MouseHover 159 | 160 | # ######################################################################################################## 161 | # Checkbox 162 | # ######################################################################################################## 163 | proc newCheckbox*(text: string, x, y: int, color = fgBlue): Checkbox = 164 | result = Checkbox( 165 | text: text, 166 | x: x, 167 | y: y, 168 | color: color, 169 | textChecked: "[X] ", 170 | textUnchecked: "[ ] " 171 | ) 172 | 173 | proc render*(tb: var TerminalBuffer, wid: Checkbox) {.preserveColor.} = 174 | let check = if wid.checked: wid.textChecked else: wid.textUnchecked 175 | tb.write(wid.x, wid.y, check & wid.text) 176 | 177 | proc inside(wid: Checkbox, mi: MouseInfo): bool = 178 | return (mi.x in wid.x .. wid.x+wid.text.len + 3) and (mi.y == wid.y) 179 | 180 | proc dispatch*(tr: var TerminalBuffer, wid: var Checkbox, mi: MouseInfo): Events {.discardable.} = 181 | if not wid.inside(mi): return 182 | result.incl MouseHover 183 | case mi.action 184 | of mbaPressed: 185 | result.incl MouseDown 186 | of mbaReleased: 187 | wid.checked = not wid.checked 188 | result.incl MouseUp 189 | of mbaNone: discard 190 | 191 | # ######################################################################################################## 192 | # RadioBox 193 | # ######################################################################################################## 194 | proc newRadioBox*(text: string, x, y: int, color = fgBlue): Checkbox = 195 | ## Radio box is actually a checkbox, you need to add the checkbox to a radio button group 196 | result = newCheckbox(text, x, y, color) 197 | result.textChecked = "(X) " 198 | result.textUnchecked = "( ) " 199 | 200 | proc newRadioBoxGroup*(radioButtons: seq[ptr Checkbox]): RadioBoxGroup = 201 | ## Create a new radio box, add radio boxes to the group, 202 | ## then call the *groups* `render` and `dispatch` proc. 203 | result = RadioBoxGroup( 204 | radioButtons: radioButtons 205 | ) 206 | 207 | proc render*(tb: var TerminalBuffer, wid: RadioBoxGroup) {.preserveColor.} = 208 | for radioButton in wid.radioButtons: 209 | tb.render(radioButton[]) 210 | 211 | proc uncheckAll*(wid: var RadioBoxGroup) = 212 | for radioButton in wid.radioButtons.mitems: 213 | radioButton[].checked = false 214 | 215 | proc dispatch*(tb: var TerminalBuffer, wid: var RadioBoxGroup, mi: MouseInfo): Events {.discardable.} = 216 | var insideSome = false 217 | for radioButton in wid.radioButtons.mitems: 218 | if radioButton[].inside(mi): insideSome = true 219 | if (not insideSome) or (mi.action != mbaReleased): return 220 | wid.uncheckAll() 221 | for radioButton in wid.radioButtons.mitems: 222 | let ev = tb.dispatch(radioButton[], mi) 223 | result.incl ev 224 | 225 | proc element*(wid: RadioBoxGroup): Checkbox = 226 | ## returns the currect selected element of the `RadioBoxGroup` 227 | for radioButton in wid.radioButtons: 228 | if radioButton.checked: 229 | return radioButton[] 230 | 231 | # ######################################################################################################## 232 | # Button 233 | # ######################################################################################################## 234 | proc newButton*(text: string, x, y, w, h: int, border = true, color = fgBlue): Button = 235 | result = Button( 236 | text: text, 237 | highlight: false, 238 | x: x, 239 | y: y, 240 | w: w, 241 | h: h, 242 | border: border, 243 | color: color, 244 | ) 245 | 246 | 247 | proc render*(tb: var TerminalBuffer, wid: Button) {.preserveColor.} = 248 | if wid.border: 249 | if wid.autoClear or wid.shouldBeCleared: tb.fill(wid.x, wid.y, wid.x+wid.w, wid.y+wid.h) 250 | tb.drawRect( 251 | wid.x, 252 | wid.y, 253 | wid.x + wid.w, 254 | wid.y + wid.h, 255 | doubleStyle=wid.highlight, 256 | ) 257 | tb.write( 258 | wid.x+1 + wid.w div 2 - wid.text.len div 2 , 259 | wid.y+1, 260 | wid.text 261 | ) 262 | else: 263 | var style = if wid.highlight: styleBright else: styleDim 264 | tb.write( 265 | wid.x + wid.w div 2 - wid.text.len div 2 , 266 | wid.y, 267 | style, wid.text 268 | ) 269 | 270 | proc inside(wid: Button, mi: MouseInfo): bool = 271 | return (mi.x in wid.x .. wid.x+wid.w) and (mi.y in wid.y .. wid.y+wid.h) 272 | 273 | proc dispatch*(tr: var TerminalBuffer, wid: var Button, mi: MouseInfo): Events {.discardable.} = 274 | ## if the mouse clicks this button 275 | if not wid.inside(mi): 276 | wid.highlight = false 277 | return 278 | result.incl MouseHover 279 | case mi.action 280 | of mbaPressed: 281 | wid.highlight = true 282 | result.incl MouseDown 283 | of mbaReleased: 284 | wid.highlight = false 285 | result.incl MouseUp 286 | of mbaNone: 287 | wid.highlight = true 288 | 289 | # ######################################################################################################## 290 | # ChooseBox 291 | # ######################################################################################################## 292 | proc grow*(wid: var ChooseBox) = 293 | ## call this to grow the box if you've added or removed a element 294 | ## from the `wid.elements` seq. 295 | if wid.elements.len >= wid.h: wid.h = wid.elements.len+1 # TODO allowedToGrow 296 | 297 | proc add*(wid: var ChooseBox, elem: string) = 298 | ## adds element to the list, grows the box immediately 299 | wid.elements.add(elem) 300 | wid.grow() 301 | 302 | proc newChooseBox*(elements: seq[string], x, y, w, h: int, 303 | color = fgBlue, label = "", choosenidx = 0, shouldGrow = true): ChooseBox = 304 | ## a list of text items to choose from, sometimes also called listbox 305 | ## if `shouldGrow == true` the chooseBox grows automatically when elements added 306 | result = ChooseBox( 307 | elements: elements, 308 | choosenidx: choosenidx, 309 | x: x, 310 | y: y, 311 | w: w, 312 | h: h, 313 | color: color, 314 | ) 315 | result.highlightIdx = -1 316 | result.chooseEnabled = true 317 | if shouldGrow: result.grow() 318 | 319 | proc setChoosenIdx*(wid: var ChooseBox, idx: int) = 320 | ## sets the choosen idex to a valid value 321 | wid.choosenidx = idx.clamp(0, wid.elements.high) 322 | 323 | proc nextChoosenidx*(wid: var ChooseBox, num = 1) = 324 | wid.setChoosenIdx(wid.choosenidx + num) 325 | 326 | proc prevChoosenidx*(wid: var ChooseBox, num = 1) = 327 | wid.setChoosenIdx(wid.choosenidx - num) 328 | 329 | proc filter*(query: string, elems: seq[string]): seq[int] = 330 | ## TODO filter must be somewhere else, maybe needet for other widgets 331 | if query.len <= 2: return @[] 332 | let queryClean = query.toLower() 333 | for idx, elem in elems: 334 | let elemClean = elem.toLower().strip() 335 | if elemClean.contains(queryClean): result.add idx 336 | 337 | proc filterElements(wid: var ChooseBox): seq[string] = 338 | if wid.filter.len == 0: return wid.elements 339 | for idx in filter(wid.filter, wid.elements): 340 | result.add wid.elements[idx] 341 | 342 | proc element*(wid: var ChooseBox): string = 343 | if wid.filter.len == 0: 344 | try: 345 | return wid.elements[wid.choosenidx] 346 | except CatchableError: 347 | return "" 348 | else: 349 | try: 350 | return wid.filterElements()[wid.choosenidx] 351 | except CatchableError: 352 | return "" 353 | 354 | proc clear(tb: var TerminalBuffer, wid: var ChooseBox) {.inline.} = 355 | tb.fill(wid.x, wid.y, wid.x+wid.w, wid.y+wid.h) # maybe not needet? 356 | wid.shouldBeCleared = false 357 | 358 | proc clampAndFillStr(str: string, upto: int): string = 359 | ## if str is smaller than upto, fill the rest 360 | ## if str is bigger than upto clamp 361 | if str.len >= upto: return str[0 .. min(upto, str.len - 1)] 362 | else: return str.alignLeft(upto, ' ') 363 | 364 | proc render*(tb: var TerminalBuffer, wid: var ChooseBox) {.preserveColor.} = 365 | tb.clear(wid) 366 | let filteredElements = wid.filterElements() 367 | var fromIdx: int = 0 368 | if wid.choosenIdx > wid.h - 4: 369 | fromIdx = wid.choosenIdx - wid.h + 2 370 | var drawIdx = -1 371 | # for elemIdx, elemRaw in wid.elements: #.view(): 372 | for elemIdx, elemRaw in filteredElements: #.view(): 373 | if fromIdx > elemIdx: 374 | continue 375 | if drawIdx >= wid.h - 2: 376 | break # TODO: Break out of for loop 377 | drawIdx.inc 378 | # if not wid.shouldGrow: 379 | # if elemIdx >= wid.h: continue # do not draw additional elements but render scrollbar 380 | let elem = elemRaw.clampAndFillStr(wid.w) 381 | if wid.chooseEnabled and elemIdx == wid.choosenIdx: #wid.mustBeHighlighted(idx): 382 | ## Draw selected 383 | tb.write resetStyle 384 | tb.write(wid.x+1, wid.y + 1 + drawIdx, wid.color, wid.bgcolor, styleReverse, elem) 385 | else: 386 | tb.write resetStyle 387 | if elemIdx == wid.highlightIdx: 388 | ## Draw "bright" 389 | tb.write(wid.x + 1, wid.y + 1 + drawIdx, wid.color, wid.bgcolor, styleBright, elem) 390 | else: 391 | ## Draw "normal" 392 | tb.write(wid.x + 1, wid.y + 1 + drawIdx, wid.color, wid.bgcolor, elem) 393 | tb.write resetStyle 394 | tb.drawRect( 395 | wid.x, 396 | wid.y, 397 | wid.x + wid.w, 398 | wid.y + wid.h, 399 | wid.highlight 400 | ) 401 | if wid.title.len > 0: 402 | tb.write(wid.x + 2, wid.y, "| " & wid.title & " |") 403 | 404 | proc inside(wid: ChooseBox, mi: MouseInfo): bool = 405 | return (mi.x in wid.x .. wid.x+wid.w) and (mi.y in wid.y .. wid.y+wid.h) 406 | 407 | proc dispatch*(tr: var TerminalBuffer, wid: var ChooseBox, mi: MouseInfo): Events {.discardable.} = 408 | result = {} 409 | if wid.shouldGrow: wid.grow() 410 | if not wid.inside(mi): return 411 | result.incl MouseHover 412 | case mi.action 413 | of mbaPressed: 414 | wid.choosenidx = clamp( (mi.y - wid.y)-1 , 0, wid.elements.len-1) # Moved up TEST if everything works 415 | result.incl MouseDown 416 | of mbaReleased: 417 | # wid.choosenidx = clamp( (mi.y - wid.y)-1 , 0, wid.elements.len-1) # Moved up TEST if everything works 418 | result.incl MouseUp 419 | of mbaNone: discard 420 | 421 | # ######################################################################################################## 422 | # TextBox 423 | # ######################################################################################################## 424 | proc newTextBox*(text: string, x, y: int, w = 10, 425 | color = fgBlack, bgcolor = bgCyan, placeholder = ""): TextBox = 426 | ## TODO a good textbox is COMPLICATED, this is a VERY basic one!! PR's welcome ;) 427 | result = TextBox( 428 | text: text, 429 | x: x, 430 | y: y, 431 | w: w, 432 | color: color, 433 | bgcolor: bgcolor, 434 | placeholder: placeholder 435 | ) 436 | 437 | proc render*(tb: var TerminalBuffer, wid: TextBox) {.preserveColor.} = 438 | # TODO save old text to only overwrite the len of the old text 439 | tb.write(wid.x, wid.y, repeat(" ", wid.w)) 440 | if wid.caretIdx == wid.text.len(): 441 | tb.write(wid.x, wid.y, wid.text) 442 | if wid.text.len < wid.w: 443 | tb.write(wid.x + wid.caretIdx, wid.y, styleReverse, " ", resetStyle) 444 | else: 445 | tb.write(wid.x, wid.y, 446 | wid.text[0..wid.caretIdx-1], 447 | styleReverse, $wid.text[wid.caretIdx], 448 | 449 | resetStyle, 450 | wid.color, wid.bgcolor, 451 | wid.text[wid.caretIdx+1..^1], 452 | resetStyle 453 | ) 454 | 455 | proc inside(wid: TextBox, mi: MouseInfo): bool = 456 | return (mi.x in wid.x .. wid.x+wid.w) and (mi.y == wid.y) 457 | 458 | proc dispatch*(tb: var TerminalBuffer, wid: var TextBox, mi: MouseInfo): Events {.discardable.} = 459 | if wid.inside(mi): 460 | result.incl MouseHover 461 | case mi.action 462 | of mbaPressed: 463 | result.incl MouseDown 464 | of mbaReleased: 465 | wid.focus = true 466 | result.incl MouseUp 467 | of mbaNone: discard 468 | elif not wid.inside(mi) and (mi.action == mbaReleased or mi.action == mbaPressed): 469 | wid.focus = false 470 | 471 | proc handleKey*(tb: var TerminalBuffer, wid: var TextBox, key: Key): bool {.discardable.} = 472 | ## if this function return "true" the textbox lost focus by enter 473 | result = false 474 | 475 | template incCaret() = 476 | wid.caretIdx.inc 477 | wid.caretIdx = clamp(wid.caretIdx, 0, wid.text.len) 478 | template decCaret() = 479 | wid.caretIdx.dec 480 | wid.caretIdx = clamp(wid.caretIdx, 0, wid.text.len) 481 | 482 | if key == Key.Mouse: return false 483 | if key == Key.None: return false 484 | 485 | case key 486 | of Enter: 487 | return true 488 | of Escape: 489 | wid.focus = false 490 | return 491 | of End: 492 | wid.caretIdx = wid.text.len 493 | of Home: 494 | wid.caretIdx = 0 495 | of Backspace: 496 | try: 497 | delete(wid.text, wid.caretIdx-1..wid.caretIdx-1) 498 | decCaret 499 | except CatchableError: 500 | discard 501 | of Right: 502 | incCaret 503 | of Left: 504 | decCaret 505 | else: 506 | # Add ascii representation 507 | var ch = $key.char 508 | if wid.text.len < wid.w: 509 | wid.text.insert(ch, wid.caretIdx) 510 | wid.caretIdx.inc 511 | wid.caretIdx = clamp(wid.caretIdx, 0, wid.text.len) 512 | 513 | template setKeyAsHandled*(key: Key) = 514 | ## call this on key when the key was handled by a textbox 515 | if key != Key.Mouse: 516 | key = Key.None 517 | 518 | # ######################################################################################################## 519 | # ProgressBar 520 | # ######################################################################################################## 521 | proc newProgressBar*(text: string, x, y: int, l = 10, value = 0.0, maxValue = 100.0, 522 | orientation = Horizontal, bgDone = bgGreen , bgTodo = bgRed): ProgressBar = 523 | result = ProgressBar( 524 | text: text, 525 | x: x, 526 | y: y, 527 | l: l, 528 | value: value, 529 | maxValue: maxValue, 530 | orientation: orientation, 531 | bgDone: bgDone, 532 | bgTodo: bgTodo, 533 | colorText: fgYellow, 534 | colorTextDone: fgBlack 535 | ) 536 | 537 | proc percent*(wid: ProgressBar): float = 538 | ## Gets the percentage the progress bar is filled 539 | return (wid.value / wid.maxValue) * 100 540 | 541 | proc `percent=`*(wid: var ProgressBar, val: float) = 542 | ## sets the percentage the progress bar should be filled 543 | wid.value = (val * wid.maxValue / 100.0).clamp(0.0, wid.maxValue) 544 | 545 | proc render*(tb: var TerminalBuffer, wid: ProgressBar) {.preserveColor.} = 546 | let num = (wid.l.float / 100.0).float * wid.percent 547 | if wid.orientation == Horizontal: 548 | # write progress idicator 549 | let doneRange = wid.x .. wid.x + num.int - 1 550 | let todoRange = wid.x + num.int .. (wid.x + wid.l) - 1 551 | for idxx in doneRange: 552 | tb.write(idxx, wid.y, wid.color, wid.bgDone, "=") 553 | for idxx in todoRange: 554 | tb.write(idxx, wid.y, wid.color, wid.bgTodo, "-") 555 | 556 | if wid.text.len > 0: 557 | let textRange = (wid.x + (wid.l div 2) ) - wid.text.len div 2 .. ((wid.x + (wid.l div 2) ) - wid.text.len div 2) + (wid.text.len - 1) 558 | var idx = 0 559 | for idxx in textRange: 560 | let ch = $wid.text[idx] # txt[] # get the char at this idxx position 561 | idx.inc 562 | if doneRange.contains idxx: 563 | tb.write(idxx, wid.y, wid.colorTextDone, wid.bgDone, ch) # TODO 564 | elif todoRange.contains idxx: 565 | tb.write(idxx, wid.y, wid.colorText, wid.bgTodo, ch) # TODO 566 | else: 567 | tb.write(idxx, wid.y, wid.colorText, ch) # TODO 568 | 569 | elif wid.orientation == Vertical: # TODO Vertical is not fully supported yet. It might work for you, though. 570 | discard 571 | # raise 572 | # DUMMY 573 | for idx in 0..wid.l: 574 | tb.write(wid.x-1, wid.y + idx, "O") 575 | # IMPL 576 | for todoIdx in 0..(wid.l - num.int): 577 | tb.write(wid.x, wid.y + num.int + todoIdx, bgRed, "-") 578 | for doneIdx in 0..num.int: 579 | let rest = wid.l - num.int 580 | tb.write(wid.x, wid.y + rest + doneIdx, bgGreen, "=") 581 | 582 | proc inside(wid: ProgressBar, mi: MouseInfo): bool = 583 | return (mi.x in wid.x .. wid.x+wid.l) and (mi.y == wid.y) 584 | 585 | # proc percentOnPos*(wid: ProgressBar, mi: MouseInfo): float = 586 | # let cell = ((mi.x - wid.x)) 587 | # return (cell / wid.w) 588 | 589 | proc valueOnPos*(wid: ProgressBar, mi: MouseInfo): float = 590 | if not wid.inside(mi): return 0.0 591 | let cell = ((mi.x - wid.x)) 592 | return (cell / wid.l) * wid.maxValue 593 | 594 | proc dispatch*(tb: var TerminalBuffer, wid: var ProgressBar, mi: MouseInfo): Events {.discardable.} = 595 | if not wid.inside(mi): return 596 | result.incl MouseHover 597 | case mi.action 598 | of mbaPressed: 599 | result.incl MouseDown 600 | of mbaReleased: 601 | result.incl MouseUp 602 | of mbaNone: discard 603 | 604 | # ######################################################################################################## 605 | # TableBox 606 | # ######################################################################################################## 607 | proc newTableBox*(x, y, w, h: int, borderColor = fgCyan, color = fgWhite, bgColor = bgBlack, colorHighlight = fgRed, bgHighlight = bgYellow): TableBox = 608 | result = TableBox( 609 | x: x, 610 | y: y, 611 | w: w, 612 | h: h, 613 | color: color, 614 | bdColor: borderColor, 615 | bgColor: bgColor, 616 | colorHighlight: colorHighlight, 617 | bgHighlight: bgHighlight 618 | ) 619 | result.highlightIdx = -1 620 | 621 | proc addCol*(wid: var TableBox, header: string, width: int) = 622 | wid.headers.add((width, header)) 623 | 624 | #TODO make it scrollable 625 | #proc clear*(wid: var TableBox) = 626 | # wid.rows = @[] 627 | 628 | proc addRow*(wid: var TableBox, row: seq[string]) = 629 | wid.rows.add(row) 630 | 631 | proc inside(wid: TableBox, mi: MouseInfo): bool = 632 | result = wid.x < mi.x and mi.x < wid.x + wid.w and wid.y < mi.y and mi.y < wid.y + wid.h 633 | 634 | proc dispatch*(tb: var TerminalBuffer, wid: var TableBox, mi: MouseInfo): Events {.discardable.} = 635 | tb.setForegroundColor(fgRed) 636 | if not wid.inside(mi): 637 | wid.highlightIdx = -1 638 | return 639 | 640 | result.incl MouseHover 641 | case mi.action 642 | of mbaPressed: 643 | result.incl MouseDown 644 | wid.highlightIdx = mi.y - (wid.y + 3) 645 | of mbaReleased: 646 | result.incl MouseUp 647 | of mbaNone: discard 648 | 649 | proc render*(tb: var TerminalBuffer, wid: var TableBox) {.preserveColor.} = 650 | let 651 | x1 = wid.x 652 | y1 = wid.y 653 | x2 = x1 + wid.w - 1 654 | y2 = y1 + wid.h - 1 655 | 656 | var bb = newBoxBuffer(tb.width, tb.height) 657 | # Draw border 658 | bb.drawVertLine(x1, y1, y2) 659 | bb.drawVertLine(x2, y1, y2) 660 | bb.drawHorizLine(x1, x2, y1) 661 | bb.drawHorizLine(x1, x2, y2) 662 | 663 | # Draw headers 664 | tb.setForegroundColor(wid.color) 665 | var y = y1 + 1 666 | bb.drawHorizLine(x1, x2, y+1) 667 | tb.setForegroundColor(wid.color) 668 | var 669 | x = x1 670 | i = 0 671 | for (w, t) in wid.headers: 672 | tb.write(x + 1, y, t) 673 | inc(x, w) 674 | inc(i) 675 | if i < wid.headers.len: 676 | bb.drawVertLine(x, y1, y2) 677 | 678 | # Draw contents 679 | i = 0 680 | for row in wid.rows: 681 | y = y1 + 2 682 | x = x1 683 | var j = 0 684 | 685 | if i == wid.highlightIdx: 686 | tb.setBackgroundColor(wid.bgHighlight) 687 | tb.setForegroundColor(wid.colorHighlight) 688 | else: 689 | tb.setBackgroundColor(wid.bgColor) 690 | tb.setForegroundColor(wid.color) 691 | 692 | for t in row: 693 | if j >= wid.headers.len: 694 | break 695 | tb.write(x + 1, y + i + 1, t) 696 | inc(x, wid.headers[j][0]) 697 | inc(j) 698 | inc(i) 699 | 700 | tb.setBackgroundColor(wid.bgColor) 701 | tb.setForegroundColor(wid.bdColor) 702 | tb.write(bb) 703 | 704 | if wid.color != wid.colorHighlight and wid.bgColor != wid.bgHighlight: 705 | if getKey() == Key.Mouse: 706 | discard tb.dispatch(wid, getMouse()) 707 | -------------------------------------------------------------------------------- /tests/table.nim: -------------------------------------------------------------------------------- 1 | import illwill, illwillWidgets 2 | import strformat, strutils 3 | import asyncdispatch, httpclient# for async demonstration 4 | import os 5 | 6 | proc exitProc() {.noconv.} = 7 | illwillDeinit() 8 | showCursor() 9 | quit(0) 10 | 11 | illwillInit(fullscreen=true, mouse=true) 12 | setControlCHook(exitProc) 13 | hideCursor() 14 | 15 | var tb = newTerminalBuffer(50, 30) 16 | 17 | var table = newTableBox(1, 1, 40, 7) 18 | table.addCol("#", 2) 19 | table.addCol("Name", 10) 20 | table.addCol("Age", 4) 21 | table.addCol("Notes", 40) 22 | 23 | table.addRow(@["1", "John Doe", "40"]) 24 | table.addRow(@["2", "Jane Doe", "38"]) 25 | while true: 26 | tb.clear() 27 | tb.render(table) 28 | tb.display() 29 | sleep(100) -------------------------------------------------------------------------------- /tests/tdebug.nim: -------------------------------------------------------------------------------- 1 | # import ../src/widgets 2 | import illwill 3 | import strformat, strutils 4 | import asyncdispatch, httpclient# for async demonstration 5 | import os 6 | 7 | proc exitProc() {.noconv.} = 8 | illwillDeinit() 9 | showCursor() 10 | quit(0) 11 | 12 | illwillInit(fullscreen=true, mouse=true) 13 | setControlCHook(exitProc) 14 | hideCursor() 15 | 16 | var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) 17 | while true: 18 | var key = getKey() 19 | sleep(50) -------------------------------------------------------------------------------- /tests/utils/tWrapText.nim: -------------------------------------------------------------------------------- 1 | from ../../src/illwillWidgets import wrapText, WrapMode 2 | 3 | const 4 | tst1 = "123456789" 5 | tst2 = "foo baa baz" 6 | 7 | doAssert tst1.wrapText(3, WrapMode.None) == tst1 8 | 9 | doAssert tst1.wrapText(3, WrapMode.Char) == "123\n456\n789\n" 10 | 11 | # doAssert tst1.wrapText(3, WrapMode.Word) == "foo\nbaa\nbaz\n" 12 | # doAssert tst1.wrapText(4, WrapMode.Word) == "foo\nbaa\nbaz" 13 | # doAssert tst1.wrapText(6, WrapMode.Word) == "foo baa\nbaz" 14 | --------------------------------------------------------------------------------