about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-01-22 15:35:56 +0100
committerbptato <nincsnevem662@gmail.com>2021-01-22 15:35:56 +0100
commit94d681b3935a3f9105dc60320230fa9657cbd7b5 (patch)
tree3eb9189d01f3627927bc25b53fca1da20ae9b1e2
parent5d6af7f57a89239554a9cd51fe60f8227d7ce549 (diff)
downloadchawan-94d681b3935a3f9105dc60320230fa9657cbd7b5.tar.gz
something broke again
-rw-r--r--Makefile2
-rw-r--r--buffer.nim17
-rw-r--r--config.nim9
-rw-r--r--display.nim62
-rw-r--r--fmttext.nim8
-rw-r--r--htmlelement.nim58
-rw-r--r--keymap1
-rw-r--r--twtio.nim111
-rw-r--r--twtstr.nim79
9 files changed, 182 insertions, 165 deletions
diff --git a/Makefile b/Makefile
index 5f71b0e1..9d231e4a 100644
--- a/Makefile
+++ b/Makefile
@@ -4,4 +4,6 @@ release:
 	nim compile -d:release -d:ssl -o:twt main.nim
 release_opt:
 	nim compile -d:danger -d:ssl -o:twt_opt main.nim
+clean:
+	rm ./twt ./dtwt ./twt_opt
 all: debug release release_opt
diff --git a/buffer.nim b/buffer.nim
index df1f0612..788f8aae 100644
--- a/buffer.nim
+++ b/buffer.nim
@@ -14,7 +14,7 @@ type
   Buffer* = ref BufferObj
   BufferObj = object
     text*: string
-    rawText*: string
+    rawtext*: string
     lines*: seq[int]
     rawlines*: seq[int]
     title*: string
@@ -45,14 +45,12 @@ proc newBuffer*(attrs: TermAttributes): Buffer =
                 cursorY: 1,
                 document: newDocument())
 
-
-
 func lastLine*(buffer: Buffer): int =
-  assert buffer.rawlines.len == buffer.lines.len
+  assert(buffer.rawlines.len == buffer.lines.len)
   return buffer.lines.len - 1
 
 func lastVisibleLine*(buffer: Buffer): int =
-  return min(buffer.fromY + buffer.height - 2, buffer.lastLine())
+  return min(buffer.fromY + buffer.height - 1, buffer.lastLine())
 
 #doesn't include newline
 func lineLength*(buffer: Buffer, line: int): int =
@@ -147,7 +145,7 @@ proc addNode*(buffer: Buffer, htmlNode: HtmlNode) =
     else: discard
   elif htmlNode.isTextNode():
     if htmlNode.parentElement != nil and htmlNode.parentElement.islink:
-      let anchor = htmlNode.getParent(TAG_A)
+      let anchor = htmlNode.ancestor(TAG_A)
       assert(anchor != nil)
       buffer.clickables.add(anchor)
 
@@ -434,14 +432,14 @@ proc checkLinkSelection*(buffer: Buffer): bool =
     if buffer.cursorOnNode(buffer.selectedlink):
       return false
     else:
-      let anchor = buffer.selectedlink.getParent(TAG_A)
+      let anchor = buffer.selectedlink.ancestor(TAG_A)
       anchor.selected = false
       buffer.selectedlink = nil
       buffer.hovertext = ""
   for node in buffer.links:
     if buffer.cursorOnNode(node):
       buffer.selectedlink = node
-      let anchor = node.getParent(TAG_A)
+      let anchor = node.ancestor(TAG_A)
       assert(anchor != nil)
       anchor.selected = true
       buffer.hovertext = HtmlAnchorElement(anchor).href
@@ -456,4 +454,7 @@ proc gotoAnchor*(buffer: Buffer): bool =
   return false
 
 proc setLocation*(buffer: Buffer, uri: Uri) =
+  buffer.document.location = uri
+
+proc gotoLocation*(buffer: Buffer, uri: Uri) =
   buffer.document.location = buffer.document.location.combine(uri)
diff --git a/config.nim b/config.nim
index df3d16d6..f13d172b 100644
--- a/config.nim
+++ b/config.nim
@@ -2,6 +2,8 @@ import tables
 import strutils
 import macros
 
+import twtstr
+
 type
   TwtAction* =
     enum
@@ -30,13 +32,6 @@ type
 var normalActionRemap*: Table[string, TwtAction]
 var linedActionRemap*: Table[string, TwtAction]
 
