diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..89d5f0ad --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,34 @@ +version: 2 +jobs: + build: + working_directory: /go/src/github.com/glvr182/git-profile + + docker: + - image: circleci/golang:1.12 + + steps: + - checkout + - run: + name: go mod + command: go mod tidy + - run: + name: go fmt + command: | + if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then + find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \; + exit 1; + fi + - run: + name: go build + command: | + cd _examples/ + for file in *.go + do + go build $file + done + +workflows: + version: 2 + build: + jobs: + - build \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..50323165 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - OS: [e.g. Linux] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..0ff45a24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe the feature you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request.md b/.github/PULL_REQUEST_TEMPLATE/pull_request.md new file mode 100644 index 00000000..ce5fc11b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request.md @@ -0,0 +1,15 @@ +--- +name: Pull request +about: Create a pull request to solve an issue +title: "" +labels: +assignees: '' + +--- + +**Related issue** + +**Describe the fix** + +**Screenshots** +If applicable, add screenshots to help explain your implementation. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..1bdac055 --- /dev/null +++ b/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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b93e45b2 --- /dev/null +++ b/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/README.md b/README.md index d7b55a3b..be212c58 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # 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) -[![GoDoc](https://godoc.org/github.com/jroimartin/gocui?status.svg)](https://godoc.org/github.com/jroimartin/gocui) - -Minimalist Go package aimed at creating Console User Interfaces. +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 @@ -13,15 +18,29 @@ Minimalist Go package aimed at creating Console User Interfaces. * Global and view-level keybindings. * Mouse support. * Colored text. -* Customizable edition mode. +* 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/jroimartin/gocui +$ go get github.com/awesome-gocui/gocui ``` ## Documentation @@ -29,13 +48,14 @@ $ go get github.com/jroimartin/gocui Execute: ``` -$ go doc github.com/jroimartin/gocui +$ go doc github.com/awesome-gocui/gocui ``` -Or visit [godoc.org](https://godoc.org/github.com/jroimartin/gocui) to read it +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 @@ -44,11 +64,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, false) if err != nil { log.Panicln(err) } @@ -60,18 +80,21 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + 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); err != nil { - if err != gocui.ErrUnknownView { + 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 } @@ -106,5 +129,7 @@ func quit(g *gocui.Gui, v *gocui.View) error { * [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/_examples/active.go b/_examples/active.go index 4c3e19b7..77ca11d9 100644 --- a/_examples/active.go +++ b/_examples/active.go @@ -8,7 +8,7 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) var ( @@ -49,8 +49,8 @@ func nextView(g *gocui.Gui, v *gocui.View) error { func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if v, err := g.SetView("v1", 0, 0, maxX/2-1, maxY/2-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v1", 0, 0, maxX/2-1, maxY/2-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "v1 (editable)" @@ -62,16 +62,16 @@ func layout(g *gocui.Gui) error { } } - if v, err := g.SetView("v2", maxX/2-1, 0, maxX-1, maxY/2-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v2", maxX/2-1, 0, maxX-1, maxY/2-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "v2" v.Wrap = true v.Autoscroll = true } - if v, err := g.SetView("v3", 0, maxY/2-1, maxX/2-1, maxY-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v3", 0, maxY/2-1, maxX/2-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "v3" @@ -79,8 +79,8 @@ func layout(g *gocui.Gui) error { v.Autoscroll = true fmt.Fprint(v, "Press TAB to change current view") } - if v, err := g.SetView("v4", maxX/2, maxY/2, maxX-1, maxY-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v4", maxX/2, maxY/2, maxX-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "v4 (editable)" @@ -94,7 +94,7 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -113,7 +113,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/bufs.go b/_examples/bufs.go index 7d145006..ac45cb1b 100644 --- a/_examples/bufs.go +++ b/_examples/bufs.go @@ -10,7 +10,7 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) var vbuf, buf string @@ -28,8 +28,8 @@ func overwrite(g *gocui.Gui, v *gocui.View) error { func layout(g *gocui.Gui) error { _, maxY := g.Size() - if v, err := g.SetView("main", 0, 0, 20, maxY-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("main", 0, 0, 20, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Editable = true @@ -42,7 +42,7 @@ func layout(g *gocui.Gui) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -59,7 +59,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } diff --git a/_examples/colors.go b/_examples/colors.go index 9542c6d7..62dc3835 100644 --- a/_examples/colors.go +++ b/_examples/colors.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -24,15 +24,15 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + 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("colors", maxX/2-7, maxY/2-12, maxX/2+7, maxY/2+13); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("colors", maxX/2-7, maxY/2-12, maxX/2+7, maxY/2+13, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } for i := 0; i <= 7; i++ { @@ -40,6 +40,9 @@ func layout(g *gocui.Gui) error { fmt.Fprintf(v, "Hello \033[3%d;%dmcolors!\033[0m\n", i, j) } } + if _, err := g.SetCurrentView("colors"); err != nil { + return err + } } return nil } diff --git a/_examples/colors256.go b/_examples/colors256.go index 3abd4234..245b7f7e 100644 --- a/_examples/colors256.go +++ b/_examples/colors256.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.Output256) + g, err := gocui.NewGui(gocui.Output256, true) if err != nil { log.Panicln(err) @@ -25,15 +25,15 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + 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("colors", -1, -1, maxX, maxY); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("colors", -1, -1, maxX, maxY, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } @@ -65,6 +65,9 @@ func layout(g *gocui.Gui) error { ctr++ } } + if _, err := g.SetCurrentView("colors"); err != nil { + return err + } } return nil } diff --git a/_examples/demo.go b/_examples/demo.go index 8f4b1ec3..1abc5eed 100644 --- a/_examples/demo.go +++ b/_examples/demo.go @@ -11,7 +11,7 @@ import ( "log" "strings" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func nextView(g *gocui.Gui, v *gocui.View) error { @@ -59,8 +59,8 @@ func getLine(g *gocui.Gui, v *gocui.View) error { } maxX, maxY := g.Size() - if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("msg", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, l) @@ -159,8 +159,8 @@ func saveVisualMain(g *gocui.Gui, v *gocui.View) error { func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if v, err := g.SetView("side", -1, -1, 30, maxY); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("side", -1, -1, 30, maxY, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Highlight = true @@ -172,8 +172,8 @@ func layout(g *gocui.Gui) error { fmt.Fprint(v, "\rWill be") fmt.Fprint(v, "deleted\rItem 4\nItem 5") } - if v, err := g.SetView("main", 30, -1, maxX, maxY); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("main", 30, -1, maxX, maxY, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } b, err := ioutil.ReadFile("Mark.Twain-Tom.Sawyer.txt") @@ -191,7 +191,7 @@ func layout(g *gocui.Gui) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -205,7 +205,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/dynamic.go b/_examples/dynamic.go index f1ec0472..d73d1178 100644 --- a/_examples/dynamic.go +++ b/_examples/dynamic.go @@ -9,7 +9,7 @@ import ( "log" "strings" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) const delta = 1 @@ -21,7 +21,7 @@ var ( ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -39,16 +39,16 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } func layout(g *gocui.Gui) error { maxX, _ := g.Size() - v, err := g.SetView("help", maxX-25, 0, maxX-1, 9) + v, err := g.SetView("help", maxX-25, 0, maxX-1, 9, 0) if err != nil { - if err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, "KEYBINDINGS") @@ -132,9 +132,9 @@ func initKeybindings(g *gocui.Gui) error { func newView(g *gocui.Gui) error { maxX, maxY := g.Size() name := fmt.Sprintf("v%v", idxView) - v, err := g.SetView(name, maxX/2-5, maxY/2-5, maxX/2+5, maxY/2+5) + v, err := g.SetView(name, maxX/2-5, maxY/2-5, maxX/2+5, maxY/2+5, 0) if err != nil { - if err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { return err } v.Wrap = true @@ -183,7 +183,7 @@ func moveView(g *gocui.Gui, v *gocui.View, dx, dy int) error { if err != nil { return err } - if _, err := g.SetView(name, x0+dx, y0+dy, x1+dx, y1+dy); err != nil { + if _, err := g.SetView(name, x0+dx, y0+dy, x1+dx, y1+dy, 0); err != nil { return err } return nil diff --git a/_examples/flow_layout.go b/_examples/flow_layout.go index e2cae79f..bf71aa0b 100644 --- a/_examples/flow_layout.go +++ b/_examples/flow_layout.go @@ -9,7 +9,7 @@ import ( "log" "strings" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) type Label struct { @@ -34,12 +34,15 @@ func NewLabel(name string, body string) *Label { } func (w *Label) Layout(g *gocui.Gui) error { - v, err := g.SetView(w.name, 0, 0, w.w, w.h) + v, err := g.SetView(w.name, 0, 0, w.w, w.h, 0) if err != nil { - if err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { return err } fmt.Fprint(v, w.body) + if _, err := g.SetCurrentView(w.name); err != nil { + return err + } } return nil } @@ -49,8 +52,8 @@ func flowLayout(g *gocui.Gui) error { x := 0 for _, v := range views { w, h := v.Size() - _, err := g.SetView(v.Name(), x, 0, x+w+1, h+1) - if err != nil && err != gocui.ErrUnknownView { + _, err := g.SetView(v.Name(), x, 0, x+w+1, h+1, 0) + if err != nil && !gocui.IsUnknownView(err) { return err } x += w + 2 @@ -59,7 +62,7 @@ func flowLayout(g *gocui.Gui) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -77,7 +80,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/goroutine.go b/_examples/goroutine.go index 81c08350..1c8214b6 100644 --- a/_examples/goroutine.go +++ b/_examples/goroutine.go @@ -10,10 +10,10 @@ import ( "sync" "time" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) -const NumGoroutines = 10 +const NumGoroutines = 20 var ( done = make(chan struct{}) @@ -24,7 +24,7 @@ var ( ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -41,7 +41,7 @@ func main() { go counter(g) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } @@ -49,11 +49,14 @@ func main() { } func layout(g *gocui.Gui) error { - if v, err := g.SetView("ctr", 2, 2, 12, 4); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("ctr", 2, 2, 22, 2+NumGoroutines+1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Clear() + if _, err := g.SetCurrentView("ctr"); err != nil { return err } - fmt.Fprintln(v, "0") } return nil } @@ -88,7 +91,14 @@ func counter(g *gocui.Gui) { if err != nil { return err } - v.Clear() + // use ctr to make it more chaotic + // "pseudo-randomly" print in one of two columns (x = 0, and x = 10) + x := (ctr / NumGoroutines) & 1 + if x != 0 { + x = 10 + } + y := ctr % NumGoroutines + v.SetWritePos(x, y) fmt.Fprintln(v, n) return nil }) diff --git a/_examples/hello.go b/_examples/hello.go index fc3c7f87..8e30bbf8 100644 --- a/_examples/hello.go +++ b/_examples/hello.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -24,19 +24,25 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { + log.Panicln(err.Error()) } } 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); err != nil { - if err != gocui.ErrUnknownView { + 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 + } + + if _, err := g.SetCurrentView("hello"); err != nil { return err } + fmt.Fprintln(v, "Hello world!") } + return nil } diff --git a/_examples/keybinds.go b/_examples/keybinds.go new file mode 100644 index 00000000..ccd900d0 --- /dev/null +++ b/_examples/keybinds.go @@ -0,0 +1,89 @@ +package main + +import ( + "log" + + "github.com/awesome-gocui/gocui" +) + +// layout generates the view +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 + } + + v.Write([]byte("Hello")) + + if _, err := g.SetCurrentView("hello"); err != nil { + return err + } + } + + return nil +} + +// quit stops the gui +func quit(_ *gocui.Gui, _ *gocui.View) error { + return gocui.ErrQuit +} + +func main() { + // Create a gui + g, err := gocui.NewGui(gocui.OutputNormal, false) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + // Add a manager function + g.SetManagerFunc(layout) + + // This will set up the recovery for MustParse + defer func() { + if r := recover(); r != nil { + log.Panicln("Error caught: ", r) + } + }() + + // The MustParse can panic, but only returns 2 values instead of 3 + keyForced, modForced := gocui.MustParse("q") + if err := g.SetKeybinding("", keyForced, modForced, quit); err != nil { + log.Panicln(err) + } + + // We can blacklist a keybinding. + // This allows us to prevent setting the keybinding. + if err := g.BlacklistKeybinding(gocui.KeyCtrlC); err != nil { + log.Panic(err) + } + + // If for some reason you want to whitelist the keybinding, + // you can allow it again by calling g.WhitelistKeybinding. + if err := g.WhitelistKeybinding(gocui.KeyCtrlC); err != nil { + log.Panic(err) + } + + // The normal parse returns an key, a modifier and an error + keyNormal, modNormal, err := gocui.Parse("Ctrl+C") + if err != nil { + log.Panicln(err) + } + + if err = g.SetKeybinding("", keyNormal, modNormal, quit); err != nil { + log.Panicln(err) + } + + // You can still block it when it is set, just blacklist it again, this will not throw + // an error at parsing, since it is already parsed above, + // but it will prevent it from being executed + //if err := g.BlacklistKeybinding(gocui.KeyCtrlC); err != nil { + // log.Panicln(err) + //} + + // Now just start a mainloop for the demo + if err = g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} diff --git a/_examples/layout.go b/_examples/layout.go index 3e9c3e32..da81aa8d 100644 --- a/_examples/layout.go +++ b/_examples/layout.go @@ -7,23 +7,25 @@ package main import ( "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if _, err := g.SetView("side", -1, -1, int(0.2*float32(maxX)), maxY-5); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("side", -1, -1, int(0.2*float32(maxX)), maxY-5, 0); err != nil && !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("main", int(0.2*float32(maxX)), -1, maxX, maxY-5); err != nil && - err != gocui.ErrUnknownView { - return err + if _, err := g.SetView("main", int(0.2*float32(maxX)), -1, maxX, maxY-5, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + + g.SetCurrentView("main") } - if _, err := g.SetView("cmdline", -1, maxY-5, maxX, maxY); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("cmdline", -1, maxY-5, maxX, maxY, 0); err != nil && !gocui.IsUnknownView(err) { return err } + return nil } @@ -32,7 +34,7 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -44,7 +46,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/mask.go b/_examples/mask.go index 481f17e5..01a7a5f7 100644 --- a/_examples/mask.go +++ b/_examples/mask.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Fatalln(err) } @@ -26,7 +26,7 @@ func main() { log.Fatalln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Fatalln(err) } } @@ -34,8 +34,8 @@ func main() { func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if v, err := g.SetView("help", maxX-23, 0, maxX-1, 3); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("help", maxX-23, 0, maxX-1, 3, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Keybindings" @@ -43,8 +43,8 @@ func layout(g *gocui.Gui) error { fmt.Fprintln(v, "^c: Exit") } - if v, err := g.SetView("input", 0, 0, maxX-24, maxY-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("input", 0, 0, maxX-24, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } if _, err := g.SetCurrentView("input"); err != nil { diff --git a/_examples/mouse.go b/_examples/mouse.go index 38dc8862..7226281d 100644 --- a/_examples/mouse.go +++ b/_examples/mouse.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -27,14 +27,14 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } func layout(g *gocui.Gui) error { - if v, err := g.SetView("but1", 2, 2, 22, 7); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("but1", 2, 2, 22, 7, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Highlight = true @@ -44,9 +44,12 @@ func layout(g *gocui.Gui) error { fmt.Fprintln(v, "Button 1 - line 2") fmt.Fprintln(v, "Button 1 - line 3") fmt.Fprintln(v, "Button 1 - line 4") + if _, err := g.SetCurrentView("but1"); err != nil { + return err + } } - if v, err := g.SetView("but2", 24, 2, 44, 4); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("but2", 24, 2, 44, 4, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Highlight = true @@ -90,8 +93,8 @@ func showMsg(g *gocui.Gui, v *gocui.View) error { } maxX, maxY := g.Size() - if v, err := g.SetView("msg", maxX/2-10, maxY/2, maxX/2+10, maxY/2+2); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("msg", maxX/2-10, maxY/2, maxX/2+10, maxY/2+2, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, l) diff --git a/_examples/ontop.go b/_examples/ontop.go index 675f5937..3f000208 100644 --- a/_examples/ontop.go +++ b/_examples/ontop.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -24,29 +24,32 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } func layout(g *gocui.Gui) error { - if v, err := g.SetView("v1", 10, 2, 30, 6); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v1", 10, 2, 30, 6, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, "View #1") } - if v, err := g.SetView("v2", 20, 4, 40, 8); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v2", 20, 4, 40, 8, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, "View #2") } - if v, err := g.SetView("v3", 30, 6, 50, 10); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v3", 30, 6, 50, 10, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, "View #3") + if _, err := g.SetCurrentView("v3"); err != nil { + return err + } } return nil diff --git a/_examples/overlap.go b/_examples/overlap.go index 5cadf949..e6dc672f 100644 --- a/_examples/overlap.go +++ b/_examples/overlap.go @@ -7,46 +7,50 @@ package main import ( "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if _, err := g.SetView("v1", -1, -1, 10, 10); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v1", -1, -1, 10, 10, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v2", maxX-10, -1, maxX, 10); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v2", maxX-10, -1, maxX, 10, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v3", maxX/2-5, -1, maxX/2+5, 10); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v3", maxX/2-5, -1, maxX/2+5, 10, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v4", -1, maxY/2-5, 10, maxY/2+5); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v4", -1, maxY/2-5, 10, maxY/2+5, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v5", maxX-10, maxY/2-5, maxX, maxY/2+5); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v5", maxX-10, maxY/2-5, maxX, maxY/2+5, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v6", -1, maxY-10, 10, maxY); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v6", -1, maxY-10, 10, maxY, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v7", maxX-10, maxY-10, maxX, maxY); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v7", maxX-10, maxY-10, maxX, maxY, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v8", maxX/2-5, maxY-10, maxX/2+5, maxY); err != nil && - err != gocui.ErrUnknownView { + if _, err := g.SetView("v8", maxX/2-5, maxY-10, maxX/2+5, maxY, 0); err != nil && + !gocui.IsUnknownView(err) { return err } - if _, err := g.SetView("v9", maxX/2-5, maxY/2-5, maxX/2+5, maxY/2+5); err != nil && - err != gocui.ErrUnknownView { - return err + if _, err := g.SetView("v9", maxX/2-5, maxY/2-5, maxX/2+5, maxY/2+5, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + if _, err := g.SetCurrentView("v9"); err != nil { + return err + } } return nil } @@ -56,7 +60,7 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -68,7 +72,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/size.go b/_examples/size.go index 6fb4f911..baeec0a5 100644 --- a/_examples/size.go +++ b/_examples/size.go @@ -8,11 +8,11 @@ import ( "fmt" "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -24,16 +24,21 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - v, err := g.SetView("size", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2) - if err != nil && err != gocui.ErrUnknownView { - return err + v, err := g.SetView("size", maxX/2-7, maxY/2, maxX/2+7, maxY/2+2, 0) + if err != nil { + if !gocui.IsUnknownView(err) { + return err + } + if _, err := g.SetCurrentView("size"); err != nil { + return err + } } v.Clear() fmt.Fprintf(v, "%d, %d", maxX, maxY) diff --git a/_examples/stdin.go b/_examples/stdin.go index 03670ab7..c0236e1c 100644 --- a/_examples/stdin.go +++ b/_examples/stdin.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// This example doesn't work when running `go run stdin.go`, you are suposed to pipe someting to this like: `/bin/ls | go run stdin.go` + package main import ( @@ -11,11 +13,11 @@ import ( "log" "os" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Fatalln(err) } @@ -29,7 +31,7 @@ func main() { log.Fatalln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Fatalln(err) } } @@ -37,8 +39,8 @@ func main() { func layout(g *gocui.Gui) error { maxX, _ := g.Size() - if v, err := g.SetView("help", maxX-23, 0, maxX-1, 5); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("help", maxX-23, 0, maxX-1, 5, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } fmt.Fprintln(v, "KEYBINDINGS") @@ -47,18 +49,19 @@ func layout(g *gocui.Gui) error { fmt.Fprintln(v, "^C: Exit") } - if v, err := g.SetView("stdin", 0, 0, 80, 35); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("stdin", 0, 0, 80, 35, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } - if _, err := g.SetCurrentView("stdin"); err != nil { + v.Wrap = true + + if _, err := io.Copy(hex.Dumper(v), os.Stdin); err != nil { return err } - dumper := hex.Dumper(v) - if _, err := io.Copy(dumper, os.Stdin); err != nil { + + if _, err := g.SetCurrentView("stdin"); err != nil { return err } - v.Wrap = true } return nil diff --git a/_examples/table.go b/_examples/table.go new file mode 100644 index 00000000..95687366 --- /dev/null +++ b/_examples/table.go @@ -0,0 +1,98 @@ +// Copyright 2017 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 main + +import ( + "log" + + "github.com/awesome-gocui/gocui" +) + +type Column struct { + Title string + Size float32 +} + +type Table struct { + name string + Left, Top int + Right, Bottom int + Columns []Column + Data [][]string +} + +func NewTable(name string, left, top, right, bottom int) *Table { + return &Table{ + name: name, + Left: left, + Top: top, + Right: right, + Bottom: bottom, + } +} + +func (t *Table) Layout(g *gocui.Gui) error { + view, err := g.SetView(t.name, t.Left, t.Top, t.Right, t.Bottom, 0) + if err != nil && !gocui.IsUnknownView(err) { + return err + } + + width, height := view.Size() + hOffset := 0 + for cid, column := range t.Columns { + size := int(float32(width) * column.Size) + + view.SetWritePos(hOffset, 0) + view.WriteString(column.Title) + + for rid := 0; rid < height; rid++ { + if rid < len(t.Data[cid]) { + view.SetWritePos(hOffset, rid+1) + view.WriteString(t.Data[cid][rid]) + } + view.SetWritePos(hOffset+size-3, rid) + view.WriteRunes([]rune{'│'}) + } + + hOffset += size + } + + return nil +} + +func main() { + g, err := gocui.NewGui(gocui.OutputNormal, false) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + table := NewTable("t", 1, 2, 80, 10) + table.Columns = []Column{ + {"Column1", 0.25}, + {"Column2", 0.25}, + {"Column3", 0.25}, + {"Column4", 0.25}, + } + table.Data = [][]string{ + {"00", "01", "02", "03"}, + {"10", "11", "12", "13"}, + {"20", "21", "22", "23"}, + {"30", "31", "32", "33"}, + } + g.SetManager(table) + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} diff --git a/_examples/title.go b/_examples/title.go index 2be56895..3080b010 100644 --- a/_examples/title.go +++ b/_examples/title.go @@ -7,11 +7,11 @@ package main import ( "log" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -23,7 +23,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } @@ -36,126 +36,130 @@ func layout(g *gocui.Gui) error { maxX, maxY := g.Size() // Overlap (front) - if v, err := g.SetView("v1", 10, 2, 30, 6); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v1", 10, 2, 30, 6, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v2", 20, 4, 40, 8); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v2", 20, 4, 40, 8, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } // Overlap (back) - if v, err := g.SetView("v3", 60, 4, 80, 8); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v3", 60, 4, 80, 8, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v4", 50, 2, 70, 6); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v4", 50, 2, 70, 6, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } // Overlap (frame) - if v, err := g.SetView("v15", 90, 2, 110, 5); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v15", 90, 2, 110, 5, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v16", 100, 5, 120, 8); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v16", 100, 5, 120, 8, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v17", 140, 5, 160, 8); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v17", 140, 5, 160, 8, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v18", 130, 2, 150, 5); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v18", 130, 2, 150, 5, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } // Long title - if v, err := g.SetView("v5", 10, 12, 30, 16); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v5", 10, 12, 30, 16, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Long long long long title" } // No title - if v, err := g.SetView("v6", 35, 12, 55, 16); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v6", 35, 12, 55, 16, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "" } - if _, err := g.SetView("v7", 60, 12, 80, 16); err != nil { - if err != gocui.ErrUnknownView { + if _, err := g.SetView("v7", 60, 12, 80, 16, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } } // Small view - if v, err := g.SetView("v8", 85, 12, 88, 16); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v8", 85, 12, 88, 16, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } // Screen borders - if v, err := g.SetView("v9", -10, 20, 10, 24); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v9", -10, 20, 10, 24, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v10", maxX-10, 20, maxX+10, 24); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v10", maxX-10, 20, maxX+10, 24, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } // Out of screen - if v, err := g.SetView("v11", -21, 28, -1, 32); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v11", -21, 28, -1, 32, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v12", maxX, 28, maxX+20, 32); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v12", maxX, 28, maxX+20, 32, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v13", 10, -7, 30, -1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v13", 10, -7, 30, -1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" } - if v, err := g.SetView("v14", 10, maxY, 30, maxY+6); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("v14", 10, maxY, 30, maxY+6, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Title = "Regular title" + + if _, err := g.SetCurrentView("v14"); err != nil { + return err + } } return nil diff --git a/_examples/widgets.go b/_examples/widgets.go index 8cfad4b2..331bfc28 100644 --- a/_examples/widgets.go +++ b/_examples/widgets.go @@ -5,12 +5,13 @@ package main import ( - "errors" "fmt" "log" "strings" - "github.com/jroimartin/gocui" + "github.com/go-errors/errors" + + "github.com/awesome-gocui/gocui" ) const delta = 0.2 @@ -38,9 +39,9 @@ func NewHelpWidget(name string, x, y int, body string) *HelpWidget { } func (w *HelpWidget) Layout(g *gocui.Gui) error { - v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h) + v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h, 0) if err != nil { - if err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { return err } fmt.Fprint(v, w.body) @@ -72,8 +73,8 @@ func (w *StatusbarWidget) Val() float64 { } func (w *StatusbarWidget) Layout(g *gocui.Gui) error { - v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+2) - if err != nil && err != gocui.ErrUnknownView { + v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+2, 0) + if err != nil && !gocui.IsUnknownView(err) { return err } v.Clear() @@ -96,9 +97,9 @@ func NewButtonWidget(name string, x, y int, label string, handler func(g *gocui. } func (w *ButtonWidget) Layout(g *gocui.Gui) error { - v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+2) + v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+2, 0) if err != nil { - if err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { return err } if _, err := g.SetCurrentView(w.name); err != nil { @@ -113,7 +114,7 @@ func (w *ButtonWidget) Layout(g *gocui.Gui) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -135,7 +136,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/_examples/wrap.go b/_examples/wrap.go index 04dd5053..1f86e663 100644 --- a/_examples/wrap.go +++ b/_examples/wrap.go @@ -9,13 +9,13 @@ import ( "log" "strings" - "github.com/jroimartin/gocui" + "github.com/awesome-gocui/gocui" ) func layout(g *gocui.Gui) error { maxX, maxY := g.Size() - if v, err := g.SetView("main", 1, 1, maxX-1, maxY-1); err != nil { - if err != gocui.ErrUnknownView { + if v, err := g.SetView("main", 1, 1, maxX-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { return err } v.Wrap = true @@ -23,6 +23,10 @@ func layout(g *gocui.Gui) error { line := strings.Repeat("This is a long line -- ", 10) fmt.Fprintf(v, "%s\n\n", line) fmt.Fprintln(v, "Short") + + if _, err := g.SetCurrentView("main"); err != nil { + return err + } } return nil } @@ -32,7 +36,7 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func main() { - g, err := gocui.NewGui(gocui.OutputNormal) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -44,7 +48,7 @@ func main() { log.Panicln(err) } - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { log.Panicln(err) } } diff --git a/attribute.go b/attribute.go index bad758a1..3d986a71 100644 --- a/attribute.go +++ b/attribute.go @@ -4,7 +4,7 @@ package gocui -import "github.com/nsf/termbox-go" +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 diff --git a/doc.go b/doc.go index fe128afb..ca7113fa 100644 --- a/doc.go +++ b/doc.go @@ -16,7 +16,7 @@ Create a new GUI: // Set GUI managers and key bindings // ... - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { // handle error } @@ -38,7 +38,7 @@ 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 err != gocui.ErrUnknownView { + if !gocui.IsUnknownView(err) { // handle error } fmt.Fprintln(v, "This is a new view") @@ -84,7 +84,7 @@ use *Gui.Update(). For example: return nil }) -By default, gocui provides a basic edition mode. This mode can be extended +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 { diff --git a/edit.go b/edit.go index e1b19c20..b5630df3 100644 --- a/edit.go +++ b/edit.go @@ -4,7 +4,11 @@ package gocui -import "errors" +import ( + "github.com/go-errors/errors" + + "github.com/mattn/go-runewidth" +) const maxInt = int(^uint(0) >> 1) @@ -49,13 +53,64 @@ func simpleEditor(v *View, key Key, ch rune, mod Modifier) { 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(1, 0, true) + 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 @@ -89,12 +144,12 @@ func (v *View) EditDelete(back bool) { v.MoveCursor(-1, 0, true) } } else { // wrapped line - v.deleteRune(len(v.viewLines[y-1].line)-1, v.cy-1) - v.MoveCursor(-1, 0, true) + n, _ := v.deleteRune(len(v.viewLines[y-1].line)-1, v.cy-1) + v.MoveCursor(-n, 0, true) } } else { // middle/end of the line - v.deleteRune(v.cx-1, v.cy) - v.MoveCursor(-1, 0, true) + 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 @@ -109,42 +164,81 @@ func (v *View) EditDelete(back bool) { func (v *View) EditNewLine() { v.breakLine(v.cx, v.cy) v.ox = 0 + v.cy = v.cy + 1 v.cx = 0 - v.MoveCursor(0, 1, true) } // 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 - if writeMode { - if v.Wrap { - curLineWidth = maxX - 1 - } else { - curLineWidth = maxInt - } - } else { + curLineWidth = maxInt + if v.Wrap { + curLineWidth = maxX - 1 + } + + if !writeMode { + curLineWidth = 0 if y >= 0 && y < len(v.viewLines) { - curLineWidth = len(v.viewLines[y].line) + curLineWidth = lineWidth(v.viewLines[y].line) if v.Wrap && curLineWidth >= maxX { curLineWidth = maxX - 1 } - } else { - curLineWidth = 0 } } // get the width of the previous line + prevLineWidth = 0 if y-1 >= 0 && y-1 < len(v.viewLines) { - prevLineWidth = len(v.viewLines[y-1].line) - } else { - prevLineWidth = 0 + 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 @@ -190,10 +284,9 @@ func (v *View) MoveCursor(dx, dy int, writeMode bool) { if !v.Wrap { // set origin so the EOL is visible nox := prevLineWidth - maxX + 1 if nox < 0 { - v.ox = 0 - } else { - v.ox = nox + nox = 0 } + v.ox = nox } v.cx = prevLineWidth } else { @@ -275,19 +368,31 @@ func (v *View) writeRune(x, y int, ch rune) error { // deleteRune removes a rune from the view's internal buffer, at the // position corresponding to the point (x, y). -func (v *View) deleteRune(x, y int) error { +// 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 err + return 0, err } if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) { - return errors.New("invalid point") + return 0, errors.New("invalid point") } - v.lines[y] = append(v.lines[y][:x], v.lines[y][x+1:]...) - return nil + + 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. diff --git a/escape.go b/escape.go index ec31bbe0..c88309b0 100644 --- a/escape.go +++ b/escape.go @@ -5,7 +5,7 @@ package gocui import ( - "errors" + "github.com/go-errors/errors" "strconv" ) diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..5791b4e4 --- /dev/null +++ b/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/go.sum b/go.sum new file mode 100644 index 00000000..25f1c037 --- /dev/null +++ b/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/gui.go b/gui.go index 9499d3c3..6fe0d5d8 100644 --- a/gui.go +++ b/gui.go @@ -5,21 +5,36 @@ package gocui import ( - "errors" + standardErrors "errors" + "runtime" - "github.com/nsf/termbox-go" + "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 ( - // ErrQuit is used to decide if the MainLoop finished successfully. - ErrQuit = errors.New("quit") + // 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 = errors.New("unknown view") -) + ErrUnknownView = standardErrors.New("unknown view") -// OutputMode represents the terminal's output mode (8 or 256 colors). -type OutputMode termbox.OutputMode + // ErrQuit is used to decide if the MainLoop finished successfully. + ErrQuit = standardErrors.New("quit") +) const ( // OutputNormal provides 8-colors terminal mode. @@ -27,6 +42,12 @@ const ( // 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 @@ -40,14 +61,16 @@ type Gui struct { 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 Attribute + BgColor, FgColor, FrameColor Attribute // SelBgColor and SelFgColor allow to configure the background and // foreground colors of the frame of the current view. - SelBgColor, SelFgColor Attribute + SelBgColor, SelFgColor, SelFrameColor Attribute // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the // frame of the current view. @@ -66,11 +89,16 @@ type Gui struct { // 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) (*Gui, error) { - if err := termbox.Init(); err != nil { +func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { + err := termbox.Init() + if err != nil { return nil, err } @@ -79,20 +107,36 @@ func NewGui(mode OutputMode) (*Gui, error) { 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) - g.maxX, g.maxY = termbox.Size() + 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() } @@ -127,8 +171,8 @@ func (g *Gui) Rune(x, y int) (rune, error) { // 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) (*View, error) { - if x0 >= x1 || y0 >= y1 { +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 == "" { @@ -147,8 +191,20 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) { 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, ErrUnknownView + 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. @@ -160,7 +216,7 @@ func (g *Gui) SetViewOnTop(name string) (*View, error) { return v, nil } } - return nil, ErrUnknownView + return nil, errors.Wrap(ErrUnknownView, 0) } // SetViewOnBottom sets the given view on bottom of the existing ones. @@ -172,7 +228,7 @@ func (g *Gui) SetViewOnBottom(name string) (*View, error) { return v, nil } } - return nil, ErrUnknownView + return nil, errors.Wrap(ErrUnknownView, 0) } // Views returns all the views in the GUI. @@ -188,7 +244,7 @@ func (g *Gui) View(name string) (*View, error) { return v, nil } } - return nil, ErrUnknownView + return nil, errors.Wrap(ErrUnknownView, 0) } // ViewByPosition returns a pointer to a view matching the given position, or @@ -201,7 +257,7 @@ func (g *Gui) ViewByPosition(x, y int) (*View, error) { return v, nil } } - return nil, ErrUnknownView + return nil, errors.Wrap(ErrUnknownView, 0) } // ViewPosition returns the coordinates of the view with the given name, or @@ -212,7 +268,7 @@ func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) { return v.x0, v.y0, v.x1, v.y1, nil } } - return 0, 0, 0, 0, ErrUnknownView + return 0, 0, 0, 0, errors.Wrap(ErrUnknownView, 0) } // DeleteView deletes a view by name. @@ -223,7 +279,7 @@ func (g *Gui) DeleteView(name string) error { return nil } } - return ErrUnknownView + return errors.Wrap(ErrUnknownView, 0) } // SetCurrentView gives the focus to a given view. @@ -234,7 +290,7 @@ func (g *Gui) SetCurrentView(name string) (*View, error) { return v, nil } } - return nil, ErrUnknownView + return nil, errors.Wrap(ErrUnknownView, 0) } // CurrentView returns the currently focused view, or nil if no view @@ -253,6 +309,11 @@ func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, hand 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 @@ -285,6 +346,28 @@ func (g *Gui) DeleteKeybindings(viewname string) { 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) { @@ -349,14 +432,24 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) { // 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 { - g.tbEvents <- termbox.PollEvent() + select { + case <-g.stop: + return + default: + g.tbEvents <- termbox.PollEvent() + } } }() inputMode := termbox.InputAlt - if g.InputEsc { + if true { // previously g.InputEsc, but didn't seem to work inputMode = termbox.InputEsc } if g.Mouse { @@ -437,20 +530,25 @@ func (g *Gui) flush() error { } } for _, v := range g.views { + if !v.Visible || v.y1 < v.y0 { + continue + } if v.Frame { - var fgColor, bgColor Attribute + 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, fgColor, bgColor); err != nil { + if err := g.drawFrameEdges(v, frameColor, bgColor); err != nil { return err } - if err := g.drawFrameCorners(v, fgColor, bgColor); err != nil { + if err := g.drawFrameCorners(v, frameColor, bgColor); err != nil { return err } if v.Title != "" { @@ -458,6 +556,11 @@ func (g *Gui) flush() error { 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 @@ -507,9 +610,36 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error { 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 = '+', '+', '+', '+' } @@ -549,6 +679,28 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error { 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 { @@ -620,17 +772,61 @@ func (g *Gui) onKey(ev *termbox.Event) error { // 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) { - matched = false + var globalKb *keybinding + for _, kb := range g.keybindings { if kb.handler == nil { continue } - if kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) && kb.matchView(v) { - if err := kb.handler(g, v); err != nil { - return false, err - } - matched = true + + 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 matched, nil + 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/gui_others.go b/gui_others.go new file mode 100644 index 00000000..5d247a19 --- /dev/null +++ b/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/gui_windows.go b/gui_windows.go new file mode 100644 index 00000000..db1faab7 --- /dev/null +++ b/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/keybinding.go b/keybinding.go index 03fe677c..d294e70d 100644 --- a/keybinding.go +++ b/keybinding.go @@ -4,7 +4,18 @@ package gocui -import "github.com/nsf/termbox-go" +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 { @@ -15,6 +26,71 @@ type keybinding struct { 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{ @@ -34,14 +110,90 @@ func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool { // matchView returns if the keybinding matches the current view. func (kb *keybinding) matchView(v *View) bool { - if kb.viewName == "" { - return true + // if the user is typing in a field, ignore char keys + if v == nil || (v.Editable && kb.ch != 0) { + return false } - return v != nil && kb.viewName == v.name + return kb.viewName == v.name } -// Key represents special keys or keys combinations. -type Key termbox.Key +// 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 ( @@ -126,10 +278,6 @@ const ( KeyCtrl8 = Key(termbox.KeyCtrl8) ) -// 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 - // Modifiers. const ( ModNone Modifier = Modifier(0) diff --git a/loader.go b/loader.go new file mode 100644 index 00000000..d6715ac6 --- /dev/null +++ b/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/view.go b/view.go index 42082f8c..81f90603 100644 --- a/view.go +++ b/view.go @@ -6,28 +6,59 @@ package gocui import ( "bytes" - "errors" "io" "strings" + "sync" + "unicode/utf8" - "github.com/nsf/termbox-go" + "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 - ox, oy int - cx, cy int - lines [][]cell - readOffset int - readCache 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 - tainted bool // marks if the viewBuffer must be updated - viewLines []viewLine // internal representation of the view's buffer + // ei is used to decode ESC sequences on Write + ei *escapeInterpreter - ei *escapeInterpreter // used to decode ESC sequences on Write + // Visible specifies whether the view is visible. + Visible bool // BgColor and FgColor allow to configure the background and foreground // colors of the View. @@ -41,7 +72,7 @@ type View struct { // buffer at the cursor position. Editable bool - // Editor allows to define the editor that manages the edition mode, + // Editor allows to define the editor that manages the editing mode, // including keybindings or cursor behaviour. DefaultEditor is used by // default. Editor Editor @@ -68,9 +99,18 @@ type View struct { // 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 { @@ -102,6 +142,7 @@ func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { y0: y0, x1: x1, y1: y1, + Visible: true, Frame: true, Editor: DefaultEditor, tainted: true, @@ -110,6 +151,11 @@ func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { 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 @@ -126,9 +172,8 @@ func (v *View) Name() string { 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 errors.New("invalid point") + return ErrInvalidPoint } - var ( ry, rcy int err error @@ -149,8 +194,12 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { bgColor = v.BgColor ch = v.Mask } else if v.Highlight && ry == rcy { - fgColor = v.SelFgColor - bgColor = v.SelBgColor + fgColor = fgColor | AttrBold + } + + // Don't display NUL characters + if ch == 0 { + ch = ' ' } termbox.SetCell(v.x0+x+1, v.y0+y+1, ch, @@ -164,7 +213,7 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { func (v *View) SetCursor(x, y int) error { maxX, maxY := v.Size() if x < 0 || x >= maxX || y < 0 || y >= maxY { - return errors.New("invalid point") + return ErrInvalidPoint } v.cx = x v.cy = y @@ -183,7 +232,7 @@ func (v *View) Cursor() (x, y int) { // or decrementing ox and oy. func (v *View) SetOrigin(x, y int) error { if x < 0 || y < 0 { - return errors.New("invalid point") + return ErrInvalidPoint } v.ox = x v.oy = y @@ -195,39 +244,141 @@ 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)) +} - for _, ch := range bytes.Runes(p) { - switch ch { +// 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.lines = append(v.lines, nil) - case '\r': - nl := len(v.lines) - if nl > 0 { - v.lines[nl-1] = nil - } else { - v.lines = make([][]cell, 1) + 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(ch) + cells := v.parseInput(r) if cells == nil { continue } - - nl := len(v.lines) - if nl > 0 { - v.lines[nl-1] = append(v.lines[nl-1], cells...) - } else { - v.lines = append(v.lines, cells) - } + v.writeCells(v.wx, v.wy, cells) + v.wx += len(cells) } } - return len(p), nil } // parseInput parses char by char the input written to the View. It returns nil @@ -251,41 +402,85 @@ func (v *View) parseInput(ch rune) []cell { if isEscape { return nil } - c := cell{ - fgColor: v.ei.curFgColor, - bgColor: v.ei.curBgColor, - chr: ch, + 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) } - cells = append(cells, c) } return cells } -// Read reads data into p. It returns the number of bytes read into p. -// At EOF, err will be io.EOF. Calling Read() after Rewind() makes the -// cache to be refreshed with the contents of the view. +// 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) { - if v.readOffset == 0 { - v.readCache = v.Buffer() + 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 } - if v.readOffset < len(v.readCache) { - n = copy(p, v.readCache[v.readOffset:]) - v.readOffset += n - } else { - err = io.EOF + 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 + return offset, io.EOF } -// Rewind sets the offset for the next Read to 0, which also refresh the -// read cache. +// Rewind sets read and write pos to (0, 0). func (v *View) Rewind() { - v.readOffset = 0 + 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 { @@ -296,29 +491,25 @@ func (v *View) draw() error { } if v.tainted { v.viewLines = nil - for i, line := range v.lines { + lines := v.lines + if v.HasLoader { + lines = v.loaderLines() + } + for i, line := range lines { + wrap := 0 if v.Wrap { - if len(line) < maxX { - vline := viewLine{linesX: 0, linesY: i, line: line} - v.viewLines = append(v.viewLines, vline) - continue - } else { - for n := 0; n <= len(line); n += maxX { - if len(line[n:]) <= maxX { - vline := viewLine{linesX: n, linesY: i, line: line[n:]} - v.viewLines = append(v.viewLines, vline) - } else { - vline := viewLine{linesX: n, linesY: i, line: line[n : n+maxX]} - v.viewLines = append(v.viewLines, vline) - } - } - } - } else { - vline := viewLine{linesX: 0, linesY: i, line: line} + 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) } } - v.tainted = false + if !v.HasLoader { + v.tainted = false + } } if v.Autoscroll && len(v.viewLines) > maxY { @@ -353,7 +544,15 @@ func (v *View) draw() error { if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil { return err } - x++ + + 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++ } @@ -367,7 +566,7 @@ func (v *View) realPosition(vx, vy int) (x, y int, err error) { vy = v.oy + vy if vx < 0 || vy < 0 { - return 0, 0, errors.New("invalid point") + return 0, 0, ErrInvalidPoint } if len(v.viewLines) == 0 { @@ -388,13 +587,16 @@ func (v *View) realPosition(vx, vy int) (x, y int, err error) { } // 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.readOffset = 0 v.clearRunes() + v.writeMutex.Unlock() } // clearRunes erases all the cells in the view. @@ -423,11 +625,7 @@ func (v *View) BufferLines() []string { // Buffer returns a string with the contents of the view's internal // buffer. func (v *View) Buffer() string { - str := "" - for _, l := range v.lines { - str += lineType(l).String() + "\n" - } - return strings.Replace(str, "\x00", " ", -1) + return linesToString(v.lines) } // ViewBufferLines returns the lines in the view's internal @@ -442,14 +640,25 @@ func (v *View) ViewBufferLines() []string { 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 { - str := "" - for _, l := range v.viewLines { - str += lineType(l.line).String() + "\n" + lines := make([][]cell, len(v.viewLines)) + for i := range v.viewLines { + lines[i] = v.viewLines[i].line } - return strings.Replace(str, "\x00", " ", -1) + + return linesToString(lines) } // Line returns a string with the line of the view's internal buffer @@ -461,7 +670,7 @@ func (v *View) Line(y int) (string, error) { } if y < 0 || y >= len(v.lines) { - return "", errors.New("invalid point") + return "", ErrInvalidPoint } return lineType(v.lines[y]).String(), nil @@ -476,7 +685,7 @@ func (v *View) Word(x, y int) (string, error) { } if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) { - return "", errors.New("invalid point") + return "", ErrInvalidPoint } str := lineType(v.lines[y]).String() @@ -501,3 +710,91 @@ func (v *View) Word(x, y int) (string, error) { 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") +}