package main import ( "github.com/mattn/go-runewidth" "github.com/nsf/termbox-go" "unicode/utf8" ) func tbprint(x, y int, fg, bg termbox.Attribute, msg string) { for _, c := range msg { termbox.SetCell(x, y, c, fg, bg) x += runewidth.RuneWidth(c) } } func fill(x, y, w, h int, cell termbox.Cell) { for ly := 0; ly < h; ly++ { for lx := 0; lx < w; lx++ { termbox.SetCell(x+lx, y+ly, cell.Ch, cell.Fg, cell.Bg) } } } func rune_advance_len(r rune, pos int) int { if r == '\t' { return tabstop_length - pos%tabstop_length } return runewidth.RuneWidth(r) } func voffset_coffset(text []byte, boffset int) (voffset, coffset int) { text = text[:boffset] for len(text) > 0 { r, size := utf8.DecodeRune(text) text = text[size:] coffset += 1 voffset += rune_advance_len(r, voffset) } return } func byte_slice_grow(s []byte, desired_cap int) []byte { if cap(s) < desired_cap { ns := make([]byte, len(s), desired_cap) copy(ns, s) return ns } return s } func byte_slice_remove(text []byte, from, to int) []byte { size := to - from copy(text[from:], text[to:]) text = text[:len(text)-size] return text } func byte_slice_insert(text []byte, offset int, what []byte) []byte { n := len(text) + len(what) text = byte_slice_grow(text, n) text = text[:n] copy(text[offset+len(what):], text[offset:]) copy(text[offset:], what) return text } const preferred_horizontal_threshold = 5 const tabstop_length = 8 type EditBox struct { text []byte line_voffset int cursor_boffset int // cursor offset in bytes cursor_voffset int // visual cursor offset in termbox cells cursor_coffset int // cursor offset in unicode code points } // Draws the EditBox in the given location, 'h' is not used at the moment func (eb *EditBox) Draw(x, y, w, h int) { eb.AdjustVOffset(w) const coldef = termbox.ColorDefault fill(x, y, w, h, termbox.Cell{Ch: ' '}) t := eb.text lx := 0 tabstop := 0 for { rx := lx - eb.line_voffset if len(t) == 0 { break } if lx == tabstop { tabstop += tabstop_length } if rx >= w { termbox.SetCell(x+w-1, y, '→', coldef, coldef) break } r, size := utf8.DecodeRune(t) if r == '\t' { for ; lx < tabstop; lx++ { rx = lx - eb.line_voffset if rx >= w { goto next } if rx >= 0 { termbox.SetCell(x+rx, y, ' ', coldef, coldef) } } } else { if rx >= 0 { termbox.SetCell(x+rx, y, r, coldef, coldef) } lx += runewidth.RuneWidth(r) } next: t = t[size:] } if eb.line_voffset != 0 { termbox.SetCell(x, y, '←', coldef, coldef) } } // Adjusts line visual offset to a proper value depending on width func (eb *EditBox) AdjustVOffset(width int) { ht := preferred_horizontal_threshold max_h_threshold := (width - 1) / 2 if ht > max_h_threshold { ht = max_h_threshold } threshold := width - 1 if eb.line_voffset != 0 { threshold = width - ht } if eb.cursor_voffset-eb.line_voffset >= threshold { eb.line_voffset = eb.cursor_voffset + (ht - width + 1) } if eb.line_voffset != 0 && eb.cursor_voffset-eb.line_voffset < ht { eb.line_voffset = eb.cursor_voffset - ht if eb.line_voffset < 0 { eb.line_voffset = 0 } } } func (eb *EditBox) MoveCursorTo(boffset int) { eb.cursor_boffset = boffset eb.cursor_voffset, eb.cursor_coffset = voffset_coffset(eb.text, boffset) } func (eb *EditBox) RuneUnderCursor() (rune, int) { return utf8.DecodeRune(eb.text[eb.cursor_boffset:]) } func (eb *EditBox) RuneBeforeCursor() (rune, int) { return utf8.DecodeLastRune(eb.text[:eb.cursor_boffset]) } func (eb *EditBox) MoveCursorOneRuneBackward() { if eb.cursor_boffset == 0 { return } _, size := eb.RuneBeforeCursor() eb.MoveCursorTo(eb.cursor_boffset - size) } func (eb *EditBox) MoveCursorOneRuneForward() { if eb.cursor_boffset == len(eb.text) { return } _, size := eb.RuneUnderCursor() eb.MoveCursorTo(eb.cursor_boffset + size) } func (eb *EditBox) MoveCursorToBeginningOfTheLine() { eb.MoveCursorTo(0) } func (eb *EditBox) MoveCursorToEndOfTheLine() { eb.MoveCursorTo(len(eb.text)) } func (eb *EditBox) DeleteRuneBackward() { if eb.cursor_boffset == 0 { return } eb.MoveCursorOneRuneBackward() _, size := eb.RuneUnderCursor() eb.text = byte_slice_remove(eb.text, eb.cursor_boffset, eb.cursor_boffset+size) } func (eb *EditBox) DeleteRuneForward() { if eb.cursor_boffset == len(eb.text) { return } _, size := eb.RuneUnderCursor() eb.text = byte_slice_remove(eb.text, eb.cursor_boffset, eb.cursor_boffset+size) } func (eb *EditBox) DeleteTheRestOfTheLine() { eb.text = eb.text[:eb.cursor_boffset] } func (eb *EditBox) InsertRune(r rune) { var buf [utf8.UTFMax]byte n := utf8.EncodeRune(buf[:], r) eb.text = byte_slice_insert(eb.text, eb.cursor_boffset, buf[:n]) eb.MoveCursorOneRuneForward() } // Please, keep in mind that cursor depends on the value of line_voffset, which // is being set on Draw() call, so.. call this method after Draw() one. func (eb *EditBox) CursorX() int { return eb.cursor_voffset - eb.line_voffset } var edit_box EditBox const edit_box_width = 30 func redraw_all() { const coldef = termbox.ColorDefault termbox.Clear(coldef, coldef) w, h := termbox.Size() midy := h / 2 midx := (w - edit_box_width) / 2 // unicode box drawing chars around the edit box termbox.SetCell(midx-1, midy, '│', coldef, coldef) termbox.SetCell(midx+edit_box_width, midy, '│', coldef, coldef) termbox.SetCell(midx-1, midy-1, '┌', coldef, coldef) termbox.SetCell(midx-1, midy+1, '└', coldef, coldef) termbox.SetCell(midx+edit_box_width, midy-1, '┐', coldef, coldef) termbox.SetCell(midx+edit_box_width, midy+1, '┘', coldef, coldef) fill(midx, midy-1, edit_box_width, 1, termbox.Cell{Ch: '─'}) fill(midx, midy+1, edit_box_width, 1, termbox.Cell{Ch: '─'}) edit_box.Draw(midx, midy, edit_box_width, 1) termbox.SetCursor(midx+edit_box.CursorX(), midy) tbprint(midx+6, midy+3, coldef, coldef, "Press ESC to quit") termbox.Flush() } func main() { err := termbox.Init() if err != nil { panic(err) } defer termbox.Close() termbox.SetInputMode(termbox.InputEsc) redraw_all() mainloop: for { switch ev := termbox.PollEvent(); ev.Type { case termbox.EventKey: switch ev.Key { case termbox.KeyEsc: break mainloop case termbox.KeyArrowLeft, termbox.KeyCtrlB: edit_box.MoveCursorOneRuneBackward() case termbox.KeyArrowRight, termbox.KeyCtrlF: edit_box.MoveCursorOneRuneForward() case termbox.KeyBackspace, termbox.KeyBackspace2: edit_box.DeleteRuneBackward() case termbox.KeyDelete, termbox.KeyCtrlD: edit_box.DeleteRuneForward() case termbox.KeyTab: edit_box.InsertRune('\t') case termbox.KeySpace: edit_box.InsertRune(' ') case termbox.KeyCtrlK: edit_box.DeleteTheRestOfTheLine() case termbox.KeyHome, termbox.KeyCtrlA: edit_box.MoveCursorToBeginningOfTheLine() case termbox.KeyEnd, termbox.KeyCtrlE: edit_box.MoveCursorToEndOfTheLine() default: if ev.Ch != 0 { edit_box.InsertRune(ev.Ch) } } case termbox.EventError: panic(ev.Err) } redraw_all() } }