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}