1package main 2 3import ( 4 "github.com/mattn/go-runewidth" 5 "github.com/nsf/termbox-go" 6 "unicode/utf8" 7) 8 9func tbprint(x, y int, fg, bg termbox.Attribute, msg string) { 10 for _, c := range msg { 11 termbox.SetCell(x, y, c, fg, bg) 12 x += runewidth.RuneWidth(c) 13 } 14} 15 16func fill(x, y, w, h int, cell termbox.Cell) { 17 for ly := 0; ly < h; ly++ { 18 for lx := 0; lx < w; lx++ { 19 termbox.SetCell(x+lx, y+ly, cell.Ch, cell.Fg, cell.Bg) 20 } 21 } 22} 23 24func rune_advance_len(r rune, pos int) int { 25 if r == '\t' { 26 return tabstop_length - pos%tabstop_length 27 } 28 return runewidth.RuneWidth(r) 29} 30 31func voffset_coffset(text []byte, boffset int) (voffset, coffset int) { 32 text = text[:boffset] 33 for len(text) > 0 { 34 r, size := utf8.DecodeRune(text) 35 text = text[size:] 36 coffset += 1 37 voffset += rune_advance_len(r, voffset) 38 } 39 return 40} 41 42func byte_slice_grow(s []byte, desired_cap int) []byte { 43 if cap(s) < desired_cap { 44 ns := make([]byte, len(s), desired_cap) 45 copy(ns, s) 46 return ns 47 } 48 return s 49} 50 51func byte_slice_remove(text []byte, from, to int) []byte { 52 size := to - from 53 copy(text[from:], text[to:]) 54 text = text[:len(text)-size] 55 return text 56} 57 58func byte_slice_insert(text []byte, offset int, what []byte) []byte { 59 n := len(text) + len(what) 60 text = byte_slice_grow(text, n) 61 text = text[:n] 62 copy(text[offset+len(what):], text[offset:]) 63 copy(text[offset:], what) 64 return text 65} 66 67const preferred_horizontal_threshold = 5 68const tabstop_length = 8 69 70type EditBox struct { 71 text []byte 72 line_voffset int 73 cursor_boffset int // cursor offset in bytes 74 cursor_voffset int // visual cursor offset in termbox cells 75 cursor_coffset int // cursor offset in unicode code points 76} 77 78// Draws the EditBox in the given location, 'h' is not used at the moment 79func (eb *EditBox) Draw(x, y, w, h int) { 80 eb.AdjustVOffset(w) 81 82 const coldef = termbox.ColorDefault 83 fill(x, y, w, h, termbox.Cell{Ch: ' '}) 84 85 t := eb.text 86 lx := 0 87 tabstop := 0 88 for { 89 rx := lx - eb.line_voffset 90 if len(t) == 0 { 91 break 92 } 93 94 if lx == tabstop { 95 tabstop += tabstop_length 96 } 97 98 if rx >= w { 99 termbox.SetCell(x+w-1, y, '→', 100 coldef, coldef) 101 break 102 } 103 104 r, size := utf8.DecodeRune(t) 105 if r == '\t' { 106 for ; lx < tabstop; lx++ { 107 rx = lx - eb.line_voffset 108 if rx >= w { 109 goto next 110 } 111 112 if rx >= 0 { 113 termbox.SetCell(x+rx, y, ' ', coldef, coldef) 114 } 115 } 116 } else { 117 if rx >= 0 { 118 termbox.SetCell(x+rx, y, r, coldef, coldef) 119 } 120 lx += runewidth.RuneWidth(r) 121 } 122 next: 123 t = t[size:] 124 } 125 126 if eb.line_voffset != 0 { 127 termbox.SetCell(x, y, '←', coldef, coldef) 128 } 129} 130 131// Adjusts line visual offset to a proper value depending on width 132func (eb *EditBox) AdjustVOffset(width int) { 133 ht := preferred_horizontal_threshold 134 max_h_threshold := (width - 1) / 2 135 if ht > max_h_threshold { 136 ht = max_h_threshold 137 } 138 139 threshold := width - 1 140 if eb.line_voffset != 0 { 141 threshold = width - ht 142 } 143 if eb.cursor_voffset-eb.line_voffset >= threshold { 144 eb.line_voffset = eb.cursor_voffset + (ht - width + 1) 145 } 146 147 if eb.line_voffset != 0 && eb.cursor_voffset-eb.line_voffset < ht { 148 eb.line_voffset = eb.cursor_voffset - ht 149 if eb.line_voffset < 0 { 150 eb.line_voffset = 0 151 } 152 } 153} 154 155func (eb *EditBox) MoveCursorTo(boffset int) { 156 eb.cursor_boffset = boffset 157 eb.cursor_voffset, eb.cursor_coffset = voffset_coffset(eb.text, boffset) 158} 159 160func (eb *EditBox) RuneUnderCursor() (rune, int) { 161 return utf8.DecodeRune(eb.text[eb.cursor_boffset:]) 162} 163 164func (eb *EditBox) RuneBeforeCursor() (rune, int) { 165 return utf8.DecodeLastRune(eb.text[:eb.cursor_boffset]) 166} 167 168func (eb *EditBox) MoveCursorOneRuneBackward() { 169 if eb.cursor_boffset == 0 { 170 return 171 } 172 _, size := eb.RuneBeforeCursor() 173 eb.MoveCursorTo(eb.cursor_boffset - size) 174} 175 176func (eb *EditBox) MoveCursorOneRuneForward() { 177 if eb.cursor_boffset == len(eb.text) { 178 return 179 } 180 _, size := eb.RuneUnderCursor() 181 eb.MoveCursorTo(eb.cursor_boffset + size) 182} 183 184func (eb *EditBox) MoveCursorToBeginningOfTheLine() { 185 eb.MoveCursorTo(0) 186} 187 188func (eb *EditBox) MoveCursorToEndOfTheLine() { 189 eb.MoveCursorTo(len(eb.text)) 190} 191 192func (eb *EditBox) DeleteRuneBackward() { 193 if eb.cursor_boffset == 0 { 194 return 195 } 196 197 eb.MoveCursorOneRuneBackward() 198 _, size := eb.RuneUnderCursor() 199 eb.text = byte_slice_remove(eb.text, eb.cursor_boffset, eb.cursor_boffset+size) 200} 201 202func (eb *EditBox) DeleteRuneForward() { 203 if eb.cursor_boffset == len(eb.text) { 204 return 205 } 206 _, size := eb.RuneUnderCursor() 207 eb.text = byte_slice_remove(eb.text, eb.cursor_boffset, eb.cursor_boffset+size) 208} 209 210func (eb *EditBox) DeleteTheRestOfTheLine() { 211 eb.text = eb.text[:eb.cursor_boffset] 212} 213 214func (eb *EditBox) InsertRune(r rune) { 215 var buf [utf8.UTFMax]byte 216 n := utf8.EncodeRune(buf[:], r) 217 eb.text = byte_slice_insert(eb.text, eb.cursor_boffset, buf[:n]) 218 eb.MoveCursorOneRuneForward() 219} 220 221// Please, keep in mind that cursor depends on the value of line_voffset, which 222// is being set on Draw() call, so.. call this method after Draw() one. 223func (eb *EditBox) CursorX() int { 224 return eb.cursor_voffset - eb.line_voffset 225} 226 227var edit_box EditBox 228 229const edit_box_width = 30 230 231func redraw_all() { 232 const coldef = termbox.ColorDefault 233 termbox.Clear(coldef, coldef) 234 w, h := termbox.Size() 235 236 midy := h / 2 237 midx := (w - edit_box_width) / 2 238 239 // unicode box drawing chars around the edit box 240 termbox.SetCell(midx-1, midy, '│', coldef, coldef) 241 termbox.SetCell(midx+edit_box_width, midy, '│', coldef, coldef) 242 termbox.SetCell(midx-1, midy-1, '┌', coldef, coldef) 243 termbox.SetCell(midx-1, midy+1, '└', coldef, coldef) 244 termbox.SetCell(midx+edit_box_width, midy-1, '┐', coldef, coldef) 245 termbox.SetCell(midx+edit_box_width, midy+1, '┘', coldef, coldef) 246 fill(midx, midy-1, edit_box_width, 1, termbox.Cell{Ch: '─'}) 247 fill(midx, midy+1, edit_box_width, 1, termbox.Cell{Ch: '─'}) 248 249 edit_box.Draw(midx, midy, edit_box_width, 1) 250 termbox.SetCursor(midx+edit_box.CursorX(), midy) 251 252 tbprint(midx+6, midy+3, coldef, coldef, "Press ESC to quit") 253 termbox.Flush() 254} 255 256func main() { 257 err := termbox.Init() 258 if err != nil { 259 panic(err) 260 } 261 defer termbox.Close() 262 termbox.SetInputMode(termbox.InputEsc) 263 264 redraw_all() 265mainloop: 266 for { 267 switch ev := termbox.PollEvent(); ev.Type { 268 case termbox.EventKey: 269 switch ev.Key { 270 case termbox.KeyEsc: 271 break mainloop 272 case termbox.KeyArrowLeft, termbox.KeyCtrlB: 273 edit_box.MoveCursorOneRuneBackward() 274 case termbox.KeyArrowRight, termbox.KeyCtrlF: 275 edit_box.MoveCursorOneRuneForward() 276 case termbox.KeyBackspace, termbox.KeyBackspace2: 277 edit_box.DeleteRuneBackward() 278 case termbox.KeyDelete, termbox.KeyCtrlD: 279 edit_box.DeleteRuneForward() 280 case termbox.KeyTab: 281 edit_box.InsertRune('\t') 282 case termbox.KeySpace: 283 edit_box.InsertRune(' ') 284 case termbox.KeyCtrlK: 285 edit_box.DeleteTheRestOfTheLine() 286 case termbox.KeyHome, termbox.KeyCtrlA: 287 edit_box.MoveCursorToBeginningOfTheLine() 288 case termbox.KeyEnd, termbox.KeyCtrlE: 289 edit_box.MoveCursorToEndOfTheLine() 290 default: 291 if ev.Ch != 0 { 292 edit_box.InsertRune(ev.Ch) 293 } 294 } 295 case termbox.EventError: 296 panic(ev.Err) 297 } 298 redraw_all() 299 } 300} 301