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 "errors" 9 10 "github.com/nsf/termbox-go" 11) 12 13var ( 14 // ErrQuit is used to decide if the MainLoop finished successfully. 15 ErrQuit = errors.New("quit") 16 17 // ErrUnknownView allows to assert if a View must be initialized. 18 ErrUnknownView = errors.New("unknown view") 19) 20 21// OutputMode represents the terminal's output mode (8 or 256 colors). 22type OutputMode termbox.OutputMode 23 24const ( 25 // OutputNormal provides 8-colors terminal mode. 26 OutputNormal = OutputMode(termbox.OutputNormal) 27 28 // Output256 provides 256-colors terminal mode. 29 Output256 = OutputMode(termbox.Output256) 30) 31 32// Gui represents the whole User Interface, including the views, layouts 33// and keybindings. 34type Gui struct { 35 tbEvents chan termbox.Event 36 userEvents chan userEvent 37 views []*View 38 currentView *View 39 managers []Manager 40 keybindings []*keybinding 41 maxX, maxY int 42 outputMode OutputMode 43 44 // BgColor and FgColor allow to configure the background and foreground 45 // colors of the GUI. 46 BgColor, FgColor Attribute 47 48 // SelBgColor and SelFgColor allow to configure the background and 49 // foreground colors of the frame of the current view. 50 SelBgColor, SelFgColor Attribute 51 52 // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the 53 // frame of the current view. 54 Highlight bool 55 56 // If Cursor is true then the cursor is enabled. 57 Cursor bool 58 59 // If Mouse is true then mouse events will be enabled. 60 Mouse bool 61 62 // If InputEsc is true, when ESC sequence is in the buffer and it doesn't 63 // match any known sequence, ESC means KeyEsc. 64 InputEsc bool 65 66 // If ASCII is true then use ASCII instead of unicode to draw the 67 // interface. Using ASCII is more portable. 68 ASCII bool 69} 70 71// NewGui returns a new Gui object with a given output mode. 72func NewGui(mode OutputMode) (*Gui, error) { 73 if err := termbox.Init(); err != nil { 74 return nil, err 75 } 76 77 g := &Gui{} 78 79 g.outputMode = mode 80 termbox.SetOutputMode(termbox.OutputMode(mode)) 81 82 g.tbEvents = make(chan termbox.Event, 20) 83 g.userEvents = make(chan userEvent, 20) 84 85 g.maxX, g.maxY = termbox.Size() 86 87 g.BgColor, g.FgColor = ColorDefault, ColorDefault 88 g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault 89 90 return g, nil 91} 92 93// Close finalizes the library. It should be called after a successful 94// initialization and when gocui is not needed anymore. 95func (g *Gui) Close() { 96 termbox.Close() 97} 98 99// Size returns the terminal's size. 100func (g *Gui) Size() (x, y int) { 101 return g.maxX, g.maxY 102} 103 104// SetRune writes a rune at the given point, relative to the top-left 105// corner of the terminal. It checks if the position is valid and applies 106// the given colors. 107func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error { 108 if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { 109 return errors.New("invalid point") 110 } 111 termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor)) 112 return nil 113} 114 115// Rune returns the rune contained in the cell at the given position. 116// It checks if the position is valid. 117func (g *Gui) Rune(x, y int) (rune, error) { 118 if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { 119 return ' ', errors.New("invalid point") 120 } 121 c := termbox.CellBuffer()[y*g.maxX+x] 122 return c.Ch, nil 123} 124 125// SetView creates a new view with its top-left corner at (x0, y0) 126// and the bottom-right one at (x1, y1). If a view with the same name 127// already exists, its dimensions are updated; otherwise, the error 128// ErrUnknownView is returned, which allows to assert if the View must 129// be initialized. It checks if the position is valid. 130func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) { 131 if x0 >= x1 || y0 >= y1 { 132 return nil, errors.New("invalid dimensions") 133 } 134 if name == "" { 135 return nil, errors.New("invalid name") 136 } 137 138 if v, err := g.View(name); err == nil { 139 v.x0 = x0 140 v.y0 = y0 141 v.x1 = x1 142 v.y1 = y1 143 v.tainted = true 144 return v, nil 145 } 146 147 v := newView(name, x0, y0, x1, y1, g.outputMode) 148 v.BgColor, v.FgColor = g.BgColor, g.FgColor 149 v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor 150 g.views = append(g.views, v) 151 return v, ErrUnknownView 152} 153 154// SetViewOnTop sets the given view on top of the existing ones. 155func (g *Gui) SetViewOnTop(name string) (*View, error) { 156 for i, v := range g.views { 157 if v.name == name { 158 s := append(g.views[:i], g.views[i+1:]...) 159 g.views = append(s, v) 160 return v, nil 161 } 162 } 163 return nil, ErrUnknownView 164} 165 166// SetViewOnBottom sets the given view on bottom of the existing ones. 167func (g *Gui) SetViewOnBottom(name string) (*View, error) { 168 for i, v := range g.views { 169 if v.name == name { 170 s := append(g.views[:i], g.views[i+1:]...) 171 g.views = append([]*View{v}, s...) 172 return v, nil 173 } 174 } 175 return nil, ErrUnknownView 176} 177 178// Views returns all the views in the GUI. 179func (g *Gui) Views() []*View { 180 return g.views 181} 182 183// View returns a pointer to the view with the given name, or error 184// ErrUnknownView if a view with that name does not exist. 185func (g *Gui) View(name string) (*View, error) { 186 for _, v := range g.views { 187 if v.name == name { 188 return v, nil 189 } 190 } 191 return nil, ErrUnknownView 192} 193 194// ViewByPosition returns a pointer to a view matching the given position, or 195// error ErrUnknownView if a view in that position does not exist. 196func (g *Gui) ViewByPosition(x, y int) (*View, error) { 197 // traverse views in reverse order checking top views first 198 for i := len(g.views); i > 0; i-- { 199 v := g.views[i-1] 200 if x > v.x0 && x < v.x1 && y > v.y0 && y < v.y1 { 201 return v, nil 202 } 203 } 204 return nil, ErrUnknownView 205} 206 207// ViewPosition returns the coordinates of the view with the given name, or 208// error ErrUnknownView if a view with that name does not exist. 209func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) { 210 for _, v := range g.views { 211 if v.name == name { 212 return v.x0, v.y0, v.x1, v.y1, nil 213 } 214 } 215 return 0, 0, 0, 0, ErrUnknownView 216} 217 218// DeleteView deletes a view by name. 219func (g *Gui) DeleteView(name string) error { 220 for i, v := range g.views { 221 if v.name == name { 222 g.views = append(g.views[:i], g.views[i+1:]...) 223 return nil 224 } 225 } 226 return ErrUnknownView 227} 228 229// SetCurrentView gives the focus to a given view. 230func (g *Gui) SetCurrentView(name string) (*View, error) { 231 for _, v := range g.views { 232 if v.name == name { 233 g.currentView = v 234 return v, nil 235 } 236 } 237 return nil, ErrUnknownView 238} 239 240// CurrentView returns the currently focused view, or nil if no view 241// owns the focus. 242func (g *Gui) CurrentView() *View { 243 return g.currentView 244} 245 246// SetKeybinding creates a new keybinding. If viewname equals to "" 247// (empty string) then the keybinding will apply to all views. key must 248// be a rune or a Key. 249func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error { 250 var kb *keybinding 251 252 k, ch, err := getKey(key) 253 if err != nil { 254 return err 255 } 256 kb = newKeybinding(viewname, k, ch, mod, handler) 257 g.keybindings = append(g.keybindings, kb) 258 return nil 259} 260 261// DeleteKeybinding deletes a keybinding. 262func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error { 263 k, ch, err := getKey(key) 264 if err != nil { 265 return err 266 } 267 268 for i, kb := range g.keybindings { 269 if kb.viewName == viewname && kb.ch == ch && kb.key == k && kb.mod == mod { 270 g.keybindings = append(g.keybindings[:i], g.keybindings[i+1:]...) 271 return nil 272 } 273 } 274 return errors.New("keybinding not found") 275} 276 277// DeleteKeybindings deletes all keybindings of view. 278func (g *Gui) DeleteKeybindings(viewname string) { 279 var s []*keybinding 280 for _, kb := range g.keybindings { 281 if kb.viewName != viewname { 282 s = append(s, kb) 283 } 284 } 285 g.keybindings = s 286} 287 288// getKey takes an empty interface with a key and returns the corresponding 289// typed Key or rune. 290func getKey(key interface{}) (Key, rune, error) { 291 switch t := key.(type) { 292 case Key: 293 return t, 0, nil 294 case rune: 295 return 0, t, nil 296 default: 297 return 0, 0, errors.New("unknown type") 298 } 299} 300 301// userEvent represents an event triggered by the user. 302type userEvent struct { 303 f func(*Gui) error 304} 305 306// Update executes the passed function. This method can be called safely from a 307// goroutine in order to update the GUI. It is important to note that the 308// passed function won't be executed immediately, instead it will be added to 309// the user events queue. Given that Update spawns a goroutine, the order in 310// which the user events will be handled is not guaranteed. 311func (g *Gui) Update(f func(*Gui) error) { 312 go func() { g.userEvents <- userEvent{f: f} }() 313} 314 315// A Manager is in charge of GUI's layout and can be used to build widgets. 316type Manager interface { 317 // Layout is called every time the GUI is redrawn, it must contain the 318 // base views and its initializations. 319 Layout(*Gui) error 320} 321 322// The ManagerFunc type is an adapter to allow the use of ordinary functions as 323// Managers. If f is a function with the appropriate signature, ManagerFunc(f) 324// is an Manager object that calls f. 325type ManagerFunc func(*Gui) error 326 327// Layout calls f(g) 328func (f ManagerFunc) Layout(g *Gui) error { 329 return f(g) 330} 331 332// SetManager sets the given GUI managers. It deletes all views and 333// keybindings. 334func (g *Gui) SetManager(managers ...Manager) { 335 g.managers = managers 336 g.currentView = nil 337 g.views = nil 338 g.keybindings = nil 339 340 go func() { g.tbEvents <- termbox.Event{Type: termbox.EventResize} }() 341} 342 343// SetManagerFunc sets the given manager function. It deletes all views and 344// keybindings. 345func (g *Gui) SetManagerFunc(manager func(*Gui) error) { 346 g.SetManager(ManagerFunc(manager)) 347} 348 349// MainLoop runs the main loop until an error is returned. A successful 350// finish should return ErrQuit. 351func (g *Gui) MainLoop() error { 352 go func() { 353 for { 354 g.tbEvents <- termbox.PollEvent() 355 } 356 }() 357 358 inputMode := termbox.InputAlt 359 if g.InputEsc { 360 inputMode = termbox.InputEsc 361 } 362 if g.Mouse { 363 inputMode |= termbox.InputMouse 364 } 365 termbox.SetInputMode(inputMode) 366 367 if err := g.flush(); err != nil { 368 return err 369 } 370 for { 371 select { 372 case ev := <-g.tbEvents: 373 if err := g.handleEvent(&ev); err != nil { 374 return err 375 } 376 case ev := <-g.userEvents: 377 if err := ev.f(g); err != nil { 378 return err 379 } 380 } 381 if err := g.consumeevents(); err != nil { 382 return err 383 } 384 if err := g.flush(); err != nil { 385 return err 386 } 387 } 388} 389 390// consumeevents handles the remaining events in the events pool. 391func (g *Gui) consumeevents() error { 392 for { 393 select { 394 case ev := <-g.tbEvents: 395 if err := g.handleEvent(&ev); err != nil { 396 return err 397 } 398 case ev := <-g.userEvents: 399 if err := ev.f(g); err != nil { 400 return err 401 } 402 default: 403 return nil 404 } 405 } 406} 407 408// handleEvent handles an event, based on its type (key-press, error, 409// etc.) 410func (g *Gui) handleEvent(ev *termbox.Event) error { 411 switch ev.Type { 412 case termbox.EventKey, termbox.EventMouse: 413 return g.onKey(ev) 414 case termbox.EventError: 415 return ev.Err 416 default: 417 return nil 418 } 419} 420 421// flush updates the gui, re-drawing frames and buffers. 422func (g *Gui) flush() error { 423 termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor)) 424 425 maxX, maxY := termbox.Size() 426 // if GUI's size has changed, we need to redraw all views 427 if maxX != g.maxX || maxY != g.maxY { 428 for _, v := range g.views { 429 v.tainted = true 430 } 431 } 432 g.maxX, g.maxY = maxX, maxY 433 434 for _, m := range g.managers { 435 if err := m.Layout(g); err != nil { 436 return err 437 } 438 } 439 for _, v := range g.views { 440 if v.Frame { 441 var fgColor, bgColor Attribute 442 if g.Highlight && v == g.currentView { 443 fgColor = g.SelFgColor 444 bgColor = g.SelBgColor 445 } else { 446 fgColor = g.FgColor 447 bgColor = g.BgColor 448 } 449 450 if err := g.drawFrameEdges(v, fgColor, bgColor); err != nil { 451 return err 452 } 453 if err := g.drawFrameCorners(v, fgColor, bgColor); err != nil { 454 return err 455 } 456 if v.Title != "" { 457 if err := g.drawTitle(v, fgColor, bgColor); err != nil { 458 return err 459 } 460 } 461 } 462 if err := g.draw(v); err != nil { 463 return err 464 } 465 } 466 termbox.Flush() 467 return nil 468} 469 470// drawFrameEdges draws the horizontal and vertical edges of a view. 471func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error { 472 runeH, runeV := '─', '│' 473 if g.ASCII { 474 runeH, runeV = '-', '|' 475 } 476 477 for x := v.x0 + 1; x < v.x1 && x < g.maxX; x++ { 478 if x < 0 { 479 continue 480 } 481 if v.y0 > -1 && v.y0 < g.maxY { 482 if err := g.SetRune(x, v.y0, runeH, fgColor, bgColor); err != nil { 483 return err 484 } 485 } 486 if v.y1 > -1 && v.y1 < g.maxY { 487 if err := g.SetRune(x, v.y1, runeH, fgColor, bgColor); err != nil { 488 return err 489 } 490 } 491 } 492 for y := v.y0 + 1; y < v.y1 && y < g.maxY; y++ { 493 if y < 0 { 494 continue 495 } 496 if v.x0 > -1 && v.x0 < g.maxX { 497 if err := g.SetRune(v.x0, y, runeV, fgColor, bgColor); err != nil { 498 return err 499 } 500 } 501 if v.x1 > -1 && v.x1 < g.maxX { 502 if err := g.SetRune(v.x1, y, runeV, fgColor, bgColor); err != nil { 503 return err 504 } 505 } 506 } 507 return nil 508} 509 510// drawFrameCorners draws the corners of the view. 511func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error { 512 runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘' 513 if g.ASCII { 514 runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+' 515 } 516 517 corners := []struct { 518 x, y int 519 ch rune 520 }{{v.x0, v.y0, runeTL}, {v.x1, v.y0, runeTR}, {v.x0, v.y1, runeBL}, {v.x1, v.y1, runeBR}} 521 522 for _, c := range corners { 523 if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY { 524 if err := g.SetRune(c.x, c.y, c.ch, fgColor, bgColor); err != nil { 525 return err 526 } 527 } 528 } 529 return nil 530} 531 532// drawTitle draws the title of the view. 533func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error { 534 if v.y0 < 0 || v.y0 >= g.maxY { 535 return nil 536 } 537 538 for i, ch := range v.Title { 539 x := v.x0 + i + 2 540 if x < 0 { 541 continue 542 } else if x > v.x1-2 || x >= g.maxX { 543 break 544 } 545 if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil { 546 return err 547 } 548 } 549 return nil 550} 551 552// draw manages the cursor and calls the draw function of a view. 553func (g *Gui) draw(v *View) error { 554 if g.Cursor { 555 if curview := g.currentView; curview != nil { 556 vMaxX, vMaxY := curview.Size() 557 if curview.cx < 0 { 558 curview.cx = 0 559 } else if curview.cx >= vMaxX { 560 curview.cx = vMaxX - 1 561 } 562 if curview.cy < 0 { 563 curview.cy = 0 564 } else if curview.cy >= vMaxY { 565 curview.cy = vMaxY - 1 566 } 567 568 gMaxX, gMaxY := g.Size() 569 cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1 570 if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY { 571 termbox.SetCursor(cx, cy) 572 } else { 573 termbox.HideCursor() 574 } 575 } 576 } else { 577 termbox.HideCursor() 578 } 579 580 v.clearRunes() 581 if err := v.draw(); err != nil { 582 return err 583 } 584 return nil 585} 586 587// onKey manages key-press events. A keybinding handler is called when 588// a key-press or mouse event satisfies a configured keybinding. Furthermore, 589// currentView's internal buffer is modified if currentView.Editable is true. 590func (g *Gui) onKey(ev *termbox.Event) error { 591 switch ev.Type { 592 case termbox.EventKey: 593 matched, err := g.execKeybindings(g.currentView, ev) 594 if err != nil { 595 return err 596 } 597 if matched { 598 break 599 } 600 if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil { 601 g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod)) 602 } 603 case termbox.EventMouse: 604 mx, my := ev.MouseX, ev.MouseY 605 v, err := g.ViewByPosition(mx, my) 606 if err != nil { 607 break 608 } 609 if err := v.SetCursor(mx-v.x0-1, my-v.y0-1); err != nil { 610 return err 611 } 612 if _, err := g.execKeybindings(v, ev); err != nil { 613 return err 614 } 615 } 616 617 return nil 618} 619 620// execKeybindings executes the keybinding handlers that match the passed view 621// and event. The value of matched is true if there is a match and no errors. 622func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) { 623 matched = false 624 for _, kb := range g.keybindings { 625 if kb.handler == nil { 626 continue 627 } 628 if kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) && kb.matchView(v) { 629 if err := kb.handler(g, v); err != nil { 630 return false, err 631 } 632 matched = true 633 } 634 } 635 return matched, nil 636} 637