1// Copyright 2014 The gocui Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package gocui
6
7import (
8	"bytes"
9	"errors"
10	"io"
11	"strings"
12
13	"github.com/miguelmota/termbox-go"
14)
15
16// A View is a window. It maintains its own internal buffer and cursor
17// position.
18type View struct {
19	name           string
20	x0, y0, x1, y1 int
21	ox, oy         int
22	cx, cy         int
23	lines          [][]cell
24	readOffset     int
25	readCache      string
26
27	tainted   bool       // marks if the viewBuffer must be updated
28	viewLines []viewLine // internal representation of the view's buffer
29
30	ei *escapeInterpreter // used to decode ESC sequences on Write
31
32	// BgColor and FgColor allow to configure the background and foreground
33	// colors of the View.
34	BgColor, FgColor Attribute
35
36	// SelBgColor and SelFgColor are used to configure the background and
37	// foreground colors of the selected line, when it is highlighted.
38	SelBgColor, SelFgColor Attribute
39
40	// If Editable is true, keystrokes will be added to the view's internal
41	// buffer at the cursor position.
42	Editable bool
43
44	// Editor allows to define the editor that manages the edition mode,
45	// including keybindings or cursor behaviour. DefaultEditor is used by
46	// default.
47	Editor Editor
48
49	// Overwrite enables or disables the overwrite mode of the view.
50	Overwrite bool
51
52	// If Highlight is true, Sel{Bg,Fg}Colors will be used
53	// for the line under the cursor position.
54	Highlight bool
55
56	// If Frame is true, a border will be drawn around the view.
57	Frame bool
58
59	// If Wrap is true, the content that is written to this View is
60	// automatically wrapped when it is longer than its width. If true the
61	// view's x-origin will be ignored.
62	Wrap bool
63
64	// If Autoscroll is true, the View will automatically scroll down when the
65	// text overflows. If true the view's y-origin will be ignored.
66	Autoscroll bool
67
68	// If Frame is true, Title allows to configure a title for the view.
69	Title string
70
71	// If Mask is true, the View will display the mask instead of the real
72	// content
73	Mask rune
74}
75
76type viewLine struct {
77	linesX, linesY int // coordinates relative to v.lines
78	line           []cell
79}
80
81type cell struct {
82	chr              rune
83	bgColor, fgColor Attribute
84}
85
86type lineType []cell
87
88// String returns a string from a given cell slice.
89func (l lineType) String() string {
90	str := ""
91	for _, c := range l {
92		str += string(c.chr)
93	}
94	return str
95}
96
97// newView returns a new View object.
98func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
99	v := &View{
100		name:    name,
101		x0:      x0,
102		y0:      y0,
103		x1:      x1,
104		y1:      y1,
105		Frame:   true,
106		Editor:  DefaultEditor,
107		tainted: true,
108		ei:      newEscapeInterpreter(mode),
109	}
110	return v
111}
112
113// Size returns the number of visible columns and rows in the View.
114func (v *View) Size() (x, y int) {
115	return v.x1 - v.x0 - 1, v.y1 - v.y0 - 1
116}
117
118// Name returns the name of the view.
119func (v *View) Name() string {
120	return v.name
121}
122
123// setRune sets a rune at the given point relative to the view. It applies the
124// specified colors, taking into account if the cell must be highlighted. Also,
125// it checks if the position is valid.
126func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
127	maxX, maxY := v.Size()
128	if x < 0 || x >= maxX || y < 0 || y >= maxY {
129		return errors.New("invalid point")
130	}
131
132	var (
133		ry, rcy int
134		err     error
135	)
136	if v.Highlight {
137		_, ry, err = v.realPosition(x, y)
138		if err != nil {
139			return err
140		}
141		_, rcy, err = v.realPosition(v.cx, v.cy)
142		if err != nil {
143			return err
144		}
145	}
146
147	if v.Mask != 0 {
148		fgColor = v.FgColor
149		bgColor = v.BgColor
150		ch = v.Mask
151	} else if v.Highlight && ry == rcy {
152		fgColor = v.SelFgColor
153		bgColor = v.SelBgColor
154	}
155
156	termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
157		termbox.Attribute(fgColor), termbox.Attribute(bgColor))
158
159	return nil
160}
161
162// SetCursor sets the cursor position of the view at the given point,
163// relative to the view. It checks if the position is valid.
164func (v *View) SetCursor(x, y int) error {
165	maxX, maxY := v.Size()
166	if x < 0 || x >= maxX || y < 0 || y >= maxY {
167		return errors.New("invalid point")
168	}
169	v.cx = x
170	v.cy = y
171	return nil
172}
173
174// Cursor returns the cursor position of the view.
175func (v *View) Cursor() (x, y int) {
176	return v.cx, v.cy
177}
178
179// SetOrigin sets the origin position of the view's internal buffer,
180// so the buffer starts to be printed from this point, which means that
181// it is linked with the origin point of view. It can be used to
182// implement Horizontal and Vertical scrolling with just incrementing
183// or decrementing ox and oy.
184func (v *View) SetOrigin(x, y int) error {
185	if x < 0 || y < 0 {
186		return errors.New("invalid point")
187	}
188	v.ox = x
189	v.oy = y
190	return nil
191}
192
193// Origin returns the origin position of the view.
194func (v *View) Origin() (x, y int) {
195	return v.ox, v.oy
196}
197
198// Write appends a byte slice into the view's internal buffer. Because
199// View implements the io.Writer interface, it can be passed as parameter
200// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must
201// be called to clear the view's buffer.
202func (v *View) Write(p []byte) (n int, err error) {
203	v.tainted = true
204
205	for _, ch := range bytes.Runes(p) {
206		switch ch {
207		case '\n':
208			v.lines = append(v.lines, nil)
209		case '\r':
210			nl := len(v.lines)
211			if nl > 0 {
212				v.lines[nl-1] = nil
213			} else {
214				v.lines = make([][]cell, 1)
215			}
216		default:
217			cells := v.parseInput(ch)
218			if cells == nil {
219				continue
220			}
221
222			nl := len(v.lines)
223			if nl > 0 {
224				v.lines[nl-1] = append(v.lines[nl-1], cells...)
225			} else {
226				v.lines = append(v.lines, cells)
227			}
228		}
229	}
230	return len(p), nil
231}
232
233// parseInput parses char by char the input written to the View. It returns nil
234// while processing ESC sequences. Otherwise, it returns a cell slice that
235// contains the processed data.
236func (v *View) parseInput(ch rune) []cell {
237	cells := []cell{}
238
239	isEscape, err := v.ei.parseOne(ch)
240	if err != nil {
241		for _, r := range v.ei.runes() {
242			c := cell{
243				fgColor: v.FgColor,
244				bgColor: v.BgColor,
245				chr:     r,
246			}
247			cells = append(cells, c)
248		}
249		v.ei.reset()
250	} else {
251		if isEscape {
252			return nil
253		}
254		c := cell{
255			fgColor: v.ei.curFgColor,
256			bgColor: v.ei.curBgColor,
257			chr:     ch,
258		}
259		cells = append(cells, c)
260	}
261
262	return cells
263}
264
265// Read reads data into p. It returns the number of bytes read into p.
266// At EOF, err will be io.EOF. Calling Read() after Rewind() makes the
267// cache to be refreshed with the contents of the view.
268func (v *View) Read(p []byte) (n int, err error) {
269	if v.readOffset == 0 {
270		v.readCache = v.Buffer()
271	}
272	if v.readOffset < len(v.readCache) {
273		n = copy(p, v.readCache[v.readOffset:])
274		v.readOffset += n
275	} else {
276		err = io.EOF
277	}
278	return
279}
280
281// Rewind sets the offset for the next Read to 0, which also refresh the
282// read cache.
283func (v *View) Rewind() {
284	v.readOffset = 0
285}
286
287// draw re-draws the view's contents.
288func (v *View) draw() error {
289	maxX, maxY := v.Size()
290
291	if v.Wrap {
292		if maxX == 0 {
293			return errors.New("X size of the view cannot be 0")
294		}
295		v.ox = 0
296	}
297	if v.tainted {
298		v.viewLines = nil
299		for i, line := range v.lines {
300			if v.Wrap {
301				if len(line) < maxX {
302					vline := viewLine{linesX: 0, linesY: i, line: line}
303					v.viewLines = append(v.viewLines, vline)
304					continue
305				} else {
306					for n := 0; n <= len(line); n += maxX {
307						if len(line[n:]) <= maxX {
308							vline := viewLine{linesX: n, linesY: i, line: line[n:]}
309							v.viewLines = append(v.viewLines, vline)
310						} else {
311							vline := viewLine{linesX: n, linesY: i, line: line[n : n+maxX]}
312							v.viewLines = append(v.viewLines, vline)
313						}
314					}
315				}
316			} else {
317				vline := viewLine{linesX: 0, linesY: i, line: line}
318				v.viewLines = append(v.viewLines, vline)
319			}
320		}
321		v.tainted = false
322	}
323
324	if v.Autoscroll && len(v.viewLines) > maxY {
325		v.oy = len(v.viewLines) - maxY
326	}
327	y := 0
328	for i, vline := range v.viewLines {
329		if i < v.oy {
330			continue
331		}
332		if y >= maxY {
333			break
334		}
335		x := 0
336		for j, c := range vline.line {
337			if j < v.ox {
338				continue
339			}
340			if x >= maxX {
341				break
342			}
343
344			fgColor := c.fgColor
345			if fgColor == ColorDefault {
346				fgColor = v.FgColor
347			}
348			bgColor := c.bgColor
349			if bgColor == ColorDefault {
350				bgColor = v.BgColor
351			}
352
353			if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
354				return err
355			}
356			x++
357		}
358		y++
359	}
360	return nil
361}
362
363// realPosition returns the position in the internal buffer corresponding to the
364// point (x, y) of the view.
365func (v *View) realPosition(vx, vy int) (x, y int, err error) {
366	vx = v.ox + vx
367	vy = v.oy + vy
368
369	if vx < 0 || vy < 0 {
370		return 0, 0, errors.New("invalid point")
371	}
372
373	if len(v.viewLines) == 0 {
374		return vx, vy, nil
375	}
376
377	if vy < len(v.viewLines) {
378		vline := v.viewLines[vy]
379		x = vline.linesX + vx
380		y = vline.linesY
381	} else {
382		vline := v.viewLines[len(v.viewLines)-1]
383		x = vx
384		y = vline.linesY + vy - len(v.viewLines) + 1
385	}
386
387	return x, y, nil
388}
389
390// Clear empties the view's internal buffer.
391func (v *View) Clear() {
392	v.tainted = true
393
394	v.lines = nil
395	v.viewLines = nil
396	v.readOffset = 0
397	v.clearRunes()
398}
399
400// clearRunes erases all the cells in the view.
401func (v *View) clearRunes() {
402	maxX, maxY := v.Size()
403	for x := 0; x < maxX; x++ {
404		for y := 0; y < maxY; y++ {
405			termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ',
406				termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
407		}
408	}
409}
410
411// BufferLines returns the lines in the view's internal
412// buffer.
413func (v *View) BufferLines() []string {
414	lines := make([]string, len(v.lines))
415	for i, l := range v.lines {
416		str := lineType(l).String()
417		str = strings.Replace(str, "\x00", " ", -1)
418		lines[i] = str
419	}
420	return lines
421}
422
423// Buffer returns a string with the contents of the view's internal
424// buffer.
425func (v *View) Buffer() string {
426	str := ""
427	for _, l := range v.lines {
428		str += lineType(l).String() + "\n"
429	}
430	return strings.Replace(str, "\x00", " ", -1)
431}
432
433// ViewBufferLines returns the lines in the view's internal
434// buffer that is shown to the user.
435func (v *View) ViewBufferLines() []string {
436	lines := make([]string, len(v.viewLines))
437	for i, l := range v.viewLines {
438		str := lineType(l.line).String()
439		str = strings.Replace(str, "\x00", " ", -1)
440		lines[i] = str
441	}
442	return lines
443}
444
445// ViewBuffer returns a string with the contents of the view's buffer that is
446// shown to the user.
447func (v *View) ViewBuffer() string {
448	str := ""
449	for _, l := range v.viewLines {
450		str += lineType(l.line).String() + "\n"
451	}
452	return strings.Replace(str, "\x00", " ", -1)
453}
454
455// Line returns a string with the line of the view's internal buffer
456// at the position corresponding to the point (x, y).
457func (v *View) Line(y int) (string, error) {
458	_, y, err := v.realPosition(0, y)
459	if err != nil {
460		return "", err
461	}
462
463	if y < 0 || y >= len(v.lines) {
464		return "", errors.New("invalid point")
465	}
466
467	return lineType(v.lines[y]).String(), nil
468}
469
470// Word returns a string with the word of the view's internal buffer
471// at the position corresponding to the point (x, y).
472func (v *View) Word(x, y int) (string, error) {
473	x, y, err := v.realPosition(x, y)
474	if err != nil {
475		return "", err
476	}
477
478	if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
479		return "", errors.New("invalid point")
480	}
481
482	str := lineType(v.lines[y]).String()
483
484	nl := strings.LastIndexFunc(str[:x], indexFunc)
485	if nl == -1 {
486		nl = 0
487	} else {
488		nl = nl + 1
489	}
490	nr := strings.IndexFunc(str[x:], indexFunc)
491	if nr == -1 {
492		nr = len(str)
493	} else {
494		nr = nr + x
495	}
496	return string(str[nl:nr]), nil
497}
498
499// indexFunc allows to split lines by words taking into account spaces
500// and 0.
501func indexFunc(r rune) bool {
502	return r == ' ' || r == 0
503}
504