-func getControlChar(c: char): char =
-  if int(c) >= int('a'):
-    return char(int(c) - int('a') + 1)
-  elif c == '?':
-    return char(127)
-  assert(false)
-
 proc getRealKey(key: string): string =
   var realk: string
   var currchar: char
diff --git a/display.nim b/display.nim
index 5ebf78f3..19876ab5 100644
--- a/display.nim
+++ b/display.nim
@@ -64,28 +64,18 @@ proc addSpaces(buffer: Buffer, state: var RenderState, n: int) =
   state.atchar += n
   state.atrawchar += n
 
+const runeSpace = " ".toRunes()[0]
 proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) =
   state.lastwidth = 0
   var n = 0
-  var fmtword: ustring = @[]
-  var rawword: ustring = @[]
+  var fmtword = ""
+  var rawword = ""
   var prevl = false
-  for r in node.fmttext:
-    fmtword &= r
-
-    if n >= node.rawtext.len or r != node.rawtext[n]:
-      continue
-
+  for r in node.rawtext.runes:
     rawword &= r
     state.x += 1
 
     if state.x > buffer.width:
-      if buffer.rawtext.len > 0 and buffer.rawtext[^1] == ' ':
-        buffer.rawtext = buffer.rawtext.substr(0, buffer.rawtext.len - 2)
-        buffer.text = buffer.text.substr(0, buffer.text.len - 2)
-        state.atchar -= 1
-        state.atrawchar -= 1
-        state.x -= 1
       state.lastwidth = max(state.lastwidth, state.x)
       buffer.flushLine(state)
       prevl = true
@@ -93,21 +83,21 @@ proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) =
       state.lastwidth = max(state.lastwidth, state.x)
 
     if r == runeSpace:
-      buffer.writefmt($fmtword)
-      buffer.writeraw($rawword)
-      state.atchar += ($fmtword).len
-      state.atrawchar += ($rawword).len
+      eprint "x at", rawword, "is", state.x, "."
+      buffer.writefmt(fmtword)
+      buffer.writeraw(rawword)
+      state.atchar += fmtword.len
+      state.atrawchar += rawword.len
       if prevl:
-        state.x += rawword.len
+        state.x += rawword.runeLen
         prevl = false
-      fmtword = @[]
-      rawword = @[]
-    n += 1
-
-  buffer.writefmt($fmtword)
-  buffer.writeraw($rawword)
-  state.atchar += ($fmtword).len
-  state.atrawchar += ($rawword).len
+      fmtword = ""
+      rawword = ""
+
+  buffer.writefmt(fmtword)
+  buffer.writeraw(rawword)
+  state.atchar += fmtword.len
+  state.atrawchar += rawword.len
   state.lastwidth = max(state.lastwidth, state.x)
 
 proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
@@ -173,8 +163,6 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
     buffer.flushLine(state)
 
 proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
-  if not node.visibleNode():
-    return
   let elem = node.nodeAttr()
   if elem.tagType == TAG_TITLE:
     if node.isTextNode():
@@ -205,9 +193,9 @@ proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   node.x = state.x
   node.y = state.y
   buffer.writeWrappedText(state, node)
-  if state.x != node.x:
-    eprint node.x, node.y, state.x, state.y, node.nodeAttr().tagType
-    eprint "len", state.atrawchar
+  #if state.x != node.x:
+  #  eprint node.x, node.y, state.x, state.y, node.nodeAttr().tagType
+  #  eprint "len", state.atrawchar
   node.width = state.lastwidth - node.x
   node.height = state.y - node.y + 1
 
@@ -228,8 +216,8 @@ type
 
 proc setLastHtmlLine(buffer: Buffer, state: var RenderState) =
   if buffer.text.len != buffer.lines[^1]:
-    state.atchar = buffer.text.len + 1
-    state.atrawchar = buffer.rawtext.len + 1
+    state.atchar = buffer.text.len
+    state.atrawchar = buffer.rawtext.len
   buffer.flushLine(state)
 
 proc renderHtml*(buffer: Buffer) =
@@ -250,7 +238,7 @@ proc renderHtml*(buffer: Buffer) =
                                 html: getHtmlNode(item, currElem.html))
         stack.add(child)
         currElem.html.childNodes.add(child.html)
