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