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