-        if not last and child.html.visibleNode():
+        if not last and not child.html.hidden:
           last = true
           if HtmlElement(currElem.html).display == DISPLAY_BLOCK:
             stack[^1].html.closeblock = true
@@ -339,8 +327,8 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
             redraw = true
         else: discard
         if selectedElem.get().islink:
-          let anchor = HtmlAnchorElement(buffer.selectedlink.getParent(TAG_A)).href
-          buffer.setLocation(parseUri(anchor))
+          let anchor = HtmlAnchorElement(buffer.selectedlink.ancestor(TAG_A)).href
+          buffer.gotoLocation(parseUri(anchor))
           return true
     of ACTION_CHANGE_LOCATION:
       var url = $buffer.document.location
diff --git a/fmttext.nim b/fmttext.nim
new file mode 100644
index 00000000..148ac626
--- /dev/null
+++ b/fmttext.nim
@@ -0,0 +1,8 @@
+type
+  FmtText* = object
+    str*: string
+    beginStyle*: string
+    endStyle*: string
+
+func `$`*(stt: FmtText): string =
+  return stt.beginStyle & stt.str & stt.endStyle
diff --git a/htmlelement.nim b/htmlelement.nim
index c37b294e..e6dc0ca8 100644
--- a/htmlelement.nim
+++ b/htmlelement.nim
@@ -24,8 +24,8 @@ type
     parentNode*: HtmlNode
     parentElement*: HtmlElement
 
-    rawtext*: ustring
-    fmttext*: ustring
+    rawtext*: string
+    fmttext*: string
     x*: int
     y*: int
     width*: int
@@ -130,12 +130,6 @@ func getFmtLen*(htmlNode: HtmlNode): int =
 func getRawLen*(htmlNode: HtmlNode): int =
   return htmlNode.rawtext.len
 
-func visibleNode*(node: HtmlNode): bool =
-  case node.nodeType
-  of NODE_TEXT: return true
-  of NODE_ELEMENT: return true
-  else: return false
-
 func toInputType*(str: string): InputType =
   case str
   of "button": INPUT_BUTTON
@@ -170,63 +164,61 @@ func toInputSize*(str: string): int =
       return 20
   return str.parseInt()
 
-func getFmtInput(inputElement: HtmlInputElement): ustring =
+func getFmtInput(inputElement: HtmlInputElement): string =
   case inputElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
-    let valueFit = fitValueToSize(inputElement.value.toRunes(), inputElement.size)
+    let valueFit = fitValueToSize(inputElement.value, inputElement.size)
     return valueFit.ansiStyle(styleUnderscore).ansiReset().buttonFmt()
   of INPUT_SUBMIT:
-    return inputElement.value.toRunes().buttonFmt()
+    return inputElement.value.buttonFmt()
   else: discard
 
-func getRawInput(inputElement: HtmlInputElement): ustring =
+func getRawInput(inputElement: HtmlInputElement): string =
   case inputElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
-    return inputElement.value.toRunes().fitValueToSize(inputElement.size).buttonFmt()
+    return inputElement.value.fitValueToSize(inputElement.size).buttonRaw()
   of INPUT_SUBMIT:
-    return inputElement.value.toRunes().buttonFmt()
+    return inputElement.value.buttonRaw()
   else: discard
 
-func getParent*(htmlNode: HtmlNode, tagType: TagType): HtmlElement =
-  var pnode = htmlNode.parentElement
-  while pnode != nil and pnode.tagType != tagType:
-    pnode = pnode.parentElement
-  
-  return pnode
+func ancestor*(htmlNode: HtmlNode, tagType: TagType): HtmlElement =
+  result = htmlNode.parentElement
+  while result != nil and result.tagType != tagType:
+    result = result.parentElement
 
-proc getRawText*(htmlNode: HtmlNode): ustring =
+proc getRawText*(htmlNode: HtmlNode): string =
   if htmlNode.isElemNode():
     case HtmlElement(htmlNode).tagType
     of TAG_INPUT: return HtmlInputElement(htmlNode).getRawInput()
-    else: return @[]
+    else: return ""
   elif htmlNode.isTextNode():
     if htmlNode.parentElement != nil and htmlNode.parentElement.tagType != TAG_PRE:
-      result = htmlNode.rawtext.remove(runeNewline)
+      result = htmlNode.rawtext.remove("\n")
       if unicode.strip($result).toRunes().len > 0:
         if htmlNode.nodeAttr().display != DISPLAY_INLINE:
           if htmlNode.previousSibling == nil or htmlNode.previousSibling.nodeAttr().display != DISPLAY_INLINE:
