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