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 // 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}