-            result = unicode.strip($result, true, false).toRunes()
+            result = unicode.strip(result, true, false)
           if htmlNode.nextSibling == nil or htmlNode.nextSibling.nodeAttr().display != DISPLAY_INLINE:
-            result = unicode.strip($result, false, true).toRunes()
+            result = unicode.strip(result, false, true)
       else:
-        result = @[]
+        result = ""
     else:
-      result = unicode.strip($htmlNode.rawtext).toRunes()
+      result = unicode.strip(htmlNode.rawtext)
     if htmlNode.parentElement != nil and htmlNode.parentElement.tagType == TAG_OPTION:
       result = result.buttonRaw()
   else:
     assert(false)
 
-func getFmtText*(htmlNode: HtmlNode): ustring =
+func getFmtText*(htmlNode: HtmlNode): string =
   if htmlNode.isElemNode():
     case HtmlElement(htmlNode).tagType
     of TAG_INPUT: return HtmlInputElement(htmlNode).getFmtInput()
-    else: return @[]
+    else: return ""
   elif htmlNode.isTextNode():
     result = htmlNode.rawtext
     if htmlNode.parentElement != nil and htmlNode.parentElement.islink:
       result = result.ansiFgColor(fgBlue)
-      let parent = HtmlElement(htmlNode.parentNode).getParent(TAG_A)
+      let parent = HtmlElement(htmlNode.parentNode).ancestor(TAG_A)
       if parent != nil and parent.selected:
         result = result.ansiStyle(styleUnderscore).ansiReset()
 
@@ -334,15 +326,15 @@ proc getHtmlNode*(xmlElement: XmlNode, parent: HtmlNode): HtmlNode =
   of xnText:
     new(result)
     result.nodeType = NODE_TEXT
-    result.rawtext = xmlElement.text.toRunes()
+    result.rawtext = xmlElement.text
   of xnComment:
     new(result)
     result.nodeType = NODE_COMMENT
-    result.rawtext = xmlElement.text.toRunes()
+    result.rawtext = xmlElement.text
   of xnCData:
     new(result)
     result.nodeType = NODE_CDATA
-    result.rawtext = xmlElement.text.toRunes()
+    result.rawtext = xmlElement.text
   else: assert(false)
 
   result.parentNode = parent
diff --git a/keymap b/keymap
index b63600f7..51708c8e 100644
--- a/keymap
+++ b/keymap
@@ -53,3 +53,4 @@ lemap C-f ACTION_LINED_FORWARD
 lemap C-u ACTION_LINED_CLEAR
 lemap C-k ACTION_LINED_KILL
 lemap C-w ACTION_LINED_KILL_WORD
+lemap C-v ACTION_LINED_ESC
diff --git a/twtio.nim b/twtio.nim
index 4c02cfb0..32e4d55c 100644
--- a/twtio.nim
+++ b/twtio.nim
@@ -1,6 +1,7 @@
 import terminal
 import tables
 import strutils
+import unicode
 
 import twtstr
 import config
@@ -9,6 +10,13 @@ template print*(s: varargs[string, `$`]) =
   for x in s:
     stdout.write(x)
 
+template printesc*(s: string) =
+  for ruby in s:
+    if ($ruby)[0].isControlChar():
+      stdout.write(($($ruby)[0].getControlLetter()).ansiFgColor(fgBlue).ansiStyle(styleBright).ansiReset())
+    else:
+      stdout.write($ruby)
+
 template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
   var a = false
   for x in s:
@@ -36,10 +44,11 @@ proc readLine*(prompt: string, current: var string): bool =
   var new = current
   print(prompt)
   print(' ')
-  print(new)
+  printesc(new)
   var s = ""
   var feedNext = false
-  var cursor = new.len
+  var escNext = false
+  var cursor = new.runeLen
   while true:
     if not feedNext:
       s = ""
@@ -47,7 +56,10 @@ proc readLine*(prompt: string, current: var string): bool =
       feedNext = false
     let c = getch()
     s &= c
-    let action = getLinedAction(s)
+    var rl = new.runeLen()
+    var action = getLinedAction(s)
+    if escNext:
+      action = NO_ACTION
     case action
     of ACTION_LINED_CANCEL:
       return false
