From cb8236c9c22007eb622b7219c58d3342f1f53d50 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Nov 2019 20:47:29 +0100 Subject: termui: migrate to awesome-gocui instead of the old fork I had --- vendor/github.com/awesome-gocui/gocui/.gitignore | 1 + vendor/github.com/awesome-gocui/gocui/AUTHORS | 30 + .../awesome-gocui/gocui/CODE_OF_CONDUCT.md | 76 ++ .../github.com/awesome-gocui/gocui/CONTRIBUTING.md | 33 + vendor/github.com/awesome-gocui/gocui/LICENSE | 23 + vendor/github.com/awesome-gocui/gocui/README.md | 135 ++++ vendor/github.com/awesome-gocui/gocui/attribute.go | 32 + vendor/github.com/awesome-gocui/gocui/doc.go | 118 +++ vendor/github.com/awesome-gocui/gocui/edit.go | 449 +++++++++++ vendor/github.com/awesome-gocui/gocui/escape.go | 229 ++++++ vendor/github.com/awesome-gocui/gocui/go.mod | 9 + vendor/github.com/awesome-gocui/gocui/go.sum | 6 + vendor/github.com/awesome-gocui/gocui/gui.go | 832 +++++++++++++++++++++ .../github.com/awesome-gocui/gocui/gui_others.go | 60 ++ .../github.com/awesome-gocui/gocui/gui_windows.go | 53 ++ .../github.com/awesome-gocui/gocui/keybinding.go | 285 +++++++ vendor/github.com/awesome-gocui/gocui/loader.go | 46 ++ vendor/github.com/awesome-gocui/gocui/view.go | 800 ++++++++++++++++++++ 18 files changed, 3217 insertions(+) create mode 100644 vendor/github.com/awesome-gocui/gocui/.gitignore create mode 100644 vendor/github.com/awesome-gocui/gocui/AUTHORS create mode 100644 vendor/github.com/awesome-gocui/gocui/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/awesome-gocui/gocui/CONTRIBUTING.md create mode 100644 vendor/github.com/awesome-gocui/gocui/LICENSE create mode 100644 vendor/github.com/awesome-gocui/gocui/README.md create mode 100644 vendor/github.com/awesome-gocui/gocui/attribute.go create mode 100644 vendor/github.com/awesome-gocui/gocui/doc.go create mode 100644 vendor/github.com/awesome-gocui/gocui/edit.go create mode 100644 vendor/github.com/awesome-gocui/gocui/escape.go create mode 100644 vendor/github.com/awesome-gocui/gocui/go.mod create mode 100644 vendor/github.com/awesome-gocui/gocui/go.sum create mode 100644 vendor/github.com/awesome-gocui/gocui/gui.go create mode 100644 vendor/github.com/awesome-gocui/gocui/gui_others.go create mode 100644 vendor/github.com/awesome-gocui/gocui/gui_windows.go create mode 100644 vendor/github.com/awesome-gocui/gocui/keybinding.go create mode 100644 vendor/github.com/awesome-gocui/gocui/loader.go create mode 100644 vendor/github.com/awesome-gocui/gocui/view.go (limited to 'vendor/github.com/awesome-gocui/gocui') diff --git a/vendor/github.com/awesome-gocui/gocui/.gitignore b/vendor/github.com/awesome-gocui/gocui/.gitignore new file mode 100644 index 00000000..1377554e --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/vendor/github.com/awesome-gocui/gocui/AUTHORS b/vendor/github.com/awesome-gocui/gocui/AUTHORS new file mode 100644 index 00000000..43ec4cec --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/AUTHORS @@ -0,0 +1,30 @@ +# This is the official list of gocui authors for copyright purposes. + +# Names should be added to this file as +# Name or Organization contribution +# Contribution +# The email address is not required for organizations. + +Roi Martin + Main developer + +Ryan Sullivan + Toggleable view frames + +Matthieu Rakotojaona + Wrapped views + +Harry Lawrence + Basic mouse support + +Danny Tylman + Masked views + +Frederik Deweerdt + Colored fonts + +Henri Koski + Custom current view color + +Dustin Willis Webber + 256-colors output mode support diff --git a/vendor/github.com/awesome-gocui/gocui/CODE_OF_CONDUCT.md b/vendor/github.com/awesome-gocui/gocui/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..1bdac055 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at mkopenga@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/github.com/awesome-gocui/gocui/CONTRIBUTING.md b/vendor/github.com/awesome-gocui/gocui/CONTRIBUTING.md new file mode 100644 index 00000000..b93e45b2 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +Everyone is welcome to help make gocui better! + +When contributing to this repository, please first discuss the change you wish +to make via issue, email, or any other method with the owners of this repository +before making a change. + +## So all code changes happen through Pull Requests +Pull requests are the best way to propose changes to the codebase. We actively +welcome your pull requests: + +1. Fork the repo and create your branch from `master` with a name like `feature/contributors-guide`. +2. If you've added code that should be tested, add tests. +3. If you've added code that need documentation, update the documentation. +4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible. +5. Be sure to test your modifications. +6. Make sure your branch is up to date with the master branch. +7. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +8. Create that pull request! + +## Code of conduct +Please note by participating in this project, you agree to abide by the [code of conduct]. + +[code of conduct]: https://github.com/awesome-gocui/gocui/blob/master/CODE-OF-CONDUCT.md + +## Any contributions you make will be under the license indicated in the [license](LICENSE.md) +In short, when you submit code changes, your submissions are understood to be +under the same license as the rest of project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using Github's [issues](https://github.com/awesome-gocui/gocui/issues) +We use GitHub issues to track public bugs. Report a bug by [opening a new +issue](https://github.com/awesome-gocui/gocui/issues/new); it's that easy! \ No newline at end of file diff --git a/vendor/github.com/awesome-gocui/gocui/LICENSE b/vendor/github.com/awesome-gocui/gocui/LICENSE new file mode 100644 index 00000000..8cb28215 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014 The gocui Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the gocui Authors nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/awesome-gocui/gocui/README.md b/vendor/github.com/awesome-gocui/gocui/README.md new file mode 100644 index 00000000..be212c58 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/README.md @@ -0,0 +1,135 @@ +# GOCUI - Go Console User Interface +[![CircleCI](https://circleci.com/gh/awesome-gocui/gocui/tree/master.svg?style=svg)](https://circleci.com/gh/awesome-gocui/gocui/tree/master) +[![CodeCov](https://codecov.io/gh/awesome-gocui/gocui/branch/master/graph/badge.svg)](https://codecov.io/gh/awesome-gocui/gocui) +[![Go Report Card](https://goreportcard.com/badge/github.com/awesome-gocui/gocui)](https://goreportcard.com/report/github.com/awesome-gocui/gocui) +[![GolangCI](https://golangci.com/badges/github.com/awesome-gocui/gocui.svg)](https://golangci.com/badges/github.com/awesome-gocui/gocui.svg) +[![GoDoc](https://godoc.org/github.com/awesome-gocui/gocui?status.svg)](https://godoc.org/github.com/awesome-gocui/gocui) +![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/awesome-gocui/gocui.svg) + +Minimalist Go package aimed at creating Console User Interfaces. +A community fork based on the amazing work of [jroimartin](https://github.com/jroimartin/gocui) + +## Features + +* Minimalist API. +* Views (the "windows" in the GUI) implement the interface io.ReadWriter. +* Support for overlapping views. +* The GUI can be modified at runtime (concurrent-safe). +* Global and view-level keybindings. +* Mouse support. +* Colored text. +* Customizable editing mode. +* Easy to build reusable widgets, complex layouts... + +## About fork + +This fork has many improvements over the original work from [jroimartin](https://github.com/jroimartin/gocui). + +* Better wide character support +* Support for 1 Line height views +* Better support for running in docker container +* Customize frame colors +* Improved code comments and quality +* Many small improvements +* Change Visibility of views + +For information about this org see: [awesome-gocui/about](https://github.com/awesome-gocui/about). + +## Installation + +Execute: + +``` +$ go get github.com/awesome-gocui/gocui +``` + +## Documentation + +Execute: + +``` +$ go doc github.com/awesome-gocui/gocui +``` + +Or visit [godoc.org](https://godoc.org/github.com/awesome-gocui/gocui) to read it +online. + +## Example +See the [_example](./_example/) folder for more examples + +```go +package main + +import ( + "fmt" + "log" + + "github.com/awesome-gocui/gocui" +) + +func main() { + g, err := gocui.NewGui(gocui.OutputNormal, false) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.SetManagerFunc(layout) + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { + log.Panicln(err) + } +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + if v, err := g.SetView("hello", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + fmt.Fprintln(v, "Hello world!") + if _, err := g.SetCurrentView("hello"); err != nil { + return err + } + } + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} +``` + +## Screenshots + +![r2cui](https://cloud.githubusercontent.com/assets/1223476/19418932/63645052-93ce-11e6-867c-da5e97e37237.png) + +![_examples/demo.go](https://cloud.githubusercontent.com/assets/1223476/5992750/720b84f0-aa36-11e4-88ec-296fa3247b52.png) + +![_examples/dynamic.go](https://cloud.githubusercontent.com/assets/1223476/5992751/76ad5cc2-aa36-11e4-8204-6a90269db827.png) + +## Projects using gocui + +* [komanda-cli](https://github.com/mephux/komanda-cli): IRC Client For Developers. +* [vuls](https://github.com/future-architect/vuls): Agentless vulnerability scanner for Linux/FreeBSD. +* [wuzz](https://github.com/asciimoo/wuzz): Interactive cli tool for HTTP inspection. +* [httplab](https://github.com/gchaincl/httplab): Interactive web server. +* [domainr](https://github.com/MichaelThessel/domainr): Tool that checks the availability of domains based on keywords. +* [gotime](https://github.com/nanohard/gotime): Time tracker for projects and tasks. +* [claws](https://github.com/thehowl/claws): Interactive command line client for testing websockets. +* [terminews](http://github.com/antavelos/terminews): Terminal based RSS reader. +* [diagram](https://github.com/esimov/diagram): Tool to convert ascii arts into hand drawn diagrams. +* [pody](https://github.com/JulienBreux/pody): CLI app to manage Pods in a Kubernetes cluster. +* [kubexp](https://github.com/alitari/kubexp): Kubernetes client. +* [kcli](https://github.com/cswank/kcli): Tool for inspecting kafka topics/partitions/messages. +* [fac](https://github.com/mkchoi212/fac): git merge conflict resolver +* [jsonui](https://github.com/gulyasm/jsonui): Interactive JSON explorer for your terminal. +* [cointop](https://github.com/miguelmota/cointop): Interactive terminal based UI application for tracking cryptocurrencies. +* [lazygit](https://github.com/jesseduffield/lazygit): simple terminal UI for git commands. +* [lazydocker](https://github.com/jesseduffield/lazydocker): The lazier way to manage everything docker. + +Note: if your project is not listed here, let us know! :) diff --git a/vendor/github.com/awesome-gocui/gocui/attribute.go b/vendor/github.com/awesome-gocui/gocui/attribute.go new file mode 100644 index 00000000..3d986a71 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/attribute.go @@ -0,0 +1,32 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import "github.com/awesome-gocui/termbox-go" + +// Attribute represents a terminal attribute, like color, font style, etc. They +// can be combined using bitwise OR (|). Note that it is not possible to +// combine multiple color attributes. +type Attribute termbox.Attribute + +// Color attributes. +const ( + ColorDefault Attribute = Attribute(termbox.ColorDefault) + ColorBlack = Attribute(termbox.ColorBlack) + ColorRed = Attribute(termbox.ColorRed) + ColorGreen = Attribute(termbox.ColorGreen) + ColorYellow = Attribute(termbox.ColorYellow) + ColorBlue = Attribute(termbox.ColorBlue) + ColorMagenta = Attribute(termbox.ColorMagenta) + ColorCyan = Attribute(termbox.ColorCyan) + ColorWhite = Attribute(termbox.ColorWhite) +) + +// Text style attributes. +const ( + AttrBold Attribute = Attribute(termbox.AttrBold) + AttrUnderline = Attribute(termbox.AttrUnderline) + AttrReverse = Attribute(termbox.AttrReverse) +) diff --git a/vendor/github.com/awesome-gocui/gocui/doc.go b/vendor/github.com/awesome-gocui/gocui/doc.go new file mode 100644 index 00000000..ca7113fa --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/doc.go @@ -0,0 +1,118 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package gocui allows to create console user interfaces. + +Create a new GUI: + + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + // handle error + } + defer g.Close() + + // Set GUI managers and key bindings + // ... + + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { + // handle error + } + +Set GUI managers: + + g.SetManager(mgr1, mgr2) + +Managers are in charge of GUI's layout and can be used to build widgets. On +each iteration of the GUI's main loop, the Layout function of each configured +manager is executed. Managers are used to set-up and update the application's +main views, being possible to freely change them during execution. Also, it is +important to mention that a main loop iteration is executed on each reported +event (key-press, mouse event, window resize, etc). + +GUIs are composed by Views, you can think of it as buffers. Views implement the +io.ReadWriter interface, so you can just write to them if you want to modify +their content. The same is valid for reading. + +Create and initialize a view with absolute coordinates: + + if v, err := g.SetView("viewname", 2, 2, 22, 7); err != nil { + if !gocui.IsUnknownView(err) { + // handle error + } + fmt.Fprintln(v, "This is a new view") + // ... + } + +Views can also be created using relative coordinates: + + maxX, maxY := g.Size() + if v, err := g.SetView("viewname", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { + // ... + } + +Configure keybindings: + + if err := g.SetKeybinding("viewname", gocui.KeyEnter, gocui.ModNone, fcn); err != nil { + // handle error + } + +gocui implements full mouse support that can be enabled with: + + g.Mouse = true + +Mouse events are handled like any other keybinding: + + if err := g.SetKeybinding("viewname", gocui.MouseLeft, gocui.ModNone, fcn); err != nil { + // handle error + } + +IMPORTANT: Views can only be created, destroyed or updated in three ways: from +the Layout function within managers, from keybinding callbacks or via +*Gui.Update(). The reason for this is that it allows gocui to be +concurrent-safe. So, if you want to update your GUI from a goroutine, you must +use *Gui.Update(). For example: + + g.Update(func(g *gocui.Gui) error { + v, err := g.View("viewname") + if err != nil { + // handle error + } + v.Clear() + fmt.Fprintln(v, "Writing from different goroutines") + return nil + }) + +By default, gocui provides a basic editing mode. This mode can be extended +and customized creating a new Editor and assigning it to *View.Editor: + + type Editor interface { + Edit(v *View, key Key, ch rune, mod Modifier) + } + +DefaultEditor can be taken as example to create your own custom Editor: + + var DefaultEditor Editor = EditorFunc(simpleEditor) + + func simpleEditor(v *View, key Key, ch rune, mod Modifier) { + switch { + case ch != 0 && mod == 0: + v.EditWrite(ch) + case key == KeySpace: + v.EditWrite(' ') + case key == KeyBackspace || key == KeyBackspace2: + v.EditDelete(true) + // ... + } + } + +Colored text: + +Views allow to add colored text using ANSI colors. For example: + + fmt.Fprintln(v, "\x1b[0;31mHello world") + +For more information, see the examples in folder "_examples/". +*/ +package gocui diff --git a/vendor/github.com/awesome-gocui/gocui/edit.go b/vendor/github.com/awesome-gocui/gocui/edit.go new file mode 100644 index 00000000..b5630df3 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/edit.go @@ -0,0 +1,449 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import ( + "github.com/go-errors/errors" + + "github.com/mattn/go-runewidth" +) + +const maxInt = int(^uint(0) >> 1) + +// Editor interface must be satisfied by gocui editors. +type Editor interface { + Edit(v *View, key Key, ch rune, mod Modifier) +} + +// The EditorFunc type is an adapter to allow the use of ordinary functions as +// Editors. If f is a function with the appropriate signature, EditorFunc(f) +// is an Editor object that calls f. +type EditorFunc func(v *View, key Key, ch rune, mod Modifier) + +// Edit calls f(v, key, ch, mod) +func (f EditorFunc) Edit(v *View, key Key, ch rune, mod Modifier) { + f(v, key, ch, mod) +} + +// DefaultEditor is the default editor. +var DefaultEditor Editor = EditorFunc(simpleEditor) + +// simpleEditor is used as the default gocui editor. +func simpleEditor(v *View, key Key, ch rune, mod Modifier) { + switch { + case ch != 0 && mod == 0: + v.EditWrite(ch) + case key == KeySpace: + v.EditWrite(' ') + case key == KeyBackspace || key == KeyBackspace2: + v.EditDelete(true) + case key == KeyDelete: + v.EditDelete(false) + case key == KeyInsert: + v.Overwrite = !v.Overwrite + case key == KeyEnter: + v.EditNewLine() + case key == KeyArrowDown: + v.MoveCursor(0, 1, false) + case key == KeyArrowUp: + v.MoveCursor(0, -1, false) + case key == KeyArrowLeft: + v.MoveCursor(-1, 0, false) + case key == KeyArrowRight: + v.MoveCursor(1, 0, false) + case key == KeyTab: + v.EditWrite('\t') + case key == KeySpace: + v.EditWrite(' ') + case key == KeyInsert: + v.Overwrite = !v.Overwrite + default: + v.EditWrite(ch) + } +} + +// EditWrite writes a rune at the cursor position. +func (v *View) EditWrite(ch rune) { + w := runewidth.RuneWidth(ch) + v.writeRune(v.cx, v.cy, ch) + v.moveCursor(w, 0, true) +} + +// EditDeleteToStartOfLine is the equivalent of pressing ctrl+U in your terminal, it deletes to the start of the line. Or if you are already at the start of the line, it deletes the newline character +func (v *View) EditDeleteToStartOfLine() { + x, _ := v.Cursor() + if x == 0 { + v.EditDelete(true) + } else { + // delete characters until we are the start of the line + for x > 0 { + v.EditDelete(true) + x, _ = v.Cursor() + } + } +} + +// EditGotoToStartOfLine takes you to the start of the current line +func (v *View) EditGotoToStartOfLine() { + x, _ := v.Cursor() + for x > 0 { + v.MoveCursor(-1, 0, false) + x, _ = v.Cursor() + } +} + +// EditGotoToEndOfLine takes you to the end of the line +func (v *View) EditGotoToEndOfLine() { + _, y := v.Cursor() + _ = v.SetCursor(0, y+1) + x, newY := v.Cursor() + if newY == y { + // we must be on the last line, so lets move to the very end + prevX := -1 + for prevX != x { + prevX = x + v.MoveCursor(1, 0, false) + x, _ = v.Cursor() + } + } else { + // most left so now we're at the end of the original line + v.MoveCursor(-1, 0, false) + } +} + +// EditDelete deletes a rune at the cursor position. back determines the +// direction. +func (v *View) EditDelete(back bool) { + x, y := v.ox+v.cx, v.oy+v.cy + if y < 0 { + return + } else if y >= len(v.viewLines) { + v.MoveCursor(-1, 0, true) + return + } + + maxX, _ := v.Size() + if back { + if x == 0 { // start of the line + if y < 1 { + return + } + + var maxPrevWidth int + if v.Wrap { + maxPrevWidth = maxX + } else { + maxPrevWidth = maxInt + } + + if v.viewLines[y].linesX == 0 { // regular line + v.mergeLines(v.cy - 1) + if len(v.viewLines[y-1].line) < maxPrevWidth { + v.MoveCursor(-1, 0, true) + } + } else { // wrapped line + n, _ := v.deleteRune(len(v.viewLines[y-1].line)-1, v.cy-1) + v.MoveCursor(-n, 0, true) + } + } else { // middle/end of the line + n, _ := v.deleteRune(v.cx-1, v.cy) + v.MoveCursor(-n, 0, true) + } + } else { + if x == len(v.viewLines[y].line) { // end of the line + v.mergeLines(v.cy) + } else { // start/middle of the line + v.deleteRune(v.cx, v.cy) + } + } +} + +// EditNewLine inserts a new line under the cursor. +func (v *View) EditNewLine() { + v.breakLine(v.cx, v.cy) + v.ox = 0 + v.cy = v.cy + 1 + v.cx = 0 +} + +// MoveCursor moves the cursor taking into account the width of the line/view, +// displacing the origin if necessary. +func (v *View) MoveCursor(dx, dy int, writeMode bool) { + ox, oy := v.cx+v.ox, v.cy+v.oy + x, y := ox+dx, oy+dy + + if y < 0 || y >= len(v.viewLines) { + v.moveCursor(dx, dy, writeMode) + return + } + + // Removing newline. + if x < 0 { + var prevLen int + if y-1 >= 0 && y-1 < len(v.viewLines) { + prevLen = lineWidth(v.viewLines[y-1].line) + } + + v.MoveCursor(prevLen, -1, writeMode) + return + } + + line := v.viewLines[y].line + var col int + var prevCol int + for i := range line { + prevCol = col + col += runewidth.RuneWidth(line[i].chr) + if dx > 0 { + if x <= col { + x = col + break + } + continue + } + + if x < col { + x = prevCol + break + } + } + + v.moveCursor(x-ox, y-oy, writeMode) +} + +func (v *View) moveCursor(dx, dy int, writeMode bool) { + maxX, maxY := v.Size() + cx, cy := v.cx+dx, v.cy+dy + x, y := v.ox+cx, v.oy+cy + + var curLineWidth, prevLineWidth int + // get the width of the current line + curLineWidth = maxInt + if v.Wrap { + curLineWidth = maxX - 1 + } + + if !writeMode { + curLineWidth = 0 + if y >= 0 && y < len(v.viewLines) { + curLineWidth = lineWidth(v.viewLines[y].line) + if v.Wrap && curLineWidth >= maxX { + curLineWidth = maxX - 1 + } + } + } + // get the width of the previous line + prevLineWidth = 0 + if y-1 >= 0 && y-1 < len(v.viewLines) { + prevLineWidth = lineWidth(v.viewLines[y-1].line) + } + // adjust cursor's x position and view's x origin + if x > curLineWidth { // move to next line + if dx > 0 { // horizontal movement + cy++ + if writeMode || v.oy+cy < len(v.viewLines) { + if !v.Wrap { + v.ox = 0 + } + v.cx = 0 + } + } else { // vertical movement + if curLineWidth > 0 { // move cursor to the EOL + if v.Wrap { + v.cx = curLineWidth + } else { + ncx := curLineWidth - v.ox + if ncx < 0 { + v.ox += ncx + if v.ox < 0 { + v.ox = 0 + } + v.cx = 0 + } else { + v.cx = ncx + } + } + } else { + if writeMode || v.oy+cy < len(v.viewLines) { + if !v.Wrap { + v.ox = 0 + } + v.cx = 0 + } + } + } + } else if cx < 0 { + if !v.Wrap && v.ox > 0 { // move origin to the left + v.ox += cx + v.cx = 0 + } else { // move to previous line + cy-- + if prevLineWidth > 0 { + if !v.Wrap { // set origin so the EOL is visible + nox := prevLineWidth - maxX + 1 + if nox < 0 { + nox = 0 + } + v.ox = nox + } + v.cx = prevLineWidth + } else { + if !v.Wrap { + v.ox = 0 + } + v.cx = 0 + } + } + } else { // stay on the same line + if v.Wrap { + v.cx = cx + } else { + if cx >= maxX { + v.ox += cx - maxX + 1 + v.cx = maxX + } else { + v.cx = cx + } + } + } + + // adjust cursor's y position and view's y origin + if cy < 0 { + if v.oy > 0 { + v.oy-- + } + } else if writeMode || v.oy+cy < len(v.viewLines) { + if cy >= maxY { + v.oy++ + } else { + v.cy = cy + } + } +} + +// writeRune writes a rune into the view's internal buffer, at the +// position corresponding to the point (x, y). The length of the internal +// buffer is increased if the point is out of bounds. Overwrite mode is +// governed by the value of View.overwrite. +func (v *View) writeRune(x, y int, ch rune) error { + v.tainted = true + + x, y, err := v.realPosition(x, y) + if err != nil { + return err + } + + if x < 0 || y < 0 { + return errors.New("invalid point") + } + + if y >= len(v.lines) { + s := make([][]cell, y-len(v.lines)+1) + v.lines = append(v.lines, s...) + } + + olen := len(v.lines[y]) + + var s []cell + if x >= len(v.lines[y]) { + s = make([]cell, x-len(v.lines[y])+1) + } else if !v.Overwrite { + s = make([]cell, 1) + } + v.lines[y] = append(v.lines[y], s...) + + if !v.Overwrite || (v.Overwrite && x >= olen-1) { + copy(v.lines[y][x+1:], v.lines[y][x:]) + } + v.lines[y][x] = cell{ + fgColor: v.FgColor, + bgColor: v.BgColor, + chr: ch, + } + + return nil +} + +// deleteRune removes a rune from the view's internal buffer, at the +// position corresponding to the point (x, y). +// returns the amount of columns that where removed. +func (v *View) deleteRune(x, y int) (int, error) { + v.tainted = true + + x, y, err := v.realPosition(x, y) + if err != nil { + return 0, err + } + + if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) { + return 0, errors.New("invalid point") + } + + var tw int + for i := range v.lines[y] { + w := runewidth.RuneWidth(v.lines[y][i].chr) + tw += w + if tw > x { + v.lines[y] = append(v.lines[y][:i], v.lines[y][i+1:]...) + return w, nil + } + + } + + return 0, nil +} + +// mergeLines merges the lines "y" and "y+1" if possible. +func (v *View) mergeLines(y int) error { + v.tainted = true + + _, y, err := v.realPosition(0, y) + if err != nil { + return err + } + + if y < 0 || y >= len(v.lines) { + return errors.New("invalid point") + } + + if y < len(v.lines)-1 { // otherwise we don't need to merge anything + v.lines[y] = append(v.lines[y], v.lines[y+1]...) + v.lines = append(v.lines[:y+1], v.lines[y+2:]...) + } + return nil +} + +// breakLine breaks a line of the internal buffer at the position corresponding +// to the point (x, y). +func (v *View) breakLine(x, y int) error { + v.tainted = true + + x, y, err := v.realPosition(x, y) + if err != nil { + return err + } + + if y < 0 || y >= len(v.lines) { + return errors.New("invalid point") + } + + var left, right []cell + if x < len(v.lines[y]) { // break line + left = make([]cell, len(v.lines[y][:x])) + copy(left, v.lines[y][:x]) + right = make([]cell, len(v.lines[y][x:])) + copy(right, v.lines[y][x:]) + } else { // new empty line + left = v.lines[y] + } + + lines := make([][]cell, len(v.lines)+1) + lines[y] = left + lines[y+1] = right + copy(lines, v.lines[:y]) + copy(lines[y+2:], v.lines[y+1:]) + v.lines = lines + return nil +} diff --git a/vendor/github.com/awesome-gocui/gocui/escape.go b/vendor/github.com/awesome-gocui/gocui/escape.go new file mode 100644 index 00000000..c88309b0 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/escape.go @@ -0,0 +1,229 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import ( + "github.com/go-errors/errors" + "strconv" +) + +type escapeInterpreter struct { + state escapeState + curch rune + csiParam []string + curFgColor, curBgColor Attribute + mode OutputMode +} + +type escapeState int + +const ( + stateNone escapeState = iota + stateEscape + stateCSI + stateParams +) + +var ( + errNotCSI = errors.New("Not a CSI escape sequence") + errCSIParseError = errors.New("CSI escape sequence parsing error") + errCSITooLong = errors.New("CSI escape sequence is too long") +) + +// runes in case of error will output the non-parsed runes as a string. +func (ei *escapeInterpreter) runes() []rune { + switch ei.state { + case stateNone: + return []rune{0x1b} + case stateEscape: + return []rune{0x1b, ei.curch} + case stateCSI: + return []rune{0x1b, '[', ei.curch} + case stateParams: + ret := []rune{0x1b, '['} + for _, s := range ei.csiParam { + ret = append(ret, []rune(s)...) + ret = append(ret, ';') + } + return append(ret, ei.curch) + } + return nil +} + +// newEscapeInterpreter returns an escapeInterpreter that will be able to parse +// terminal escape sequences. +func newEscapeInterpreter(mode OutputMode) *escapeInterpreter { + ei := &escapeInterpreter{ + state: stateNone, + curFgColor: ColorDefault, + curBgColor: ColorDefault, + mode: mode, + } + return ei +} + +// reset sets the escapeInterpreter in initial state. +func (ei *escapeInterpreter) reset() { + ei.state = stateNone + ei.curFgColor = ColorDefault + ei.curBgColor = ColorDefault + ei.csiParam = nil +} + +// parseOne parses a rune. If isEscape is true, it means that the rune is part +// of an escape sequence, and as such should not be printed verbatim. Otherwise, +// it's not an escape sequence. +func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) { + // Sanity checks + if len(ei.csiParam) > 20 { + return false, errCSITooLong + } + if len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255 { + return false, errCSITooLong + } + + ei.curch = ch + + switch ei.state { + case stateNone: + if ch == 0x1b { + ei.state = stateEscape + return true, nil + } + return false, nil + case stateEscape: + if ch == '[' { + ei.state = stateCSI + return true, nil + } + return false, errNotCSI + case stateCSI: + switch { + case ch >= '0' && ch <= '9': + ei.csiParam = append(ei.csiParam, "") + case ch == 'm': + ei.csiParam = append(ei.csiParam, "0") + default: + return false, errCSIParseError + } + ei.state = stateParams + fallthrough + case stateParams: + switch { + case ch >= '0' && ch <= '9': + ei.csiParam[len(ei.csiParam)-1] += string(ch) + return true, nil + case ch == ';': + ei.csiParam = append(ei.csiParam, "") + return true, nil + case ch == 'm': + var err error + switch ei.mode { + case OutputNormal: + err = ei.outputNormal() + case Output256: + err = ei.output256() + } + if err != nil { + return false, errCSIParseError + } + + ei.state = stateNone + ei.csiParam = nil + return true, nil + default: + return false, errCSIParseError + } + } + return false, nil +} + +// outputNormal provides 8 different colors: +// black, red, green, yellow, blue, magenta, cyan, white +func (ei *escapeInterpreter) outputNormal() error { + for _, param := range ei.csiParam { + p, err := strconv.Atoi(param) + if err != nil { + return errCSIParseError + } + + switch { + case p >= 30 && p <= 37: + ei.curFgColor = Attribute(p - 30 + 1) + case p == 39: + ei.curFgColor = ColorDefault + case p >= 40 && p <= 47: + ei.curBgColor = Attribute(p - 40 + 1) + case p == 49: + ei.curBgColor = ColorDefault + case p == 1: + ei.curFgColor |= AttrBold + case p == 4: + ei.curFgColor |= AttrUnderline + case p == 7: + ei.curFgColor |= AttrReverse + case p == 0: + ei.curFgColor = ColorDefault + ei.curBgColor = ColorDefault + } + } + + return nil +} + +// output256 allows you to leverage the 256-colors terminal mode: +// 0x01 - 0x08: the 8 colors as in OutputNormal +// 0x09 - 0x10: Color* | AttrBold +// 0x11 - 0xe8: 216 different colors +// 0xe9 - 0x1ff: 24 different shades of grey +func (ei *escapeInterpreter) output256() error { + if len(ei.csiParam) < 3 { + return ei.outputNormal() + } + + mode, err := strconv.Atoi(ei.csiParam[1]) + if err != nil { + return errCSIParseError + } + if mode != 5 { + return ei.outputNormal() + } + + fgbg, err := strconv.Atoi(ei.csiParam[0]) + if err != nil { + return errCSIParseError + } + color, err := strconv.Atoi(ei.csiParam[2]) + if err != nil { + return errCSIParseError + } + + switch fgbg { + case 38: + ei.curFgColor = Attribute(color + 1) + + for _, param := range ei.csiParam[3:] { + p, err := strconv.Atoi(param) + if err != nil { + return errCSIParseError + } + + switch { + case p == 1: + ei.curFgColor |= AttrBold + case p == 4: + ei.curFgColor |= AttrUnderline + case p == 7: + ei.curFgColor |= AttrReverse + } + } + case 48: + ei.curBgColor = Attribute(color + 1) + default: + return errCSIParseError + } + + return nil +} diff --git a/vendor/github.com/awesome-gocui/gocui/go.mod b/vendor/github.com/awesome-gocui/gocui/go.mod new file mode 100644 index 00000000..5791b4e4 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/go.mod @@ -0,0 +1,9 @@ +module github.com/awesome-gocui/gocui + +go 1.12 + +require ( + github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc + github.com/go-errors/errors v1.0.1 + github.com/mattn/go-runewidth v0.0.4 +) diff --git a/vendor/github.com/awesome-gocui/gocui/go.sum b/vendor/github.com/awesome-gocui/gocui/go.sum new file mode 100644 index 00000000..25f1c037 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/go.sum @@ -0,0 +1,6 @@ +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= diff --git a/vendor/github.com/awesome-gocui/gocui/gui.go b/vendor/github.com/awesome-gocui/gocui/gui.go new file mode 100644 index 00000000..6fe0d5d8 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/gui.go @@ -0,0 +1,832 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import ( + standardErrors "errors" + "runtime" + + "github.com/go-errors/errors" + + "github.com/awesome-gocui/termbox-go" +) + +// OutputMode represents the terminal's output mode (8 or 256 colors). +type OutputMode termbox.OutputMode + +var ( + // ErrAlreadyBlacklisted is returned when the keybinding is already blacklisted. + ErrAlreadyBlacklisted = standardErrors.New("keybind already blacklisted") + + // ErrBlacklisted is returned when the keybinding being parsed / used is blacklisted. + ErrBlacklisted = standardErrors.New("keybind blacklisted") + + // ErrNotBlacklisted is returned when a keybinding being whitelisted is not blacklisted. + ErrNotBlacklisted = standardErrors.New("keybind not blacklisted") + + // ErrNoSuchKeybind is returned when the keybinding being parsed does not exist. + ErrNoSuchKeybind = standardErrors.New("no such keybind") + + // ErrUnknownView allows to assert if a View must be initialized. + ErrUnknownView = standardErrors.New("unknown view") + + // ErrQuit is used to decide if the MainLoop finished successfully. + ErrQuit = standardErrors.New("quit") +) + +const ( + // OutputNormal provides 8-colors terminal mode. + OutputNormal = OutputMode(termbox.OutputNormal) + + // Output256 provides 256-colors terminal mode. + Output256 = OutputMode(termbox.Output256) + + // OutputGrayScale provides greyscale terminal mode. + OutputGrayScale = OutputMode(termbox.OutputGrayscale) + + // Output216 provides greyscale terminal mode. + Output216 = OutputMode(termbox.Output216) +) + +// Gui represents the whole User Interface, including the views, layouts +// and keybindings. +type Gui struct { + tbEvents chan termbox.Event + userEvents chan userEvent + views []*View + currentView *View + managers []Manager + keybindings []*keybinding + maxX, maxY int + outputMode OutputMode + stop chan struct{} + blacklist []Key + + // BgColor and FgColor allow to configure the background and foreground + // colors of the GUI. + BgColor, FgColor, FrameColor Attribute + + // SelBgColor and SelFgColor allow to configure the background and + // foreground colors of the frame of the current view. + SelBgColor, SelFgColor, SelFrameColor Attribute + + // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the + // frame of the current view. + Highlight bool + + // If Cursor is true then the cursor is enabled. + Cursor bool + + // If Mouse is true then mouse events will be enabled. + Mouse bool + + // If InputEsc is true, when ESC sequence is in the buffer and it doesn't + // match any known sequence, ESC means KeyEsc. + InputEsc bool + + // If ASCII is true then use ASCII instead of unicode to draw the + // interface. Using ASCII is more portable. + ASCII bool + + // SupportOverlaps is true when we allow for view edges to overlap with other + // view edges + SupportOverlaps bool +} + +// NewGui returns a new Gui object with a given output mode. +func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { + err := termbox.Init() + if err != nil { + return nil, err + } + + g := &Gui{} + + g.outputMode = mode + termbox.SetOutputMode(termbox.OutputMode(mode)) + + g.stop = make(chan struct{}) + + g.tbEvents = make(chan termbox.Event, 20) + g.userEvents = make(chan userEvent, 20) + + if runtime.GOOS != "windows" { + g.maxX, g.maxY, err = g.getTermWindowSize() + if err != nil { + return nil, err + } + } else { + g.maxX, g.maxY = termbox.Size() + } + + g.BgColor, g.FgColor = ColorDefault, ColorDefault + g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault + + // SupportOverlaps is true when we allow for view edges to overlap with other + // view edges + g.SupportOverlaps = supportOverlaps + + return g, nil +} + +// Close finalizes the library. It should be called after a successful +// initialization and when gocui is not needed anymore. +func (g *Gui) Close() { + go func() { + g.stop <- struct{}{} + }() + termbox.Close() +} + +// Size returns the terminal's size. +func (g *Gui) Size() (x, y int) { + return g.maxX, g.maxY +} + +// SetRune writes a rune at the given point, relative to the top-left +// corner of the terminal. It checks if the position is valid and applies +// the given colors. +func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error { + if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { + return errors.New("invalid point") + } + termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor)) + return nil +} + +// Rune returns the rune contained in the cell at the given position. +// It checks if the position is valid. +func (g *Gui) Rune(x, y int) (rune, error) { + if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { + return ' ', errors.New("invalid point") + } + c := termbox.CellBuffer()[y*g.maxX+x] + return c.Ch, nil +} + +// SetView creates a new view with its top-left corner at (x0, y0) +// and the bottom-right one at (x1, y1). If a view with the same name +// already exists, its dimensions are updated; otherwise, the error +// ErrUnknownView is returned, which allows to assert if the View must +// be initialized. It checks if the position is valid. +func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, error) { + if x0 >= x1 { + return nil, errors.New("invalid dimensions") + } + if name == "" { + return nil, errors.New("invalid name") + } + + if v, err := g.View(name); err == nil { + v.x0 = x0 + v.y0 = y0 + v.x1 = x1 + v.y1 = y1 + v.tainted = true + return v, nil + } + + v := newView(name, x0, y0, x1, y1, g.outputMode) + v.BgColor, v.FgColor = g.BgColor, g.FgColor + v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor + v.Overlaps = overlaps + g.views = append(g.views, v) + return v, errors.Wrap(ErrUnknownView, 0) +} + +// SetViewBeneath sets a view stacked beneath another view +func (g *Gui) SetViewBeneath(name string, aboveViewName string, height int) (*View, error) { + aboveView, err := g.View(aboveViewName) + if err != nil { + return nil, err + } + + viewTop := aboveView.y1 + 1 + return g.SetView(name, aboveView.x0, viewTop, aboveView.x1, viewTop+height-1, 0) +} + +// SetViewOnTop sets the given view on top of the existing ones. +func (g *Gui) SetViewOnTop(name string) (*View, error) { + for i, v := range g.views { + if v.name == name { + s := append(g.views[:i], g.views[i+1:]...) + g.views = append(s, v) + return v, nil + } + } + return nil, errors.Wrap(ErrUnknownView, 0) +} + +// SetViewOnBottom sets the given view on bottom of the existing ones. +func (g *Gui) SetViewOnBottom(name string) (*View, error) { + for i, v := range g.views { + if v.name == name { + s := append(g.views[:i], g.views[i+1:]...) + g.views = append([]*View{v}, s...) + return v, nil + } + } + return nil, errors.Wrap(ErrUnknownView, 0) +} + +// Views returns all the views in the GUI. +func (g *Gui) Views() []*View { + return g.views +} + +// View returns a pointer to the view with the given name, or error +// ErrUnknownView if a view with that name does not exist. +func (g *Gui) View(name string) (*View, error) { + for _, v := range g.views { + if v.name == name { + return v, nil + } + } + return nil, errors.Wrap(ErrUnknownView, 0) +} + +// ViewByPosition returns a pointer to a view matching the given position, or +// error ErrUnknownView if a view in that position does not exist. +func (g *Gui) ViewByPosition(x, y int) (*View, error) { + // traverse views in reverse order checking top views first + for i := len(g.views); i > 0; i-- { + v := g.views[i-1] + if x > v.x0 && x < v.x1 && y > v.y0 && y < v.y1 { + return v, nil + } + } + return nil, errors.Wrap(ErrUnknownView, 0) +} + +// ViewPosition returns the coordinates of the view with the given name, or +// error ErrUnknownView if a view with that name does not exist. +func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) { + for _, v := range g.views { + if v.name == name { + return v.x0, v.y0, v.x1, v.y1, nil + } + } + return 0, 0, 0, 0, errors.Wrap(ErrUnknownView, 0) +} + +// DeleteView deletes a view by name. +func (g *Gui) DeleteView(name string) error { + for i, v := range g.views { + if v.name == name { + g.views = append(g.views[:i], g.views[i+1:]...) + return nil + } + } + return errors.Wrap(ErrUnknownView, 0) +} + +// SetCurrentView gives the focus to a given view. +func (g *Gui) SetCurrentView(name string) (*View, error) { + for _, v := range g.views { + if v.name == name { + g.currentView = v + return v, nil + } + } + return nil, errors.Wrap(ErrUnknownView, 0) +} + +// CurrentView returns the currently focused view, or nil if no view +// owns the focus. +func (g *Gui) CurrentView() *View { + return g.currentView +} + +// SetKeybinding creates a new keybinding. If viewname equals to "" +// (empty string) then the keybinding will apply to all views. key must +// be a rune or a Key. +func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error { + var kb *keybinding + + k, ch, err := getKey(key) + if err != nil { + return err + } + + if g.isBlacklisted(k) { + return ErrBlacklisted + } + + kb = newKeybinding(viewname, k, ch, mod, handler) + g.keybindings = append(g.keybindings, kb) + return nil +} + +// DeleteKeybinding deletes a keybinding. +func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error { + k, ch, err := getKey(key) + if err != nil { + return err + } + + for i, kb := range g.keybindings { + if kb.viewName == viewname && kb.ch == ch && kb.key == k && kb.mod == mod { + g.keybindings = append(g.keybindings[:i], g.keybindings[i+1:]...) + return nil + } + } + return errors.New("keybinding not found") +} + +// DeleteKeybindings deletes all keybindings of view. +func (g *Gui) DeleteKeybindings(viewname string) { + var s []*keybinding + for _, kb := range g.keybindings { + if kb.viewName != viewname { + s = append(s, kb) + } + } + g.keybindings = s +} + +// BlackListKeybinding adds a keybinding to the blacklist +func (g *Gui) BlacklistKeybinding(k Key) error { + for _, j := range g.blacklist { + if j == k { + return ErrAlreadyBlacklisted + } + } + g.blacklist = append(g.blacklist, k) + return nil +} + +// WhiteListKeybinding removes a keybinding from the blacklist +func (g *Gui) WhitelistKeybinding(k Key) error { + for i, j := range g.blacklist { + if j == k { + g.blacklist = append(g.blacklist[:i], g.blacklist[i+1:]...) + return nil + } + } + return ErrNotBlacklisted +} + +// getKey takes an empty interface with a key and returns the corresponding +// typed Key or rune. +func getKey(key interface{}) (Key, rune, error) { + switch t := key.(type) { + case Key: + return t, 0, nil + case rune: + return 0, t, nil + default: + return 0, 0, errors.New("unknown type") + } +} + +// userEvent represents an event triggered by the user. +type userEvent struct { + f func(*Gui) error +} + +// Update executes the passed function. This method can be called safely from a +// goroutine in order to update the GUI. It is important to note that the +// passed function won't be executed immediately, instead it will be added to +// the user events queue. Given that Update spawns a goroutine, the order in +// which the user events will be handled is not guaranteed. +func (g *Gui) Update(f func(*Gui) error) { + go func() { g.userEvents <- userEvent{f: f} }() +} + +// A Manager is in charge of GUI's layout and can be used to build widgets. +type Manager interface { + // Layout is called every time the GUI is redrawn, it must contain the + // base views and its initializations. + Layout(*Gui) error +} + +// The ManagerFunc type is an adapter to allow the use of ordinary functions as +// Managers. If f is a function with the appropriate signature, ManagerFunc(f) +// is an Manager object that calls f. +type ManagerFunc func(*Gui) error + +// Layout calls f(g) +func (f ManagerFunc) Layout(g *Gui) error { + return f(g) +} + +// SetManager sets the given GUI managers. It deletes all views and +// keybindings. +func (g *Gui) SetManager(managers ...Manager) { + g.managers = managers + g.currentView = nil + g.views = nil + g.keybindings = nil + + go func() { g.tbEvents <- termbox.Event{Type: termbox.EventResize} }() +} + +// SetManagerFunc sets the given manager function. It deletes all views and +// keybindings. +func (g *Gui) SetManagerFunc(manager func(*Gui) error) { + g.SetManager(ManagerFunc(manager)) +} + +// MainLoop runs the main loop until an error is returned. A successful +// finish should return ErrQuit. +func (g *Gui) MainLoop() error { + g.loaderTick() + if err := g.flush(); err != nil { + return err + } + + go func() { + for { + select { + case <-g.stop: + return + default: + g.tbEvents <- termbox.PollEvent() + } + } + }() + + inputMode := termbox.InputAlt + if true { // previously g.InputEsc, but didn't seem to work + inputMode = termbox.InputEsc + } + if g.Mouse { + inputMode |= termbox.InputMouse + } + termbox.SetInputMode(inputMode) + + if err := g.flush(); err != nil { + return err + } + for { + select { + case ev := <-g.tbEvents: + if err := g.handleEvent(&ev); err != nil { + return err + } + case ev := <-g.userEvents: + if err := ev.f(g); err != nil { + return err + } + } + if err := g.consumeevents(); err != nil { + return err + } + if err := g.flush(); err != nil { + return err + } + } +} + +// consumeevents handles the remaining events in the events pool. +func (g *Gui) consumeevents() error { + for { + select { + case ev := <-g.tbEvents: + if err := g.handleEvent(&ev); err != nil { + return err + } + case ev := <-g.userEvents: + if err := ev.f(g); err != nil { + return err + } + default: + return nil + } + } +} + +// handleEvent handles an event, based on its type (key-press, error, +// etc.) +func (g *Gui) handleEvent(ev *termbox.Event) error { + switch ev.Type { + case termbox.EventKey, termbox.EventMouse: + return g.onKey(ev) + case termbox.EventError: + return ev.Err + default: + return nil + } +} + +// flush updates the gui, re-drawing frames and buffers. +func (g *Gui) flush() error { + termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor)) + + maxX, maxY := termbox.Size() + // if GUI's size has changed, we need to redraw all views + if maxX != g.maxX || maxY != g.maxY { + for _, v := range g.views { + v.tainted = true + } + } + g.maxX, g.maxY = maxX, maxY + + for _, m := range g.managers { + if err := m.Layout(g); err != nil { + return err + } + } + for _, v := range g.views { + if !v.Visible || v.y1 < v.y0 { + continue + } + if v.Frame { + var fgColor, bgColor, frameColor Attribute + if g.Highlight && v == g.currentView { + fgColor = g.SelFgColor + bgColor = g.SelBgColor + frameColor = g.SelFrameColor + } else { + fgColor = g.FgColor + bgColor = g.BgColor + frameColor = g.FrameColor + } + + if err := g.drawFrameEdges(v, frameColor, bgColor); err != nil { + return err + } + if err := g.drawFrameCorners(v, frameColor, bgColor); err != nil { + return err + } + if v.Title != "" { + if err := g.drawTitle(v, fgColor, bgColor); err != nil { + return err + } + } + if v.Subtitle != "" { + if err := g.drawSubtitle(v, fgColor, bgColor); err != nil { + return err + } + } + } + if err := g.draw(v); err != nil { + return err + } + } + termbox.Flush() + return nil +} + +// drawFrameEdges draws the horizontal and vertical edges of a view. +func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error { + runeH, runeV := '─', '│' + if g.ASCII { + runeH, runeV = '-', '|' + } + + for x := v.x0 + 1; x < v.x1 && x < g.maxX; x++ { + if x < 0 { + continue + } + if v.y0 > -1 && v.y0 < g.maxY { + if err := g.SetRune(x, v.y0, runeH, fgColor, bgColor); err != nil { + return err + } + } + if v.y1 > -1 && v.y1 < g.maxY { + if err := g.SetRune(x, v.y1, runeH, fgColor, bgColor); err != nil { + return err + } + } + } + for y := v.y0 + 1; y < v.y1 && y < g.maxY; y++ { + if y < 0 { + continue + } + if v.x0 > -1 && v.x0 < g.maxX { + if err := g.SetRune(v.x0, y, runeV, fgColor, bgColor); err != nil { + return err + } + } + if v.x1 > -1 && v.x1 < g.maxX { + if err := g.SetRune(v.x1, y, runeV, fgColor, bgColor); err != nil { + return err + } + } + } + return nil +} + +func cornerRune(index byte) rune { + return []rune{' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '├', '┴', '┬', '┼'}[index] +} + +func corner(v *View, directions byte) rune { + index := v.Overlaps | directions + return cornerRune(index) +} + +// drawFrameCorners draws the corners of the view. +func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error { + if v.y0 == v.y1 { + if !g.SupportOverlaps && v.x0 >= 0 && v.x1 >= 0 && v.y0 >= 0 && v.x0 < g.maxX && v.x1 < g.maxX && v.y0 < g.maxY { + if err := g.SetRune(v.x0, v.y0, '╶', fgColor, bgColor); err != nil { + return err + } + if err := g.SetRune(v.x1, v.y0, '╴', fgColor, bgColor); err != nil { + return err + } + } + return nil + } + + runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘' + if g.SupportOverlaps { + runeTL = corner(v, BOTTOM|RIGHT) + runeTR = corner(v, BOTTOM|LEFT) + runeBL = corner(v, TOP|RIGHT) + runeBR = corner(v, TOP|LEFT) + } + if g.ASCII { + runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+' + } + + corners := []struct { + x, y int + ch rune + }{{v.x0, v.y0, runeTL}, {v.x1, v.y0, runeTR}, {v.x0, v.y1, runeBL}, {v.x1, v.y1, runeBR}} + + for _, c := range corners { + if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY { + if err := g.SetRune(c.x, c.y, c.ch, fgColor, bgColor); err != nil { + return err + } + } + } + return nil +} + +// drawTitle draws the title of the view. +func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error { + if v.y0 < 0 || v.y0 >= g.maxY { + return nil + } + + for i, ch := range v.Title { + x := v.x0 + i + 2 + if x < 0 { + continue + } else if x > v.x1-2 || x >= g.maxX { + break + } + if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil { + return err + } + } + return nil +} + +// drawSubtitle draws the subtitle of the view. +func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error { + if v.y0 < 0 || v.y0 >= g.maxY { + return nil + } + + start := v.x1 - 5 - len(v.Subtitle) + if start < v.x0 { + return nil + } + for i, ch := range v.Subtitle { + x := start + i + if x >= v.x1 { + break + } + if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil { + return err + } + } + return nil +} + +// draw manages the cursor and calls the draw function of a view. +func (g *Gui) draw(v *View) error { + if g.Cursor { + if curview := g.currentView; curview != nil { + vMaxX, vMaxY := curview.Size() + if curview.cx < 0 { + curview.cx = 0 + } else if curview.cx >= vMaxX { + curview.cx = vMaxX - 1 + } + if curview.cy < 0 { + curview.cy = 0 + } else if curview.cy >= vMaxY { + curview.cy = vMaxY - 1 + } + + gMaxX, gMaxY := g.Size() + cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1 + if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY { + termbox.SetCursor(cx, cy) + } else { + termbox.HideCursor() + } + } + } else { + termbox.HideCursor() + } + + v.clearRunes() + if err := v.draw(); err != nil { + return err + } + return nil +} + +// onKey manages key-press events. A keybinding handler is called when +// a key-press or mouse event satisfies a configured keybinding. Furthermore, +// currentView's internal buffer is modified if currentView.Editable is true. +func (g *Gui) onKey(ev *termbox.Event) error { + switch ev.Type { + case termbox.EventKey: + matched, err := g.execKeybindings(g.currentView, ev) + if err != nil { + return err + } + if matched { + break + } + if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil { + g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod)) + } + case termbox.EventMouse: + mx, my := ev.MouseX, ev.MouseY + v, err := g.ViewByPosition(mx, my) + if err != nil { + break + } + if err := v.SetCursor(mx-v.x0-1, my-v.y0-1); err != nil { + return err + } + if _, err := g.execKeybindings(v, ev); err != nil { + return err + } + } + + return nil +} + +// execKeybindings executes the keybinding handlers that match the passed view +// and event. The value of matched is true if there is a match and no errors. +func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) { + var globalKb *keybinding + + for _, kb := range g.keybindings { + if kb.handler == nil { + continue + } + + if !kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) { + continue + } + + if kb.matchView(v) { + return g.execKeybinding(v, kb) + } + + if kb.viewName == "" && ((v != nil && !v.Editable) || kb.ch == 0) { + globalKb = kb + } + } + + if globalKb != nil { + return g.execKeybinding(v, globalKb) + } + + return false, nil +} + +// execKeybinding executes a given keybinding +func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) { + if g.isBlacklisted(kb.key) { + return true, nil + } + + if err := kb.handler(g, v); err != nil { + return false, err + } + return true, nil +} + +// isBlacklisted reports whether the key is blacklisted +func (g *Gui) isBlacklisted(k Key) bool { + for _, j := range g.blacklist { + if j == k { + return true + } + } + return false +} + +// IsUnknownView reports whether the contents of an error is "unknown view". +func IsUnknownView(err error) bool { + return err != nil && err.Error() == ErrUnknownView.Error() +} + +// IsQuit reports whether the contents of an error is "quit". +func IsQuit(err error) bool { + return err != nil && err.Error() == ErrQuit.Error() +} diff --git a/vendor/github.com/awesome-gocui/gocui/gui_others.go b/vendor/github.com/awesome-gocui/gocui/gui_others.go new file mode 100644 index 00000000..5d247a19 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/gui_others.go @@ -0,0 +1,60 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !windows + +package gocui + +import ( + "os" + "os/signal" + "syscall" + "unsafe" + + "github.com/go-errors/errors" +) + +// getTermWindowSize is get terminal window size on linux or unix. +// When gocui run inside the docker contaienr need to check and get the window size. +func (g *Gui) getTermWindowSize() (int, int, error) { + var sz struct { + rows uint16 + cols uint16 + _ [2]uint16 // to match underlying syscall; see https://github.com/awesome-gocui/gocui/issues/33 + } + + var termw, termh int + + out, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) + if err != nil { + return 0, 0, err + } + defer out.Close() + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, syscall.SIGWINCH, syscall.SIGINT) + + for { + _, _, _ = syscall.Syscall(syscall.SYS_IOCTL, + out.Fd(), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) + + // check terminal window size + termw, termh = int(sz.cols), int(sz.rows) + if termw > 0 && termh > 0 { + return termw, termh, nil + } + + select { + case signal := <-signalCh: + switch signal { + // when the terminal window size is changed + case syscall.SIGWINCH: + continue + // ctrl + c to cancel + case syscall.SIGINT: + return 0, 0, errors.New("stop to get term window size") + } + } + } +} diff --git a/vendor/github.com/awesome-gocui/gocui/gui_windows.go b/vendor/github.com/awesome-gocui/gocui/gui_windows.go new file mode 100644 index 00000000..db1faab7 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/gui_windows.go @@ -0,0 +1,53 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package gocui + +import ( + "os" + "syscall" + "unsafe" +) + +type wchar uint16 +type short int16 +type dword uint32 +type word uint16 + +type coord struct { + x short + y short +} + +type smallRect struct { + left short + top short + right short + bottom short +} + +type consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") +) + +// getTermWindowSize is get terminal window size on windows. +func (g *Gui) getTermWindowSize() (int, int, error) { + var csbi consoleScreenBufferInfo + r1, _, err := procGetConsoleScreenBufferInfo.Call(os.Stdout.Fd(), uintptr(unsafe.Pointer(&csbi))) + if r1 == 0 { + return 0, 0, err + } + return int(csbi.window.right - csbi.window.left + 1), int(csbi.window.bottom - csbi.window.top + 1), nil +} diff --git a/vendor/github.com/awesome-gocui/gocui/keybinding.go b/vendor/github.com/awesome-gocui/gocui/keybinding.go new file mode 100644 index 00000000..d294e70d --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/keybinding.go @@ -0,0 +1,285 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import ( + "strings" + + "github.com/awesome-gocui/termbox-go" +) + +// Key represents special keys or keys combinations. +type Key termbox.Key + +// Modifier allows to define special keys combinations. They can be used +// in combination with Keys or Runes when a new keybinding is defined. +type Modifier termbox.Modifier + +// Keybidings are used to link a given key-press event with a handler. +type keybinding struct { + viewName string + key Key + ch rune + mod Modifier + handler func(*Gui, *View) error +} + +// Parse takes the input string and extracts the keybinding. +// Returns a Key / rune, a Modifier and an error. +func Parse(input string) (interface{}, Modifier, error) { + if len(input) == 1 { + _, r, err := getKey(rune(input[0])) + if err != nil { + return nil, ModNone, err + } + return r, ModNone, nil + } + + var modifier Modifier + cleaned := make([]string, 0) + + tokens := strings.Split(input, "+") + for _, t := range tokens { + normalized := strings.Title(strings.ToLower(t)) + if t == "Alt" { + modifier = ModAlt + continue + } + cleaned = append(cleaned, normalized) + } + + key, exist := translate[strings.Join(cleaned, "")] + if !exist { + return nil, ModNone, ErrNoSuchKeybind + } + + return key, modifier, nil +} + +// ParseAll takes an array of strings and returns a map of all keybindings. +func ParseAll(input []string) (map[interface{}]Modifier, error) { + ret := make(map[interface{}]Modifier) + for _, i := range input { + k, m, err := Parse(i) + if err != nil { + return ret, err + } + ret[k] = m + } + return ret, nil +} + +// MustParse takes the input string and returns a Key / rune and a Modifier. +// It will panic if any error occured. +func MustParse(input string) (interface{}, Modifier) { + k, m, err := Parse(input) + if err != nil { + panic(err) + } + return k, m +} + +// MustParseAll takes an array of strings and returns a map of all keybindings. +// It will panic if any error occured. +func MustParseAll(input []string) map[interface{}]Modifier { + result, err := ParseAll(input) + if err != nil { + panic(err) + } + return result +} + +// newKeybinding returns a new Keybinding object. +func newKeybinding(viewname string, key Key, ch rune, mod Modifier, handler func(*Gui, *View) error) (kb *keybinding) { + kb = &keybinding{ + viewName: viewname, + key: key, + ch: ch, + mod: mod, + handler: handler, + } + return kb +} + +// matchKeypress returns if the keybinding matches the keypress. +func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool { + return kb.key == key && kb.ch == ch && kb.mod == mod +} + +// matchView returns if the keybinding matches the current view. +func (kb *keybinding) matchView(v *View) bool { + // if the user is typing in a field, ignore char keys + if v == nil || (v.Editable && kb.ch != 0) { + return false + } + return kb.viewName == v.name +} + +// translations for strings to keys +var translate = map[string]Key{ + "F1": KeyF1, + "F2": KeyF2, + "F3": KeyF3, + "F4": KeyF4, + "F5": KeyF5, + "F6": KeyF6, + "F7": KeyF7, + "F8": KeyF8, + "F9": KeyF9, + "F10": KeyF10, + "F11": KeyF11, + "F12": KeyF12, + "Insert": KeyInsert, + "Delete": KeyDelete, + "Home": KeyHome, + "End": KeyEnd, + "Pgup": KeyPgup, + "Pgdn": KeyPgdn, + "ArrowUp": KeyArrowUp, + "ArrowDown": KeyArrowDown, + "ArrowLeft": KeyArrowLeft, + "ArrowRight": KeyArrowRight, + "CtrlTilde": KeyCtrlTilde, + "Ctrl2": KeyCtrl2, + "CtrlSpace": KeyCtrlSpace, + "CtrlA": KeyCtrlA, + "CtrlB": KeyCtrlB, + "CtrlC": KeyCtrlC, + "CtrlD": KeyCtrlD, + "CtrlE": KeyCtrlE, + "CtrlF": KeyCtrlF, + "CtrlG": KeyCtrlG, + "Backspace": KeyBackspace, + "CtrlH": KeyCtrlH, + "Tab": KeyTab, + "CtrlI": KeyCtrlI, + "CtrlJ": KeyCtrlJ, + "CtrlK": KeyCtrlK, + "CtrlL": KeyCtrlL, + "Enter": KeyEnter, + "CtrlM": KeyCtrlM, + "CtrlN": KeyCtrlN, + "CtrlO": KeyCtrlO, + "CtrlP": KeyCtrlP, + "CtrlQ": KeyCtrlQ, + "CtrlR": KeyCtrlR, + "CtrlS": KeyCtrlS, + "CtrlT": KeyCtrlT, + "CtrlU": KeyCtrlU, + "CtrlV": KeyCtrlV, + "CtrlW": KeyCtrlW, + "CtrlX": KeyCtrlX, + "CtrlY": KeyCtrlY, + "CtrlZ": KeyCtrlZ, + "Esc": KeyEsc, + "CtrlLsqBracket": KeyCtrlLsqBracket, + "Ctrl3": KeyCtrl3, + "Ctrl4": KeyCtrl4, + "CtrlBackslash": KeyCtrlBackslash, + "Ctrl5": KeyCtrl5, + "CtrlRsqBracket": KeyCtrlRsqBracket, + "Ctrl6": KeyCtrl6, + "Ctrl7": KeyCtrl7, + "CtrlSlash": KeyCtrlSlash, + "CtrlUnderscore": KeyCtrlUnderscore, + "Space": KeySpace, + "Backspace2": KeyBackspace2, + "Ctrl8": KeyCtrl8, + "Mouseleft": MouseLeft, + "Mousemiddle": MouseMiddle, + "Mouseright": MouseRight, + "Mouserelease": MouseRelease, + "MousewheelUp": MouseWheelUp, + "MousewheelDown": MouseWheelDown, +} + +// Special keys. +const ( + KeyF1 Key = Key(termbox.KeyF1) + KeyF2 = Key(termbox.KeyF2) + KeyF3 = Key(termbox.KeyF3) + KeyF4 = Key(termbox.KeyF4) + KeyF5 = Key(termbox.KeyF5) + KeyF6 = Key(termbox.KeyF6) + KeyF7 = Key(termbox.KeyF7) + KeyF8 = Key(termbox.KeyF8) + KeyF9 = Key(termbox.KeyF9) + KeyF10 = Key(termbox.KeyF10) + KeyF11 = Key(termbox.KeyF11) + KeyF12 = Key(termbox.KeyF12) + KeyInsert = Key(termbox.KeyInsert) + KeyDelete = Key(termbox.KeyDelete) + KeyHome = Key(termbox.KeyHome) + KeyEnd = Key(termbox.KeyEnd) + KeyPgup = Key(termbox.KeyPgup) + KeyPgdn = Key(termbox.KeyPgdn) + KeyArrowUp = Key(termbox.KeyArrowUp) + KeyArrowDown = Key(termbox.KeyArrowDown) + KeyArrowLeft = Key(termbox.KeyArrowLeft) + KeyArrowRight = Key(termbox.KeyArrowRight) + + MouseLeft = Key(termbox.MouseLeft) + MouseMiddle = Key(termbox.MouseMiddle) + MouseRight = Key(termbox.MouseRight) + MouseRelease = Key(termbox.MouseRelease) + MouseWheelUp = Key(termbox.MouseWheelUp) + MouseWheelDown = Key(termbox.MouseWheelDown) +) + +// Keys combinations. +const ( + KeyCtrlTilde Key = Key(termbox.KeyCtrlTilde) + KeyCtrl2 = Key(termbox.KeyCtrl2) + KeyCtrlSpace = Key(termbox.KeyCtrlSpace) + KeyCtrlA = Key(termbox.KeyCtrlA) + KeyCtrlB = Key(termbox.KeyCtrlB) + KeyCtrlC = Key(termbox.KeyCtrlC) + KeyCtrlD = Key(termbox.KeyCtrlD) + KeyCtrlE = Key(termbox.KeyCtrlE) + KeyCtrlF = Key(termbox.KeyCtrlF) + KeyCtrlG = Key(termbox.KeyCtrlG) + KeyBackspace = Key(termbox.KeyBackspace) + KeyCtrlH = Key(termbox.KeyCtrlH) + KeyTab = Key(termbox.KeyTab) + KeyCtrlI = Key(termbox.KeyCtrlI) + KeyCtrlJ = Key(termbox.KeyCtrlJ) + KeyCtrlK = Key(termbox.KeyCtrlK) + KeyCtrlL = Key(termbox.KeyCtrlL) + KeyEnter = Key(termbox.KeyEnter) + KeyCtrlM = Key(termbox.KeyCtrlM) + KeyCtrlN = Key(termbox.KeyCtrlN) + KeyCtrlO = Key(termbox.KeyCtrlO) + KeyCtrlP = Key(termbox.KeyCtrlP) + KeyCtrlQ = Key(termbox.KeyCtrlQ) + KeyCtrlR = Key(termbox.KeyCtrlR) + KeyCtrlS = Key(termbox.KeyCtrlS) + KeyCtrlT = Key(termbox.KeyCtrlT) + KeyCtrlU = Key(termbox.KeyCtrlU) + KeyCtrlV = Key(termbox.KeyCtrlV) + KeyCtrlW = Key(termbox.KeyCtrlW) + KeyCtrlX = Key(termbox.KeyCtrlX) + KeyCtrlY = Key(termbox.KeyCtrlY) + KeyCtrlZ = Key(termbox.KeyCtrlZ) + KeyEsc = Key(termbox.KeyEsc) + KeyCtrlLsqBracket = Key(termbox.KeyCtrlLsqBracket) + KeyCtrl3 = Key(termbox.KeyCtrl3) + KeyCtrl4 = Key(termbox.KeyCtrl4) + KeyCtrlBackslash = Key(termbox.KeyCtrlBackslash) + KeyCtrl5 = Key(termbox.KeyCtrl5) + KeyCtrlRsqBracket = Key(termbox.KeyCtrlRsqBracket) + KeyCtrl6 = Key(termbox.KeyCtrl6) + KeyCtrl7 = Key(termbox.KeyCtrl7) + KeyCtrlSlash = Key(termbox.KeyCtrlSlash) + KeyCtrlUnderscore = Key(termbox.KeyCtrlUnderscore) + KeySpace = Key(termbox.KeySpace) + KeyBackspace2 = Key(termbox.KeyBackspace2) + KeyCtrl8 = Key(termbox.KeyCtrl8) +) + +// Modifiers. +const ( + ModNone Modifier = Modifier(0) + ModAlt = Modifier(termbox.ModAlt) +) diff --git a/vendor/github.com/awesome-gocui/gocui/loader.go b/vendor/github.com/awesome-gocui/gocui/loader.go new file mode 100644 index 00000000..d6715ac6 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/loader.go @@ -0,0 +1,46 @@ +package gocui + +import "time" + +func (g *Gui) loaderTick() { + go func() { + for range time.Tick(time.Millisecond * 50) { + for _, view := range g.Views() { + if view.HasLoader { + g.userEvents <- userEvent{func(g *Gui) error { return nil }} + break + } + } + } + }() +} + +func (v *View) loaderLines() [][]cell { + duplicate := make([][]cell, len(v.lines)) + for i := range v.lines { + if i < len(v.lines)-1 { + duplicate[i] = make([]cell, len(v.lines[i])) + copy(duplicate[i], v.lines[i]) + } else { + duplicate[i] = make([]cell, len(v.lines[i])+2) + copy(duplicate[i], v.lines[i]) + duplicate[i][len(duplicate[i])-2] = cell{chr: ' '} + duplicate[i][len(duplicate[i])-1] = Loader() + } + } + + return duplicate +} + +// Loader can show a loading animation +func Loader() cell { + characters := "|/-\\" + now := time.Now() + nanos := now.UnixNano() + index := nanos / 50000000 % int64(len(characters)) + str := characters[index : index+1] + chr := []rune(str)[0] + return cell{ + chr: chr, + } +} diff --git a/vendor/github.com/awesome-gocui/gocui/view.go b/vendor/github.com/awesome-gocui/gocui/view.go new file mode 100644 index 00000000..81f90603 --- /dev/null +++ b/vendor/github.com/awesome-gocui/gocui/view.go @@ -0,0 +1,800 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gocui + +import ( + "bytes" + "io" + "strings" + "sync" + "unicode/utf8" + + "github.com/go-errors/errors" + + "github.com/awesome-gocui/termbox-go" + "github.com/mattn/go-runewidth" +) + +// Constants for overlapping edges +const ( + TOP = 1 // view is overlapping at top edge + BOTTOM = 2 // view is overlapping at bottom edge + LEFT = 4 // view is overlapping at left edge + RIGHT = 8 // view is overlapping at right edge +) + +var ( + // ErrInvalidPoint is returned when client passed invalid coordinates of a cell. + // Most likely client has passed negative coordinates of a cell. + ErrInvalidPoint = errors.New("invalid point") +) + +// A View is a window. It maintains its own internal buffer and cursor +// position. +type View struct { + name string + x0, y0, x1, y1 int // left top right bottom + ox, oy int // view offsets + cx, cy int // cursor position + rx, ry int // Read() offsets + wx, wy int // Write() offsets + lines [][]cell // All the data + + // readBuffer is used for storing unread bytes + readBuffer []byte + + // tained is true if the viewLines must be updated + tainted bool + + // internal representation of the view's buffer + viewLines []viewLine + + // writeMutex protects locks the write process + writeMutex sync.Mutex + + // ei is used to decode ESC sequences on Write + ei *escapeInterpreter + + // Visible specifies whether the view is visible. + Visible bool + + // BgColor and FgColor allow to configure the background and foreground + // colors of the View. + BgColor, FgColor Attribute + + // SelBgColor and SelFgColor are used to configure the background and + // foreground colors of the selected line, when it is highlighted. + SelBgColor, SelFgColor Attribute + + // If Editable is true, keystrokes will be added to the view's internal + // buffer at the cursor position. + Editable bool + + // Editor allows to define the editor that manages the editing mode, + // including keybindings or cursor behaviour. DefaultEditor is used by + // default. + Editor Editor + + // Overwrite enables or disables the overwrite mode of the view. + Overwrite bool + + // If Highlight is true, Sel{Bg,Fg}Colors will be used + // for the line under the cursor position. + Highlight bool + + // If Frame is true, a border will be drawn around the view. + Frame bool + + // If Wrap is true, the content that is written to this View is + // automatically wrapped when it is longer than its width. If true the + // view's x-origin will be ignored. + Wrap bool + + // If Autoscroll is true, the View will automatically scroll down when the + // text overflows. If true the view's y-origin will be ignored. + Autoscroll bool + + // If Frame is true, Title allows to configure a title for the view. + Title string + + // If Frame is true, Subtitle allows to configure a subtitle for the view. + Subtitle string + + // If Mask is true, the View will display the mask instead of the real + // content + Mask rune + + // Overlaps describes which edges are overlapping with another view's edges + Overlaps byte + + // If HasLoader is true, the message will be appended with a spinning loader animation + HasLoader bool +} + +type viewLine struct { + linesX, linesY int // coordinates relative to v.lines + line []cell +} + +type cell struct { + chr rune + bgColor, fgColor Attribute +} + +type lineType []cell + +// String returns a string from a given cell slice. +func (l lineType) String() string { + str := "" + for _, c := range l { + str += string(c.chr) + } + return str +} + +// newView returns a new View object. +func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { + v := &View{ + name: name, + x0: x0, + y0: y0, + x1: x1, + y1: y1, + Visible: true, + Frame: true, + Editor: DefaultEditor, + tainted: true, + ei: newEscapeInterpreter(mode), + } + return v +} + +// Dimensions returns the dimensions of the View +func (v *View) Dimensions() (int, int, int, int) { + return v.x0, v.y0, v.x1, v.y1 +} + +// Size returns the number of visible columns and rows in the View. +func (v *View) Size() (x, y int) { + return v.x1 - v.x0 - 1, v.y1 - v.y0 - 1 +} + +// Name returns the name of the view. +func (v *View) Name() string { + return v.name +} + +// setRune sets a rune at the given point relative to the view. It applies the +// specified colors, taking into account if the cell must be highlighted. Also, +// it checks if the position is valid. +func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { + maxX, maxY := v.Size() + if x < 0 || x >= maxX || y < 0 || y >= maxY { + return ErrInvalidPoint + } + var ( + ry, rcy int + err error + ) + if v.Highlight { + _, ry, err = v.realPosition(x, y) + if err != nil { + return err + } + _, rcy, err = v.realPosition(v.cx, v.cy) + if err != nil { + return err + } + } + + if v.Mask != 0 { + fgColor = v.FgColor + bgColor = v.BgColor + ch = v.Mask + } else if v.Highlight && ry == rcy { + fgColor = fgColor | AttrBold + } + + // Don't display NUL characters + if ch == 0 { + ch = ' ' + } + + termbox.SetCell(v.x0+x+1, v.y0+y+1, ch, + termbox.Attribute(fgColor), termbox.Attribute(bgColor)) + + return nil +} + +// SetCursor sets the cursor position of the view at the given point, +// relative to the view. It checks if the position is valid. +func (v *View) SetCursor(x, y int) error { + maxX, maxY := v.Size() + if x < 0 || x >= maxX || y < 0 || y >= maxY { + return ErrInvalidPoint + } + v.cx = x + v.cy = y + return nil +} + +// Cursor returns the cursor position of the view. +func (v *View) Cursor() (x, y int) { + return v.cx, v.cy +} + +// SetOrigin sets the origin position of the view's internal buffer, +// so the buffer starts to be printed from this point, which means that +// it is linked with the origin point of view. It can be used to +// implement Horizontal and Vertical scrolling with just incrementing +// or decrementing ox and oy. +func (v *View) SetOrigin(x, y int) error { + if x < 0 || y < 0 { + return ErrInvalidPoint + } + v.ox = x + v.oy = y + return nil +} + +// Origin returns the origin position of the view. +func (v *View) Origin() (x, y int) { + return v.ox, v.oy +} + +// SetWritePos sets the write position of the view's internal buffer. +// So the next Write call would write directly to the specified position. +func (v *View) SetWritePos(x, y int) error { + if x < 0 || y < 0 { + return ErrInvalidPoint + } + v.wx = x + v.wy = y + return nil +} + +// WritePos returns the current write position of the view's internal buffer. +func (v *View) WritePos() (x, y int) { + return v.wx, v.wy +} + +// SetReadPos sets the read position of the view's internal buffer. +// So the next Read call would read from the specified position. +func (v *View) SetReadPos(x, y int) error { + if x < 0 || y < 0 { + return ErrInvalidPoint + } + v.readBuffer = nil + v.rx = x + v.ry = y + return nil +} + +// ReadPos returns the current read position of the view's internal buffer. +func (v *View) ReadPos() (x, y int) { + return v.rx, v.ry +} + +// makeWriteable creates empty cells if required to make position (x, y) writeable. +func (v *View) makeWriteable(x, y int) { + // TODO: make this more efficient + + // line `y` must be index-able (that's why `<=`) + for len(v.lines) <= y { + if cap(v.lines) > len(v.lines) { + newLen := cap(v.lines) + if newLen > y { + newLen = y + 1 + } + v.lines = v.lines[:newLen] + } else { + v.lines = append(v.lines, nil) + } + } + // cell `x` must not be index-able (that's why `<`) + // append should be used by `lines[y]` user if he wants to write beyond `x` + for len(v.lines[y]) < x { + if cap(v.lines[y]) > len(v.lines[y]) { + newLen := cap(v.lines[y]) + if newLen > x { + newLen = x + } + v.lines[y] = v.lines[y][:newLen] + } else { + v.lines[y] = append(v.lines[y], cell{}) + } + } +} + +// writeCells copies []cell to specified location (x, y) +// !!! caller MUST ensure that specified location (x, y) is writeable by calling makeWriteable +func (v *View) writeCells(x, y int, cells []cell) { + var newLen int + // use maximum len available + line := v.lines[y][:cap(v.lines[y])] + maxCopy := len(line) - x + if maxCopy < len(cells) { + copy(line[x:], cells[:maxCopy]) + line = append(line, cells[maxCopy:]...) + newLen = len(line) + } else { // maxCopy >= len(cells) + copy(line[x:], cells) + newLen = x + len(cells) + if newLen < len(v.lines[y]) { + newLen = len(v.lines[y]) + } + } + v.lines[y] = line[:newLen] +} + +// Write appends a byte slice into the view's internal buffer. Because +// View implements the io.Writer interface, it can be passed as parameter +// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must +// be called to clear the view's buffer. +func (v *View) Write(p []byte) (n int, err error) { + v.tainted = true + v.writeMutex.Lock() + v.makeWriteable(v.wx, v.wy) + v.writeRunes(bytes.Runes(p)) + v.writeMutex.Unlock() + + return len(p), nil +} + +func (v *View) WriteRunes(p []rune) { + v.tainted = true + + // Fill with empty cells, if writing outside current view buffer + v.makeWriteable(v.wx, v.wy) + v.writeRunes(p) +} + +func (v *View) WriteString(s string) { + v.WriteRunes([]rune(s)) +} + +// writeRunes copies slice of runes into internal lines buffer. +// caller must make sure that writing position is accessable. +func (v *View) writeRunes(p []rune) { + for _, r := range p { + switch r { + case '\n': + v.wy++ + if v.wy >= len(v.lines) { + v.lines = append(v.lines, nil) + } + + fallthrough + // not valid in every OS, but making runtime OS checks in cycle is bad. + case '\r': + v.wx = 0 + default: + cells := v.parseInput(r) + if cells == nil { + continue + } + v.writeCells(v.wx, v.wy, cells) + v.wx += len(cells) + } + } +} + +// parseInput parses char by char the input written to the View. It returns nil +// while processing ESC sequences. Otherwise, it returns a cell slice that +// contains the processed data. +func (v *View) parseInput(ch rune) []cell { + cells := []cell{} + + isEscape, err := v.ei.parseOne(ch) + if err != nil { + for _, r := range v.ei.runes() { + c := cell{ + fgColor: v.FgColor, + bgColor: v.BgColor, + chr: r, + } + cells = append(cells, c) + } + v.ei.reset() + } else { + if isEscape { + return nil + } + repeatCount := 1 + if ch == '\t' { + ch = ' ' + repeatCount = 4 + } + for i := 0; i < repeatCount; i++ { + c := cell{ + fgColor: v.ei.curFgColor, + bgColor: v.ei.curBgColor, + chr: ch, + } + cells = append(cells, c) + } + } + + return cells +} + +// Read reads data into p from the current reading position set by SetReadPos. +// It returns the number of bytes read into p. +// At EOF, err will be io.EOF. +func (v *View) Read(p []byte) (n int, err error) { + buffer := make([]byte, utf8.UTFMax) + offset := 0 + if v.readBuffer != nil { + copy(p, v.readBuffer) + if len(v.readBuffer) >= len(p) { + if len(v.readBuffer) > len(p) { + v.readBuffer = v.readBuffer[len(p):] + } + return len(p), nil + } + v.readBuffer = nil + } + for v.ry < len(v.lines) { + for v.rx < len(v.lines[v.ry]) { + count := utf8.EncodeRune(buffer, v.lines[v.ry][v.rx].chr) + copy(p[offset:], buffer[:count]) + v.rx++ + newOffset := offset + count + if newOffset >= len(p) { + if newOffset > len(p) { + v.readBuffer = buffer[newOffset-len(p):] + } + return len(p), nil + } + offset += count + } + v.rx = 0 + v.ry++ + } + return offset, io.EOF +} + +// Rewind sets read and write pos to (0, 0). +func (v *View) Rewind() { + if err := v.SetReadPos(0, 0); err != nil { + // SetReadPos returns error only if x and y are negative + // we are passing 0, 0, thus no error should occur. + panic(err) + } + if err := v.SetWritePos(0, 0); err != nil { + // SetWritePos returns error only if x and y are negative + // we are passing 0, 0, thus no error should occur. + panic(err) + } +} + +// IsTainted tells us if the view is tainted +func (v *View) IsTainted() bool { + return v.tainted +} + +// draw re-draws the view's contents. +func (v *View) draw() error { + if !v.Visible { + return nil + } + + maxX, maxY := v.Size() + + if v.Wrap { + if maxX == 0 { + return errors.New("X size of the view cannot be 0") + } + v.ox = 0 + } + if v.tainted { + v.viewLines = nil + lines := v.lines + if v.HasLoader { + lines = v.loaderLines() + } + for i, line := range lines { + wrap := 0 + if v.Wrap { + wrap = maxX + } + + ls := lineWrap(line, wrap) + for j := range ls { + vline := viewLine{linesX: j, linesY: i, line: ls[j]} + v.viewLines = append(v.viewLines, vline) + } + } + if !v.HasLoader { + v.tainted = false + } + } + + if v.Autoscroll && len(v.viewLines) > maxY { + v.oy = len(v.viewLines) - maxY + } + y := 0 + for i, vline := range v.viewLines { + if i < v.oy { + continue + } + if y >= maxY { + break + } + x := 0 + for j, c := range vline.line { + if j < v.ox { + continue + } + if x >= maxX { + break + } + + fgColor := c.fgColor + if fgColor == ColorDefault { + fgColor = v.FgColor + } + bgColor := c.bgColor + if bgColor == ColorDefault { + bgColor = v.BgColor + } + + if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil { + return err + } + + if c.chr != 0 { + // If it is a rune, add rune width + x += runewidth.RuneWidth(c.chr) + } else { + // If it is NULL rune, add 1 to be able to use SetWritePos + // (runewidth.RuneWidth of space is 1) + x++ + } + } + y++ + } + return nil +} + +// realPosition returns the position in the internal buffer corresponding to the +// point (x, y) of the view. +func (v *View) realPosition(vx, vy int) (x, y int, err error) { + vx = v.ox + vx + vy = v.oy + vy + + if vx < 0 || vy < 0 { + return 0, 0, ErrInvalidPoint + } + + if len(v.viewLines) == 0 { + return vx, vy, nil + } + + if vy < len(v.viewLines) { + vline := v.viewLines[vy] + x = vline.linesX + vx + y = vline.linesY + } else { + vline := v.viewLines[len(v.viewLines)-1] + x = vx + y = vline.linesY + vy - len(v.viewLines) + 1 + } + + return x, y, nil +} + +// Clear empties the view's internal buffer. +// And resets reading and writing offsets. +func (v *View) Clear() { + v.writeMutex.Lock() + v.Rewind() + v.tainted = true + v.ei.reset() + v.lines = nil + v.viewLines = nil + v.clearRunes() + v.writeMutex.Unlock() +} + +// clearRunes erases all the cells in the view. +func (v *View) clearRunes() { + maxX, maxY := v.Size() + for x := 0; x < maxX; x++ { + for y := 0; y < maxY; y++ { + termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ', + termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor)) + } + } +} + +// BufferLines returns the lines in the view's internal +// buffer. +func (v *View) BufferLines() []string { + lines := make([]string, len(v.lines)) + for i, l := range v.lines { + str := lineType(l).String() + str = strings.Replace(str, "\x00", " ", -1) + lines[i] = str + } + return lines +} + +// Buffer returns a string with the contents of the view's internal +// buffer. +func (v *View) Buffer() string { + return linesToString(v.lines) +} + +// ViewBufferLines returns the lines in the view's internal +// buffer that is shown to the user. +func (v *View) ViewBufferLines() []string { + lines := make([]string, len(v.viewLines)) + for i, l := range v.viewLines { + str := lineType(l.line).String() + str = strings.Replace(str, "\x00", " ", -1) + lines[i] = str + } + return lines +} + +// LinesHeight is the count of view lines (i.e. lines excluding wrapping) +func (v *View) LinesHeight() int { + return len(v.lines) +} + +// ViewLinesHeight is the count of view lines (i.e. lines including wrapping) +func (v *View) ViewLinesHeight() int { + return len(v.viewLines) +} + +// ViewBuffer returns a string with the contents of the view's buffer that is +// shown to the user. +func (v *View) ViewBuffer() string { + lines := make([][]cell, len(v.viewLines)) + for i := range v.viewLines { + lines[i] = v.viewLines[i].line + } + + return linesToString(lines) +} + +// Line returns a string with the line of the view's internal buffer +// at the position corresponding to the point (x, y). +func (v *View) Line(y int) (string, error) { + _, y, err := v.realPosition(0, y) + if err != nil { + return "", err + } + + if y < 0 || y >= len(v.lines) { + return "", ErrInvalidPoint + } + + return lineType(v.lines[y]).String(), nil +} + +// Word returns a string with the word of the view's internal buffer +// at the position corresponding to the point (x, y). +func (v *View) Word(x, y int) (string, error) { + x, y, err := v.realPosition(x, y) + if err != nil { + return "", err + } + + if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) { + return "", ErrInvalidPoint + } + + str := lineType(v.lines[y]).String() + + nl := strings.LastIndexFunc(str[:x], indexFunc) + if nl == -1 { + nl = 0 + } else { + nl = nl + 1 + } + nr := strings.IndexFunc(str[x:], indexFunc) + if nr == -1 { + nr = len(str) + } else { + nr = nr + x + } + return string(str[nl:nr]), nil +} + +// indexFunc allows to split lines by words taking into account spaces +// and 0. +func indexFunc(r rune) bool { + return r == ' ' || r == 0 +} + +// SetLine changes the contents of an existing line. +func (v *View) SetLine(y int, text string) error { + if y < 0 || y >= len(v.lines) { + err := ErrInvalidPoint + return err + } + + v.tainted = true + line := make([]cell, 0) + for _, r := range text { + c := v.parseInput(r) + line = append(line, c...) + } + v.lines[y] = line + return nil +} + +// SetHighlight toggles highlighting of separate lines, for custom lists +// or multiple selection in views. +func (v *View) SetHighlight(y int, on bool) error { + if y < 0 || y >= len(v.lines) { + err := ErrInvalidPoint + return err + } + + line := v.lines[y] + cells := make([]cell, 0) + for _, c := range line { + if on { + c.bgColor = v.SelBgColor + c.fgColor = v.SelFgColor + } else { + c.bgColor = v.BgColor + c.fgColor = v.FgColor + } + cells = append(cells, c) + } + v.tainted = true + v.lines[y] = cells + return nil +} + +func lineWidth(line []cell) (n int) { + for i := range line { + n += runewidth.RuneWidth(line[i].chr) + } + + return +} + +func lineWrap(line []cell, columns int) [][]cell { + if columns == 0 { + return [][]cell{line} + } + + var n int + var offset int + lines := make([][]cell, 0, 1) + for i := range line { + rw := runewidth.RuneWidth(line[i].chr) + n += rw + if n > columns { + n = rw + lines = append(lines, line[offset:i]) + offset = i + } + } + + lines = append(lines, line[offset:]) + return lines +} + +func linesToString(lines [][]cell) string { + str := make([]string, len(lines)) + for i := range lines { + rns := make([]rune, 0, len(lines[i])) + line := lineType(lines[i]).String() + for _, c := range line { + if c != '\x00' { + rns = append(rns, c) + } + } + str[i] = string(rns) + } + + return strings.Join(str, "\n") +} -- cgit