view.go

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