@@ -56,70 +68,95 @@ proc readLine*(prompt: string, current: var string): bool =
       return true
     of ACTION_LINED_BACKSPACE:
       if cursor > 0:
-        print(' '.repeat(new.len - cursor + 1))
-        print('\b'.repeat(new.len - cursor + 1))
+        print(' '.repeat(rl - cursor + 1))
+        print('\b'.repeat(rl - cursor + 1))
         print("\b \b")
-        new = new.substr(0, cursor - 2) & new.substr(cursor, new.len)
+        new = new.runeSubstr(0, cursor - 1) & new.runeSubstr(cursor, rl)
+        rl = new.runeLen()
         cursor -= 1
-        print(new.substr(cursor, new.len))
-        print('\b'.repeat(new.len - cursor))
+        printesc(new.runeSubstr(cursor, rl))
+        print('\b'.repeat(rl - cursor))
     of ACTION_LINED_ESC:
-      new &= c
-      print("^[".ansiFgColor(fgBlue).ansiStyle(styleBright).ansiReset())
+      escNext = true
     of ACTION_LINED_CLEAR:
-      print(' '.repeat(new.len - cursor + 1))
-      print('\b'.repeat(new.len - cursor + 1))
+      print(' '.repeat(rl - cursor + 1))
+      print('\b'.repeat(rl - cursor + 1))
       print('\b'.repeat(cursor))
       print(' '.repeat(cursor))
       print('\b'.repeat(cursor))
-      new = new.substr(cursor, new.len)
-      print(new)
-      print('\b'.repeat(new.len))
+      new = new.runeSubstr(cursor, rl)
+      rl = new.runeLen()
+      printesc(new)
+      print('\b'.repeat(rl))
       cursor = 0
     of ACTION_LINED_KILL:
-      print(' '.repeat(new.len - cursor + 1))
-      print('\b'.repeat(new.len - cursor + 1))
-      new = new.substr(0, cursor - 1)
+      print(' '.repeat(rl - cursor + 1))
+      print('\b'.repeat(rl - cursor + 1))
+      new = new.runeSubstr(0, cursor)
     of ACTION_LINED_BACK:
       if cursor > 0:
         cursor -= 1
         print("\b")
     of ACTION_LINED_FORWARD:
-      if cursor < new.len:
-        print(new[cursor])
+      if cursor < rl:
+        var rune: Rune
+        new.fastRuneAt(cursor, rune, false)
+        printesc($rune)
         cursor += 1
     of ACTION_LINED_PREV_WORD:
       while cursor > 0:
         print('\b')
         cursor -= 1
-        if new[cursor] == ' ':
+        var rune: Rune
+        new.fastRuneAt(cursor, rune, false)
+        if rune == runeSpace:
           break
     of ACTION_LINED_NEXT_WORD:
-      while cursor < new.len:
-        print(new[cursor])
+      while cursor < rl:
+        var rune: Rune
+        new.fastRuneAt(cursor, rune, false)
+        printesc($rune)
         cursor += 1
-        if cursor < new.len and new[cursor] == ' ':
-          break
+        if cursor < rl:
+          new.fastRuneAt(cursor, rune, false)
+          if rune == runeSpace:
+            break
     of ACTION_LINED_KILL_WORD:
       var chars = 0
       while cursor > chars:
         chars += 1
-        if new[cursor - chars] == ' ':
+        var rune: Rune
+        new.fastRuneAt(cursor - chars, rune, false)
+        if rune == runeSpace:
           break
       if chars > 0:
-        print(' '.repeat(new.len - cursor + 1))
-        print('\b'.repeat(new.len - cursor + 1))
+        print(' '.repeat(rl - cursor + 1))
+        print('\b'.repeat(rl - cursor + 1))
         print("\b \b".repeat(chars))
-        new = new.substr(0, cursor - 1 - chars) & new.substr(cursor, new.len)
+        new = new.runeSubstr(0, cursor - chars) & new.runeSubstr(cursor, rl)
+        rl = new.runeLen()
         cursor -= chars
-        print(new.substr(cursor, new.len))
-        print('\b'.repeat(new.len - cursor))
+        printesc(new.runeSubstr(cursor, rl))
+        print('\b'.repeat(rl - cursor))
     of ACTION_FEED_NEXT:
       feedNext = true
-    else:
-      print(' '.repeat(new.len - cursor + 1))
-      print('\b'.repeat(new.len - cursor + 1))
-      new = new.substr(0, cursor - 1) & c & new.substr(cursor, new.len)
-      print(new.substr(cursor, new.len))
-      print('\b'.repeat(new.len - cursor - 1))
+    elif validateUtf8(s) == -1:
+      var cs = ""
+      for c in s:
+        if not c.isControlChar():
+          cs &= c
+        elif escNext:
+          cs &= c
+          escNext = false
+      escNext = false
+      if cs.len == 0:
+        continue
+      print(' '.repeat(rl - cursor + 1))
+      print('\b'.repeat(rl - cursor + 1))
+      new = new.runeSubstr(0, cursor) & cs & new.runeSubstr(cursor, rl)
+      rl = new.runeLen()
+      printesc(new.runeSubstr(cursor, rl))
+      print('\b'.repeat(rl - cursor - 1))
       cursor += 1
+    else:
+      feedNext = true
diff --git a/twtstr.nim b/twtstr.nim
index 50f0e4da..85035a61 100644
--- a/twtstr.nim
+++ b/twtstr.nim
@@ -2,59 +2,52 @@ import terminal
 import strutils
 import unicode
 
-type ustring* = seq[Rune]
-
-const runeSpace*: Rune = " ".toRunes()[0]
-const runeNewline*: Rune = "\n".toRunes()[0]
-const runeReturn*: Rune = "\r".toRunes()[0]
-
-func isWhitespace(r: Rune): bool =
-  case r
-  of runeSpace, runeNewline, runeReturn: return true
-  else: return false
+const runeSpace* = " ".runeAt(0)
 
 func ansiStyle*(str: string, style: Style): string =
-  return ansiStyleCode(style) & str
+  return ansiStyleCode(style) & str & "\e[0m"
 
 func ansiFgColor*(str: string, color: ForegroundColor): string =
-  return ansiForegroundColorCode(color) & str
+  return ansiForegroundColorCode(color) & str & ansiResetCode
 
 func ansiReset*(str: string): string =
   return str & ansiResetCode
 
-func ansiStyle*(str: ustring, style: Style): ustring =
-  return ansiStyleCode(style).toRunes() & str
-
-func ansiFgColor*(str: ustring, color: ForegroundColor): ustring =
-  return ansiForegroundColorCode(color).toRunes & str
-
-func ansiReset*(str: ustring): ustring =
-  return str & ansiResetCode.toRunes()
-
-func maxString*(str: ustring, max: int): ustring =
-  result = str
-  if max < str.len:
-    result.setLen(max - 2)
-    result &= "$".toRunes()
-
 func maxString*(str: string, max: int): string =
-  result = str
   if max < str.len:
-    result.setLen(max - 1)
-    result[max - 2] = '$'
+    return str.substr(0, max - 2) & "$"
+  return str
 
-func fitValueToSize*(str: ustring, size: int): ustring =
-  if str.len < size:
-    return str & ' '.repeat(size - str.len).toRunes()
+func fitValueToSize*(str: string, size: int): string =
+  if str.runeLen < size:
+    return str & ' '.repeat(size - str.runeLen)
   return str.maxString(size)
 
-func buttonFmt*(str: ustring): ustring =
-  return "[".toRunes().ansiFgColor(fgRed).ansiReset() & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).toRunes().ansiReset()
-
-func buttonRaw*(str: ustring): ustring = 
-  return "[".toRunes() & str & "]".toRunes()
-
-func remove*(s: ustring, r: Rune): ustring =
-  for c in s:
-    if c != r:
-      result.add(c)
+func buttonFmt*(str: string): string =
+  return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset()
+
+func buttonRaw*(str: string): string =
+  return "[" & str & "]"
+
+func remove*(str: string, c: string): string =
+  let rem = c.toRunes()[0]
+  for rune in str.runes:
+    if rem != rune:
+      result &= $rune
+
+func isControlChar*(c: char): bool =
+  return int(c) <= 0x1F or int(c) == 0x7F
+
+func getControlChar*(c: char): char =
+  if int(c) >= int('a'):
+    return char(int(c) - int('a') + 1)
+  elif c == '?':
+    return char(127)
+  assert(false)
+
+func getControlLetter*(c: char): char =
+  if int(c) <= 0x1F:
+    return char(int(c) + int('A') - 1)
+  elif c == '\x7F':
+    return '?'
+  assert(false)