1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-05 00:59:19 +02:00

use tcell via porting over code from awesome-gocui

This commit is contained in:
Jesse Duffield
2020-12-24 14:45:59 +11:00
parent 8901d11674
commit 6a6024e38f
309 changed files with 28156 additions and 5896 deletions

56
vendor/github.com/jesseduffield/gocui/CHANGES_tcell.md generated vendored Normal file
View File

@ -0,0 +1,56 @@
# Change from termbox to tcell
Original GOCUI was written on top of [termbox](https://github.com/nsf/termbox-go) package. This document describes changes which were done to be able to use to [tcell/v2](https://github.com/gdamore/tcell) package.
## Attribute color
Attribute type represents a terminal attribute like color and font effects. Color and font effects can be combined using bitwise OR (`|`).
In `termbox` colors were represented by range 1 to 256. `0` was default color which uses the terminal default setting.
In `tcell` colors can be represented in 24bit, and all of them starts from 0. Valid colors have special flag which gives them real value starting from 4294967296. `0` is a default similart to `termbox`.
The change to support all these colors was made in a way, that original colors from 1 to 256 are backward compatible and if user has color specified as
`Attribute(ansicolor+1)` without the valid color flag, it will be translated to `tcell` color by subtracting 1 and making the color valid by adding the flag. This should ensure backward compatibility.
All the color constants are the same with different underlying values. From user perspective, this should be fine unless some arithmetic is done with it. For example `ColorBlack` was `1` in original version but is `4294967296` in new version.
GOCUI provides a few helper functions which could be used to get the real color value or to create a color attribute.
- `(a Attribute).Hex()` - returns `int32` value of the color represented as `Red << 16 | Green << 8 | Blue`
- `(a Attribute).RGB()` - returns 3 `int32` values for red, green and blue color.
- `GetColor(string)` - creates `Attribute` from color passed as a string. This can be hex value or color name (W3C name).
- `Get256Color(int32)` - creates `Attribute` from color number (ANSI colors).
- `GetRGBColor(int32)` - creates `Attribute` from color number created the same way as `Hex()` function returns.
- `NewRGBColor(int32, int32, int32)` - creates `Attribute` from color numbers for red, green and blue values.
## Attribute font effect
There were 3 attributes for font effect, `AttrBold`, `AttrUnderline` and `AttrReverse`.
`tcell` supports more attributes, so they were added. All of these attributes have different values from before. However they can be used in the same way as before.
All the font effect attributes:
- `AttrBold`
- `AttrBlink`
- `AttrReverse`
- `AttrUnderline`
- `AttrDim`
- `AttrItalic`
- `AttrStrikeThrough`
## OutputMode
`OutputMode` in `termbox` was used to translate colors into the correct range. So for example in `OutputGrayscale` you had colors from 1 - 24 all representing gray colors in range 232 - 255, and white and black color.
`tcell` colors are 24bit and they are translated by the library into the color which can be read by terminal.
The original translation from `termbox` was included in GOCUI to be backward compatible. This is enabled in all the original modes: `OutputNormal`, `Output216`, `OutputGrayscale` and `Output256`.
`OutputTrue` is a new mode. It is recomended, because in this mode GOCUI doesn't do any kind of translation of the colors and pass them directly to `tcell`. If user wants to use true color in terminal and this mode doesn't work, it might be because of the terminal setup. `tcell` has a documentation what needs to be done, but in short `COLORTERM=truecolor` environment variable should help (see [_examples/colorstrue.go](./_examples/colorstrue.go)). Other way would be to have `TERM` environment variable having value with suffix `-truecolor`. To disable true color set `TCELL_TRUECOLOR=disable`.
## Keybinding
`termbox` had different way of handling input from terminal than `tcell`. This leads to some adjustement on how the keys are represented.
In general, all the keys in GOCUI should be presented from before, but the underlying values might be different. This could lead to some problems if a user uses different parser to create the `Key` for the keybinding. If using GOCUI parser, everything should be ok.
Mouse is handled differently in `tcell`, but translation was done to keep it in the same way as it was before. However this was harder to test due to different behaviour across the platforms, so if anything is missing or not working, please report.

View File

@ -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

33
vendor/github.com/jesseduffield/gocui/CONTRIBUTING.md generated vendored Normal file
View File

@ -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!

View File

@ -1,8 +1,13 @@
# GOCUI - Go Console User Interface
[![GoDoc](https://godoc.org/github.com/jroimartin/gocui?status.svg)](https://godoc.org/github.com/jroimartin/gocui)
[![CircleCI](https://circleci.com/gh/awesome-gocui/gocui/tree/master.svg?style=svg)](https://circleci.com/gh/awesome-gocui/gocui/tree/master)
[![CodeCov](https://codecov.io/gh/awesome-gocui/gocui/branch/master/graph/badge.svg)](https://codecov.io/gh/awesome-gocui/gocui)
[![Go Report Card](https://goreportcard.com/badge/github.com/awesome-gocui/gocui)](https://goreportcard.com/report/github.com/awesome-gocui/gocui)
[![GolangCI](https://golangci.com/badges/github.com/awesome-gocui/gocui.svg)](https://golangci.com/badges/github.com/awesome-gocui/gocui.svg)
[![GoDoc](https://godoc.org/github.com/awesome-gocui/gocui?status.svg)](https://godoc.org/github.com/awesome-gocui/gocui)
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/awesome-gocui/gocui.svg)
Minimalist Go package aimed at creating Console User Interfaces.
A community fork based on the amazing work of [jroimartin](https://github.com/jroimartin/gocui)
## Features
@ -13,15 +18,30 @@ 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).
* Written ontop of TCell
* 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 +49,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 +65,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)
}
@ -60,19 +81,25 @@ 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
}
if _, err := g.SetCurrentView("hello"); err != nil {
return err
}
fmt.Fprintln(v, "Hello world!")
}
return nil
}
@ -106,5 +133,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! :)

View File

@ -1,32 +1,165 @@
// Copyright 2014 The gocui Authors. All rights reserved.
// Copyright 2020 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocui
import "github.com/jesseduffield/termbox-go"
import "github.com/gdamore/tcell/v2"
// Attribute represents a terminal attribute, like color, font style, etc. They
// can be combined using bitwise OR (|). Note that it is not possible to
// combine multiple color attributes.
type Attribute termbox.Attribute
// Attribute affects the presentation of characters, such as color, boldness, etc.
type Attribute uint64
// Color attributes.
const (
ColorDefault Attribute = Attribute(termbox.ColorDefault)
ColorBlack = Attribute(termbox.ColorBlack)
ColorRed = Attribute(termbox.ColorRed)
ColorGreen = Attribute(termbox.ColorGreen)
ColorYellow = Attribute(termbox.ColorYellow)
ColorBlue = Attribute(termbox.ColorBlue)
ColorMagenta = Attribute(termbox.ColorMagenta)
ColorCyan = Attribute(termbox.ColorCyan)
ColorWhite = Attribute(termbox.ColorWhite)
// ColorDefault is used to leave the Color unchanged from whatever system or teminal default may exist.
ColorDefault = Attribute(tcell.ColorDefault)
// AttrIsValidColor is used to indicate the color value is actually
// valid (initialized). This is useful to permit the zero value
// to be treated as the default.
AttrIsValidColor = Attribute(tcell.ColorValid)
// AttrIsRGBColor is used to indicate that the Attribute value is RGB value of color.
// The lower order 3 bytes are RGB.
// (It's not a color in basic ANSI range 256).
AttrIsRGBColor = Attribute(tcell.ColorIsRGB)
// AttrColorBits is a mask where color is located in Attribute
AttrColorBits = 0xffffffffff // roughly 5 bytes, tcell uses 4 bytes and half-byte as a special flags for color (rest is reserved for future)
// AttrStyleBits is a mask where character attributes (e.g.: bold, italic, underline) are located in Attribute
AttrStyleBits = 0xffffff0000000000 // remaining 3 bytes in the 8 bytes Attribute (tcell is not using it, so we should be fine)
)
// Text style attributes.
// Color attributes. These colors are compatible with tcell.Color type and can be expanded like:
// g.FgColor := gocui.Attribute(tcell.ColorLime)
const (
AttrBold Attribute = Attribute(termbox.AttrBold)
AttrUnderline = Attribute(termbox.AttrUnderline)
AttrReverse = Attribute(termbox.AttrReverse)
ColorBlack Attribute = AttrIsValidColor + iota
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)
// grayscale indexes (for backward compatibility with termbox-go original grayscale)
var grayscale = []tcell.Color{
16, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 231,
}
// Attributes are not colors, but effects (e.g.: bold, dim) which affect the display of text.
// They can be combined.
const (
AttrBold Attribute = 1 << (40 + iota)
AttrBlink
AttrReverse
AttrUnderline
AttrDim
AttrItalic
AttrStrikeThrough
AttrNone Attribute = 0 // Just normal text.
)
// AttrAll represents all the text effect attributes turned on
const AttrAll = AttrBold | AttrBlink | AttrReverse | AttrUnderline | AttrDim | AttrItalic
// IsValidColor indicates if the Attribute is a valid color value (has been set).
func (a Attribute) IsValidColor() bool {
return a&AttrIsValidColor != 0
}
// Hex returns the color's hexadecimal RGB 24-bit value with each component
// consisting of a single byte, ala R << 16 | G << 8 | B. If the color
// is unknown or unset, -1 is returned.
//
// This function produce the same output as `tcell.Hex()` with additional
// support for `termbox-go` colors (to 256).
func (a Attribute) Hex() int32 {
if !a.IsValidColor() {
return -1
}
tc := getTcellColor(a, OutputTrue)
return tc.Hex()
}
// RGB returns the red, green, and blue components of the color, with
// each component represented as a value 0-255. If the color
// is unknown or unset, -1 is returned for each component.
//
// This function produce the same output as `tcell.RGB()` with additional
// support for `termbox-go` colors (to 256).
func (a Attribute) RGB() (int32, int32, int32) {
v := a.Hex()
if v < 0 {
return -1, -1, -1
}
return (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff
}
// GetColor creates a Color from a color name (W3C name). A hex value may
// be supplied as a string in the format "#ffffff".
func GetColor(color string) Attribute {
return Attribute(tcell.GetColor(color))
}
// Get256Color creates Attribute which stores ANSI color (0-255)
func Get256Color(color int32) Attribute {
return Attribute(color) | AttrIsValidColor
}
// GetRGBColor creates Attribute which stores RGB color.
// Color is passed as 24bit RGB value, where R << 16 | G << 8 | B
func GetRGBColor(color int32) Attribute {
return Attribute(color) | AttrIsValidColor | AttrIsRGBColor
}
// NewRGBColor creates Attribute which stores RGB color.
func NewRGBColor(r, g, b int32) Attribute {
return Attribute(tcell.NewRGBColor(r, g, b))
}
// getTcellColor transform Attribute into tcell.Color
func getTcellColor(c Attribute, omode OutputMode) tcell.Color {
c = c & AttrColorBits
// Default color is 0 in tcell/v2 and was 0 in termbox-go, so we are good here
if c == ColorDefault {
return tcell.ColorDefault
}
tc := tcell.ColorDefault
// Check if we have valid color
if c.IsValidColor() {
tc = tcell.Color(c)
} else if c > 0 && c <= 256 {
// It's not valid color, but it has value in range 1-256
// This is old Attribute style of color from termbox-go (black=1, etc.)
// convert to tcell color (black=0|ColorValid)
tc = tcell.Color(c-1) | tcell.ColorValid
}
switch omode {
case OutputTrue:
return tc
case OutputNormal:
tc &= tcell.Color(0xf) | tcell.ColorValid
case Output256:
tc &= tcell.Color(0xff) | tcell.ColorValid
case Output216:
tc &= tcell.Color(0xff)
if tc > 215 {
return tcell.ColorDefault
}
tc += tcell.Color(16) | tcell.ColorValid
case OutputGrayscale:
tc &= tcell.Color(0x1f)
if tc > 26 {
return tcell.ColorDefault
}
tc = grayscale[tc] | tcell.ColorValid
default:
return tcell.ColorDefault
}
return tc
}

View File

@ -7,7 +7,7 @@ Package gocui allows to create console user interfaces.
Create a new GUI:
g, err := gocui.NewGui(gocui.OutputNormal)
g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil {
// handle error
}
@ -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
}
@ -37,8 +37,8 @@ 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 v, err := g.SetView("viewname", 2, 2, 22, 7, 0); err != nil {
if !gocui.IsUnknownView(err) {
// handle error
}
fmt.Fprintln(v, "This is a new view")
@ -48,7 +48,7 @@ Create and initialize a view with absolute coordinates:
Views can also be created using relative coordinates:
maxX, maxY := g.Size()
if v, err := g.SetView("viewname", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2); err != nil {
if v, err := g.SetView("viewname", maxX/2-30, maxY/2, maxX/2+30, maxY/2+2, 0); err != nil {
// ...
}
@ -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 {

View File

@ -33,10 +33,16 @@ var DefaultEditor Editor = EditorFunc(simpleEditor)
// simpleEditor is used as the default gocui editor.
func simpleEditor(v *View, key Key, ch rune, mod Modifier) {
switch {
case ch != 0 && mod == 0:
v.EditWrite(ch)
case key == KeySpace:
v.EditWrite(' ')
case key == KeyBackspace || key == KeyBackspace2:
v.EditDelete(true)
case key == KeyDelete:
v.EditDelete(false)
case key == KeyInsert:
v.Overwrite = !v.Overwrite
case key == KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == KeyArrowUp:
@ -73,7 +79,7 @@ func (v *View) EditWrite(ch rune) {
v.moveCursor(w, 0, true)
}
// EditDeleteToStartOfLine is the equivalent of pressing ctrl+U in your terminal, it deletes to the end of the line. Or if you are already at the start of the line, it deletes the newline character
// 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 {
@ -350,18 +356,17 @@ func (v *View) writeRune(x, y int, ch rune) error {
}
olen := len(v.lines[y])
w := runewidth.RuneWidth(ch)
var s []cell
if x >= len(v.lines[y]) {
s = make([]cell, x-len(v.lines[y])+w)
s = make([]cell, x-len(v.lines[y])+1)
} else if !v.Overwrite {
s = make([]cell, w)
s = make([]cell, 1)
}
v.lines[y] = append(v.lines[y], s...)
if !v.Overwrite || (v.Overwrite && x >= olen-w) {
copy(v.lines[y][x+w:], v.lines[y][x:])
if !v.Overwrite || (v.Overwrite && x >= olen-1) {
copy(v.lines[y][x+1:], v.lines[y][x:])
}
v.lines[y][x] = cell{
fgColor: v.FgColor,
@ -369,14 +374,6 @@ func (v *View) writeRune(x, y int, ch rune) error {
chr: ch,
}
for i := 1; i < w; i++ {
v.lines[y][x+i] = cell{
fgColor: v.FgColor,
bgColor: v.BgColor,
chr: '\x00',
}
}
return nil
}
@ -403,7 +400,7 @@ func (v *View) deleteRune(x, y int) (int, error) {
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+w:]...)
v.lines[y] = append(v.lines[y][:i], v.lines[y][i+1:]...)
return w, nil
}

View File

@ -24,6 +24,11 @@ const (
ERASE_IN_LINE
)
type (
escapeState int
fontEffect int
)
type instruction struct {
kind int
param1 int
@ -31,13 +36,21 @@ type instruction struct {
toWrite []rune
}
type escapeState int
const (
stateNone escapeState = iota
stateEscape
stateCSI
stateParams
bold fontEffect = 1
faint fontEffect = 2
italic fontEffect = 3
underline fontEffect = 4
blink fontEffect = 5
reverse fontEffect = 7
strike fontEffect = 9
setForegroundColor fontEffect = 38
setBackgroundColor fontEffect = 48
)
var (
@ -144,6 +157,8 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
err = ei.outputNormal()
case Output256:
err = ei.output256()
case OutputTrue:
err = ei.outputTrue()
}
if err != nil {
return false, errCSIParseError
@ -181,22 +196,18 @@ func (ei *escapeInterpreter) outputNormal() error {
switch {
case p >= 30 && p <= 37:
ei.curFgColor |= Attribute(p - 30 + 1)
ei.curFgColor = Get256Color(int32(p) - 30)
case p == 39:
ei.curFgColor |= ColorDefault
ei.curFgColor = ColorDefault
case p >= 40 && p <= 47:
ei.curBgColor |= Attribute(p - 40 + 1)
ei.curBgColor = Get256Color(int32(p) - 40)
case p == 49:
ei.curBgColor |= ColorDefault
case p == 1:
ei.curFgColor |= AttrBold
case p == 4:
ei.curFgColor |= AttrUnderline
case p == 7:
ei.curFgColor |= AttrReverse
ei.curBgColor = ColorDefault
case p == 0:
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
default:
ei.curFgColor |= getFontEffect(p)
}
}
@ -221,39 +232,130 @@ func (ei *escapeInterpreter) output256() error {
return ei.outputNormal()
}
fgbg, err := strconv.Atoi(ei.csiParam[0])
if err != nil {
return errCSIParseError
}
color, err := strconv.Atoi(ei.csiParam[2])
if err != nil {
return errCSIParseError
}
switch fgbg {
case 38:
ei.curFgColor = Attribute(color + 1)
for _, param := range ei.csiParam[3:] {
p, err := strconv.Atoi(param)
if err != nil {
return errCSIParseError
}
switch {
case p == 1:
ei.curFgColor |= AttrBold
case p == 4:
ei.curFgColor |= AttrUnderline
case p == 7:
ei.curFgColor |= AttrReverse
}
for _, param := range splitFgBg(ei.csiParam, 3) {
fgbg, err := strconv.Atoi(param[0])
if err != nil {
return errCSIParseError
}
color, err := strconv.Atoi(param[2])
if err != nil {
return errCSIParseError
}
case 48:
ei.curBgColor = Attribute(color + 1)
default:
return errCSIParseError
}
switch fontEffect(fgbg) {
case setForegroundColor:
ei.curFgColor = Get256Color(int32(color))
for _, s := range param[3:] {
p, err := strconv.Atoi(s)
if err != nil {
return errCSIParseError
}
ei.curFgColor |= getFontEffect(p)
}
case setBackgroundColor:
ei.curBgColor = Get256Color(int32(color))
default:
return errCSIParseError
}
}
return nil
}
// outputTrue allows you to leverage the true-color terminal mode.
//
// Works with rgb ANSI sequence: `\x1b[38;2;<r>;<g>;<b>m`, `\x1b[48;2;<r>;<g>;<b>m`
func (ei *escapeInterpreter) outputTrue() error {
if len(ei.csiParam) < 5 {
return ei.output256()
}
mode, err := strconv.Atoi(ei.csiParam[1])
if err != nil {
return errCSIParseError
}
if mode != 2 {
return ei.output256()
}
for _, param := range splitFgBg(ei.csiParam, 5) {
fgbg, err := strconv.Atoi(param[0])
if err != nil {
return errCSIParseError
}
colr, err := strconv.Atoi(param[2])
if err != nil {
return errCSIParseError
}
colg, err := strconv.Atoi(param[3])
if err != nil {
return errCSIParseError
}
colb, err := strconv.Atoi(param[4])
if err != nil {
return errCSIParseError
}
color := NewRGBColor(int32(colr), int32(colg), int32(colb))
switch fontEffect(fgbg) {
case setForegroundColor:
ei.curFgColor = color
for _, s := range param[5:] {
p, err := strconv.Atoi(s)
if err != nil {
return errCSIParseError
}
ei.curFgColor |= getFontEffect(p)
}
case setBackgroundColor:
ei.curBgColor = color
default:
return errCSIParseError
}
}
return nil
}
// splitFgBg splits foreground and background color according to ANSI sequence.
//
// num (number of segments in ansi) is used to determine if it's 256 mode or rgb mode (3 - 256-color, 5 - rgb-color)
func splitFgBg(params []string, num int) [][]string {
var out [][]string
var current []string
for _, p := range params {
if len(current) == num && (p == "48" || p == "38") {
out = append(out, current)
current = []string{}
}
current = append(current, p)
}
if len(current) > 0 {
out = append(out, current)
}
return out
}
func getFontEffect(f int) Attribute {
switch fontEffect(f) {
case bold:
return AttrBold
case faint:
return AttrDim
case italic:
return AttrItalic
case underline:
return AttrUnderline
case blink:
return AttrBlink
case reverse:
return AttrReverse
case strike:
return AttrStrikeThrough
}
return AttrNone
}

10
vendor/github.com/jesseduffield/gocui/go.mod generated vendored Normal file
View File

@ -0,0 +1,10 @@
module github.com/jesseduffield/gocui
go 1.12
require (
github.com/gdamore/tcell/v2 v2.0.0
github.com/go-errors/errors v1.0.2
github.com/mattn/go-runewidth v0.0.9
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00
)

17
vendor/github.com/jesseduffield/gocui/go.sum generated vendored Normal file
View File

@ -0,0 +1,17 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -7,38 +7,57 @@ package gocui
import (
standardErrors "errors"
"fmt"
"runtime"
"strings"
"sync"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/termbox-go"
)
// OutputMode represents an output mode, which determines how colors
// are used.
type OutputMode int
var (
// ErrQuit is used to decide if the MainLoop finished successfully.
ErrQuit = standardErrors.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 = 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.
OutputNormal = OutputMode(termbox.OutputNormal)
OutputNormal OutputMode = iota
// Output256 provides 256-colors terminal mode.
Output256 = OutputMode(termbox.Output256)
Output256
// OutputGrayScale provides greyscale terminal mode.
OutputGrayScale = OutputMode(termbox.OutputGrayscale)
// Output216 provides 216 ansi color terminal mode.
Output216
// Output216 provides greyscale terminal mode.
Output216 = OutputMode(termbox.Output216)
// OutputGrayscale provides greyscale terminal mode.
OutputGrayscale
// OutputTrue provides 24bit color terminal mode.
// This mode is recommended even if your terminal doesn't support
// such mode. The colors are represented exactly as you
// write them (no clamping or truncating). `tcell` should take care
// of what your terminal can do.
OutputTrue
)
type tabClickHandler func(int) error
@ -59,30 +78,30 @@ type GuiMutexes struct {
// Gui represents the whole User Interface, including the views, layouts
// and keybindings.
type Gui struct {
tbEvents chan termbox.Event
userEvents chan userEvent
// ReplayedEvents is a channel for passing pre-recorded input events, for the purposes of testing
ReplayedEvents chan termbox.Event
ReplayedEvents chan GocuiEvent
RecordEvents bool
RecordedEvents chan *termbox.Event
RecordedEvents chan *GocuiEvent
tabClickBindings []*tabClickBinding
gEvents chan GocuiEvent
userEvents chan userEvent
views []*View
currentView *View
managers []Manager
keybindings []*keybinding
tabClickBindings []*tabClickBinding
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.
@ -117,29 +136,33 @@ type Gui struct {
// NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, error) {
err := tcellInit()
if err != nil {
return nil, err
}
g := &Gui{}
var err error
if g.maxX, g.maxY, err = g.getTermWindowSize(); err != nil {
return nil, err
}
if err := termbox.Init(); err != nil {
return nil, err
}
g.outputMode = mode
termbox.SetOutputMode(termbox.OutputMode(mode))
g.stop = make(chan struct{}, 0)
g.stop = make(chan struct{})
g.tbEvents = make(chan termbox.Event, 20)
g.ReplayedEvents = make(chan termbox.Event)
g.ReplayedEvents = make(chan GocuiEvent)
g.gEvents = make(chan GocuiEvent, 20)
g.userEvents = make(chan userEvent, 20)
g.RecordedEvents = make(chan *termbox.Event)
g.RecordedEvents = make(chan *GocuiEvent)
g.BgColor, g.FgColor = ColorDefault, ColorDefault
g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault
if runtime.GOOS != "windows" {
g.maxX, g.maxY, err = g.getTermWindowSize()
if err != nil {
return nil, err
}
} else {
g.maxX, g.maxY = screen.Size()
}
g.BgColor, g.FgColor, g.FrameColor = ColorDefault, ColorDefault, ColorDefault
g.SelBgColor, g.SelFgColor, g.SelFrameColor = ColorDefault, ColorDefault, ColorDefault
// SupportOverlaps is true when we allow for view edges to overlap with other
// view edges
@ -158,8 +181,10 @@ func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, err
// Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore.
func (g *Gui) Close() {
close(g.stop)
termbox.Close()
go func() {
g.stop <- struct{}{}
}()
screen.Fini()
}
// Size returns the terminal's size.
@ -175,7 +200,7 @@ func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
// swallowing error because it's not that big of a deal
return nil
}
termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor))
tcellSetCell(x, y, ch, fgColor, bgColor, g.outputMode)
return nil
}
@ -185,8 +210,8 @@ func (g *Gui) Rune(x, y int) (rune, error) {
if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
return ' ', errors.New("invalid point")
}
c := termbox.CellBuffer()[y*g.maxX+x]
return c.Ch, nil
c, _, _, _ := screen.GetContent(x, y)
return c, nil
}
// SetView creates a new view with its top-left corner at (x0, y0)
@ -290,11 +315,7 @@ func (g *Gui) ViewByPosition(x, y int) (*View, error) {
// traverse views in reverse order checking top views first
for i := len(g.views); i > 0; i-- {
v := g.views[i-1]
frameOffset := 0
if v.Frame {
frameOffset = 1
}
if x > v.x0-frameOffset && x < v.x1+frameOffset && y > v.y0-frameOffset && y < v.y1+frameOffset {
if x > v.x0 && x < v.x1 && y > v.y0 && y < v.y1 {
return v, nil
}
}
@ -304,9 +325,6 @@ func (g *Gui) ViewByPosition(x, y int) (*View, error) {
// ViewPosition returns the coordinates of the view with the given name, or
// error ErrUnknownView if a view with that name does not exist.
func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) {
g.Mutexes.ViewsMutex.Lock()
defer g.Mutexes.ViewsMutex.Unlock()
for _, v := range g.views {
if v.name == name {
return v.x0, v.y0, v.x1, v.y1, nil
@ -317,9 +335,6 @@ func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) {
// DeleteView deletes a view by name.
func (g *Gui) DeleteView(name string) error {
g.Mutexes.ViewsMutex.Lock()
defer g.Mutexes.ViewsMutex.Unlock()
for i, v := range g.views {
if v.name == name {
g.views = append(g.views[:i], g.views[i+1:]...)
@ -352,6 +367,11 @@ func (g *Gui) CurrentView() *View {
// SetKeybinding creates a new keybinding. If viewname equals to ""
// (empty string) then the keybinding will apply to all views. key must
// be a rune or a Key.
//
// When mouse keys are used (MouseLeft, MouseRight, ...), modifier might not work correctly.
// It behaves differently on different platforms. Somewhere it doesn't register Alt key press,
// on others it might report Ctrl as Alt. It's not consistent and therefore it's not recommended
// to use with mouse keys.
func (g *Gui) SetKeybinding(viewname string, contexts []string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error {
var kb *keybinding
@ -359,6 +379,11 @@ func (g *Gui) SetKeybinding(viewname string, contexts []string, key interface{},
if err != nil {
return err
}
if g.isBlacklisted(k) {
return ErrBlacklisted
}
kb = newKeybinding(viewname, contexts, k, ch, mod, handler)
g.keybindings = append(g.keybindings, kb)
return nil
@ -401,6 +426,28 @@ func (g *Gui) SetTabClickBinding(viewName string, handler tabClickHandler) error
return nil
}
// 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) {
@ -425,7 +472,14 @@ type userEvent struct {
// the user events queue. Given that Update spawns a goroutine, the order in
// which the user events will be handled is not guaranteed.
func (g *Gui) Update(f func(*Gui) error) {
go func() { g.userEvents <- userEvent{f: f} }()
go g.UpdateAsync(f)
}
// UpdateAsync is a version of Update that does not spawn a go routine, it can
// be a bit more efficient in cases where Update is called many times like when
// tailing a file. In general you should use Update()
func (g *Gui) UpdateAsync(f func(*Gui) error) {
g.userEvents <- userEvent{f: f}
}
// A Manager is in charge of GUI's layout and can be used to build widgets.
@ -454,7 +508,7 @@ func (g *Gui) SetManager(managers ...Manager) {
g.keybindings = nil
g.tabClickBindings = nil
go func() { g.tbEvents <- termbox.Event{Type: termbox.EventResize} }()
go func() { g.gEvents <- GocuiEvent{Type: eventResize} }()
}
// SetManagerFunc sets the given manager function. It deletes all views and
@ -476,26 +530,21 @@ func (g *Gui) MainLoop() error {
case <-g.stop:
return
default:
g.tbEvents <- termbox.PollEvent()
g.gEvents <- pollEvent()
}
}
}()
inputMode := termbox.InputAlt
if true { // previously g.InputEsc, but didn't seem to work
inputMode = termbox.InputEsc
}
if g.Mouse {
inputMode |= termbox.InputMouse
screen.EnableMouse()
}
termbox.SetInputMode(inputMode)
if err := g.flush(); err != nil {
return err
}
for {
select {
case ev := <-g.tbEvents:
case ev := <-g.gEvents:
if err := g.handleEvent(&ev); err != nil {
return err
}
@ -521,7 +570,7 @@ func (g *Gui) MainLoop() error {
func (g *Gui) consumeevents() error {
for {
select {
case ev := <-g.tbEvents:
case ev := <-g.gEvents:
if err := g.handleEvent(&ev); err != nil {
return err
}
@ -541,16 +590,15 @@ func (g *Gui) consumeevents() error {
// handleEvent handles an event, based on its type (key-press, error,
// etc.)
func (g *Gui) handleEvent(ev *termbox.Event) error {
if g.RecordEvents {
g.RecordedEvents <- ev
}
func (g *Gui) handleEvent(ev *GocuiEvent) error {
switch ev.Type {
case termbox.EventKey, termbox.EventMouse:
case eventKey, eventMouse:
return g.onKey(ev)
case termbox.EventError:
case eventError:
return ev.Err
// Not sure if this should be handled. It acts weirder when it's here
// case eventResize:
// return Sync()
default:
return nil
}
@ -558,9 +606,9 @@ func (g *Gui) handleEvent(ev *termbox.Event) error {
// flush updates the gui, re-drawing frames and buffers.
func (g *Gui) flush() error {
termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor))
g.clear(g.FgColor, g.BgColor)
maxX, maxY := termbox.Size()
maxX, maxY := screen.Size()
// if GUI's size has changed, we need to redraw all views
if maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views {
@ -575,16 +623,33 @@ func (g *Gui) flush() error {
}
}
for _, v := range g.views {
if v.y1 < v.y0 {
if !v.Visible || v.y1 < v.y0 {
continue
}
if v.Frame {
fgColor, bgColor := g.viewColors(v)
var fgColor, bgColor, frameColor Attribute
if g.Highlight && v == g.currentView {
fgColor = g.SelFgColor
bgColor = g.SelBgColor
frameColor = g.SelFrameColor
} else {
bgColor = g.BgColor
if v.TitleColor != ColorDefault {
fgColor = v.TitleColor
} else {
fgColor = g.FgColor
}
if v.FrameColor != ColorDefault {
frameColor = v.FrameColor
} else {
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 != "" || len(v.Tabs) > 0 {
@ -607,15 +672,19 @@ func (g *Gui) flush() error {
return err
}
}
termbox.Flush()
screen.Show()
return nil
}
func (g *Gui) viewColors(v *View) (Attribute, Attribute) {
if g.Highlight && v == g.currentView {
return g.SelFgColor, g.SelBgColor
func (g *Gui) clear(fg, bg Attribute) (int, int) {
st := getTcellStyle(fg, bg, g.outputMode)
w, h := screen.Size()
for row := 0; row < h; row++ {
for col := 0; col < w; col++ {
screen.SetContent(col, row, ' ', nil, st)
}
}
return g.FgColor, g.BgColor
return w, h
}
// drawFrameEdges draws the horizontal and vertical edges of a view.
@ -623,6 +692,8 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
runeH, runeV := '─', '│'
if g.ASCII {
runeH, runeV = '-', '|'
} else if len(v.FrameRunes) >= 2 {
runeH, runeV = v.FrameRunes[0], v.FrameRunes[1]
}
for x := v.x0 + 1; x < v.x1 && x < g.maxX; x++ {
@ -662,8 +733,64 @@ func cornerRune(index byte) rune {
return []rune{' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '├', '┴', '┬', '┼'}[index]
}
// cornerCustomRune returns rune from `v.FrameRunes` slice. If the length of slice is less than 11
// all the missing runes will be translated to the default `cornerRune()`
func cornerCustomRune(v *View, index byte) rune {
// Translate `cornerRune()` index
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// ' ', '│', '│', '│', '─', '┘', '┐', '┤', '─', '└', '┌', '├', '├', '┴', '┬', '┼'
// into `FrameRunes` index
// 0 1 2 3 4 5 6 7 8 9 10
// '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼'
switch index {
case 1, 2, 3:
return v.FrameRunes[1]
case 4, 8:
return v.FrameRunes[0]
case 5:
return v.FrameRunes[5]
case 6:
return v.FrameRunes[3]
case 7:
if len(v.FrameRunes) < 8 {
break
}
return v.FrameRunes[7]
case 9:
return v.FrameRunes[4]
case 10:
return v.FrameRunes[2]
case 11, 12:
if len(v.FrameRunes) < 7 {
break
}
return v.FrameRunes[6]
case 13:
if len(v.FrameRunes) < 10 {
break
}
return v.FrameRunes[9]
case 14:
if len(v.FrameRunes) < 9 {
break
}
return v.FrameRunes[8]
case 15:
if len(v.FrameRunes) < 11 {
break
}
return v.FrameRunes[10]
default:
return ' ' // cornerRune(0)
}
return cornerRune(index)
}
func corner(v *View, directions byte) rune {
index := v.Overlaps | directions
if len(v.FrameRunes) >= 6 {
return cornerCustomRune(v, index)
}
return cornerRune(index)
}
@ -682,6 +809,9 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
}
runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘'
if len(v.FrameRunes) >= 6 {
runeTL, runeTR, runeBL, runeBR = v.FrameRunes[2], v.FrameRunes[3], v.FrameRunes[4], v.FrameRunes[5]
}
if g.SupportOverlaps {
runeTL = corner(v, BOTTOM|RIGHT)
runeTR = corner(v, BOTTOM|LEFT)
@ -743,7 +873,6 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
} else if x > v.x1-2 || x >= g.maxX {
break
}
currentFgColor := fgColor
currentBgColor := bgColor
// if you are the current view and you have multiple tabs, de-highlight the non-selected tabs
@ -765,7 +894,6 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
return err
}
}
return nil
}
@ -837,14 +965,17 @@ func (g *Gui) draw(v *View) error {
gMaxX, gMaxY := g.Size()
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
// This test probably doesn't need to be here.
// tcell is hiding cursor by setting coordinates outside of screen.
// Keeping it here for now, as I'm not 100% sure :)
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
termbox.SetCursor(cx, cy)
screen.ShowCursor(cx, cy)
} else {
termbox.HideCursor()
screen.HideCursor()
}
}
} else {
termbox.HideCursor()
screen.HideCursor()
}
v.clearRunes()
@ -857,9 +988,9 @@ func (g *Gui) draw(v *View) error {
// onKey manages key-press events. A keybinding handler is called when
// a key-press or mouse event satisfies a configured keybinding. Furthermore,
// currentView's internal buffer is modified if currentView.Editable is true.
func (g *Gui) onKey(ev *termbox.Event) error {
func (g *Gui) onKey(ev *GocuiEvent) error {
switch ev.Type {
case termbox.EventKey:
case eventKey:
matched, err := g.execKeybindings(g.currentView, ev)
if err != nil {
return err
@ -870,7 +1001,7 @@ func (g *Gui) onKey(ev *termbox.Event) error {
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod))
}
case termbox.EventMouse:
case eventMouse:
mx, my := ev.MouseX, ev.MouseY
v, err := g.ViewByPosition(mx, my)
if err != nil {
@ -911,7 +1042,7 @@ 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) {
func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) {
var globalKb *keybinding
var matchingParentViewKb *keybinding
@ -960,6 +1091,10 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err
// 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
}
@ -989,3 +1124,23 @@ func (g *Gui) StartTicking() {
}
}()
}
// isBlacklisted reports whether the key is blacklisted
func (g *Gui) isBlacklisted(k Key) bool {
for _, j := range g.blacklist {
if j == k {
return true
}
}
return false
}
// IsUnknownView reports whether the contents of an error is "unknown view".
func IsUnknownView(err error) bool {
return err != nil && err.Error() == ErrUnknownView.Error()
}
// IsQuit reports whether the contents of an error is "quit".
func IsQuit(err error) bool {
return err != nil && err.Error() == ErrQuit.Error()
}

View File

@ -1,3 +1,7 @@
// 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
@ -11,16 +15,17 @@ import (
"github.com/go-errors/errors"
)
type windowSize struct {
rows uint16
cols uint16
xpixels uint16
ypixels uint16
}
// 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
@ -29,21 +34,15 @@ func (g *Gui) getTermWindowSize() (int, int, error) {
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGWINCH, syscall.SIGINT)
defer signal.Stop(signalCh)
var sz windowSize
for {
_, _, err = syscall.Syscall(
syscall.SYS_IOCTL,
out.Fd(),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&sz)),
)
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
out.Fd(), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
// check terminal window size
if sz.cols > 0 && sz.rows > 0 {
return int(sz.cols), int(sz.rows), nil
termw, termh = int(sz.cols), int(sz.rows)
if termw > 0 && termh > 0 {
return termw, termh, nil
}
select {
@ -54,7 +53,7 @@ func (g *Gui) getTermWindowSize() (int, int, error) {
continue
// ctrl + c to cancel
case syscall.SIGINT:
return 0, 0, errors.New("There was not enough window space to start the application")
return 0, 0, errors.New("stop to get term window size")
}
}
}

View File

@ -1,10 +0,0 @@
// +build windows
package gocui
import "github.com/jesseduffield/termbox-go"
func (g *Gui) getTermWindowSize() (int, int, error) {
x, y := termbox.Size()
return x, y, nil
}

53
vendor/github.com/jesseduffield/gocui/gui_windows.go generated vendored Normal file
View File

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

View File

@ -4,7 +4,18 @@
package gocui
import "github.com/jesseduffield/termbox-go"
import (
"strings"
"github.com/gdamore/tcell/v2"
)
// Key represents special keys or keys combinations.
type Key tcell.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 tcell.ModMask
// Keybidings are used to link a given key-press event with a handler.
type keybinding struct {
@ -16,6 +27,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, contexts []string, key Key, ch rune, mod Modifier, handler func(*Gui, *View) error) (kb *keybinding) {
kb = &keybinding{
@ -29,7 +105,7 @@ func newKeybinding(viewname string, contexts []string, key Key, ch rune, mod Mod
return kb
}
func eventMatchesKey(ev *termbox.Event, key interface{}) bool {
func eventMatchesKey(ev *GocuiEvent, key interface{}) bool {
// assuming ModNone for now
if Modifier(ev.Mod) != ModNone {
return false
@ -72,99 +148,177 @@ func (kb *keybinding) matchView(v *View) bool {
return false
}
// 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 (
KeyF1 Key = Key(termbox.KeyF1)
KeyF2 = Key(termbox.KeyF2)
KeyF3 = Key(termbox.KeyF3)
KeyF4 = Key(termbox.KeyF4)
KeyF5 = Key(termbox.KeyF5)
KeyF6 = Key(termbox.KeyF6)
KeyF7 = Key(termbox.KeyF7)
KeyF8 = Key(termbox.KeyF8)
KeyF9 = Key(termbox.KeyF9)
KeyF10 = Key(termbox.KeyF10)
KeyF11 = Key(termbox.KeyF11)
KeyF12 = Key(termbox.KeyF12)
KeyInsert = Key(termbox.KeyInsert)
KeyDelete = Key(termbox.KeyDelete)
KeyHome = Key(termbox.KeyHome)
KeyEnd = Key(termbox.KeyEnd)
KeyPgup = Key(termbox.KeyPgup)
KeyPgdn = Key(termbox.KeyPgdn)
KeyArrowUp = Key(termbox.KeyArrowUp)
KeyArrowDown = Key(termbox.KeyArrowDown)
KeyArrowLeft = Key(termbox.KeyArrowLeft)
KeyArrowRight = Key(termbox.KeyArrowRight)
MouseLeft = Key(termbox.MouseLeft)
MouseMiddle = Key(termbox.MouseMiddle)
MouseRight = Key(termbox.MouseRight)
MouseRelease = Key(termbox.MouseRelease)
MouseWheelUp = Key(termbox.MouseWheelUp)
MouseWheelDown = Key(termbox.MouseWheelDown)
KeyF1 Key = Key(tcell.KeyF1)
KeyF2 = Key(tcell.KeyF2)
KeyF3 = Key(tcell.KeyF3)
KeyF4 = Key(tcell.KeyF4)
KeyF5 = Key(tcell.KeyF5)
KeyF6 = Key(tcell.KeyF6)
KeyF7 = Key(tcell.KeyF7)
KeyF8 = Key(tcell.KeyF8)
KeyF9 = Key(tcell.KeyF9)
KeyF10 = Key(tcell.KeyF10)
KeyF11 = Key(tcell.KeyF11)
KeyF12 = Key(tcell.KeyF12)
KeyInsert = Key(tcell.KeyInsert)
KeyDelete = Key(tcell.KeyDelete)
KeyHome = Key(tcell.KeyHome)
KeyEnd = Key(tcell.KeyEnd)
KeyPgdn = Key(tcell.KeyPgDn)
KeyPgup = Key(tcell.KeyPgUp)
KeyArrowUp = Key(tcell.KeyUp)
KeyArrowDown = Key(tcell.KeyDown)
KeyArrowLeft = Key(tcell.KeyLeft)
KeyArrowRight = Key(tcell.KeyRight)
)
// Keys combinations.
const (
KeyCtrlTilde Key = Key(termbox.KeyCtrlTilde)
KeyCtrl2 = Key(termbox.KeyCtrl2)
KeyCtrlSpace = Key(termbox.KeyCtrlSpace)
KeyCtrlA = Key(termbox.KeyCtrlA)
KeyCtrlB = Key(termbox.KeyCtrlB)
KeyCtrlC = Key(termbox.KeyCtrlC)
KeyCtrlD = Key(termbox.KeyCtrlD)
KeyCtrlE = Key(termbox.KeyCtrlE)
KeyCtrlF = Key(termbox.KeyCtrlF)
KeyCtrlG = Key(termbox.KeyCtrlG)
KeyBackspace = Key(termbox.KeyBackspace)
KeyCtrlH = Key(termbox.KeyCtrlH)
KeyTab = Key(termbox.KeyTab)
KeyCtrlI = Key(termbox.KeyCtrlI)
KeyCtrlJ = Key(termbox.KeyCtrlJ)
KeyCtrlK = Key(termbox.KeyCtrlK)
KeyCtrlL = Key(termbox.KeyCtrlL)
KeyEnter = Key(termbox.KeyEnter)
KeyCtrlM = Key(termbox.KeyCtrlM)
KeyCtrlN = Key(termbox.KeyCtrlN)
KeyCtrlO = Key(termbox.KeyCtrlO)
KeyCtrlP = Key(termbox.KeyCtrlP)
KeyCtrlQ = Key(termbox.KeyCtrlQ)
KeyCtrlR = Key(termbox.KeyCtrlR)
KeyCtrlS = Key(termbox.KeyCtrlS)
KeyCtrlT = Key(termbox.KeyCtrlT)
KeyCtrlU = Key(termbox.KeyCtrlU)
KeyCtrlV = Key(termbox.KeyCtrlV)
KeyCtrlW = Key(termbox.KeyCtrlW)
KeyCtrlX = Key(termbox.KeyCtrlX)
KeyCtrlY = Key(termbox.KeyCtrlY)
KeyCtrlZ = Key(termbox.KeyCtrlZ)
KeyEsc = Key(termbox.KeyEsc)
KeyCtrlLsqBracket = Key(termbox.KeyCtrlLsqBracket)
KeyCtrl3 = Key(termbox.KeyCtrl3)
KeyCtrl4 = Key(termbox.KeyCtrl4)
KeyCtrlBackslash = Key(termbox.KeyCtrlBackslash)
KeyCtrl5 = Key(termbox.KeyCtrl5)
KeyCtrlRsqBracket = Key(termbox.KeyCtrlRsqBracket)
KeyCtrl6 = Key(termbox.KeyCtrl6)
KeyCtrl7 = Key(termbox.KeyCtrl7)
KeyCtrlSlash = Key(termbox.KeyCtrlSlash)
KeyCtrlUnderscore = Key(termbox.KeyCtrlUnderscore)
KeySpace = Key(termbox.KeySpace)
KeyBackspace2 = Key(termbox.KeyBackspace2)
KeyCtrl8 = Key(termbox.KeyCtrl8)
)
KeyCtrlTilde = Key(tcell.KeyF64) // arbitrary assignment
KeyCtrlSpace = Key(tcell.KeyCtrlSpace)
KeyCtrlA = Key(tcell.KeyCtrlA)
KeyCtrlB = Key(tcell.KeyCtrlB)
KeyCtrlC = Key(tcell.KeyCtrlC)
KeyCtrlD = Key(tcell.KeyCtrlD)
KeyCtrlE = Key(tcell.KeyCtrlE)
KeyCtrlF = Key(tcell.KeyCtrlF)
KeyCtrlG = Key(tcell.KeyCtrlG)
KeyBackspace = Key(tcell.KeyBackspace)
KeyCtrlH = Key(tcell.KeyCtrlH)
KeyTab = Key(tcell.KeyTab)
KeyCtrlI = Key(tcell.KeyCtrlI)
KeyCtrlJ = Key(tcell.KeyCtrlJ)
KeyCtrlK = Key(tcell.KeyCtrlK)
KeyCtrlL = Key(tcell.KeyCtrlL)
KeyEnter = Key(tcell.KeyEnter)
KeyCtrlM = Key(tcell.KeyCtrlM)
KeyCtrlN = Key(tcell.KeyCtrlN)
KeyCtrlO = Key(tcell.KeyCtrlO)
KeyCtrlP = Key(tcell.KeyCtrlP)
KeyCtrlQ = Key(tcell.KeyCtrlQ)
KeyCtrlR = Key(tcell.KeyCtrlR)
KeyCtrlS = Key(tcell.KeyCtrlS)
KeyCtrlT = Key(tcell.KeyCtrlT)
KeyCtrlU = Key(tcell.KeyCtrlU)
KeyCtrlV = Key(tcell.KeyCtrlV)
KeyCtrlW = Key(tcell.KeyCtrlW)
KeyCtrlX = Key(tcell.KeyCtrlX)
KeyCtrlY = Key(tcell.KeyCtrlY)
KeyCtrlZ = Key(tcell.KeyCtrlZ)
KeyEsc = Key(tcell.KeyEscape)
KeyCtrlUnderscore = Key(tcell.KeyCtrlUnderscore)
KeySpace = Key(32)
KeyBackspace2 = Key(tcell.KeyBackspace2)
KeyCtrl8 = Key(tcell.KeyBackspace2) // same key as in termbox-go
// 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
// The following assignments were used in termbox implementation.
// In tcell, these are not keys per se. But in gocui we have them
// mapped to the keys so we have to use placeholder keys.
MouseLeft = Key(tcell.KeyF63) // arbitrary assignments
MouseRight = Key(tcell.KeyF62)
MouseMiddle = Key(tcell.KeyF61)
MouseRelease = Key(tcell.KeyF60)
MouseWheelUp = Key(tcell.KeyF59)
MouseWheelDown = Key(tcell.KeyF58)
MouseWheelLeft = Key(tcell.KeyF57)
MouseWheelRight = Key(tcell.KeyF56)
KeyCtrl2 = Key(tcell.KeyNUL) // termbox defines theses
KeyCtrl3 = Key(tcell.KeyEscape)
KeyCtrl4 = Key(tcell.KeyCtrlBackslash)
KeyCtrl5 = Key(tcell.KeyCtrlRightSq)
KeyCtrl6 = Key(tcell.KeyCtrlCarat)
KeyCtrl7 = Key(tcell.KeyCtrlUnderscore)
KeyCtrlSlash = Key(tcell.KeyCtrlUnderscore)
KeyCtrlRsqBracket = Key(tcell.KeyCtrlRightSq)
KeyCtrlBackslash = Key(tcell.KeyCtrlBackslash)
KeyCtrlLsqBracket = Key(tcell.KeyCtrlLeftSq)
)
// Modifiers.
const (
ModNone Modifier = Modifier(0)
ModAlt = Modifier(termbox.ModAlt)
ModMotion = Modifier(termbox.ModMotion)
ModNone Modifier = Modifier(0)
ModAlt = Modifier(tcell.ModAlt)
// ModCtrl doesn't work with keyboard keys. Use CtrlKey in Key and ModNone. This is was for mouse clicks only (tcell.v1)
// ModCtrl = Modifier(tcell.ModCtrl)
)

46
vendor/github.com/jesseduffield/gocui/loader.go generated vendored Normal file
View File

@ -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,
}
}

205
vendor/github.com/jesseduffield/gocui/tcell_driver.go generated vendored Normal file
View File

@ -0,0 +1,205 @@
// Copyright 2020 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocui
import (
"github.com/gdamore/tcell/v2"
)
var screen tcell.Screen
// tcellInit initializes tcell screen for use.
func tcellInit() error {
if s, e := tcell.NewScreen(); e != nil {
return e
} else if e = s.Init(); e != nil {
return e
} else {
screen = s
return nil
}
}
// tcellSetCell sets the character cell at a given location to the given
// content (rune) and attributes using provided OutputMode
func tcellSetCell(x, y int, ch rune, fg, bg Attribute, omode OutputMode) {
st := getTcellStyle(fg, bg, omode)
screen.SetContent(x, y, ch, nil, st)
}
// getTcellStyle creates tcell.Style from Attributes
func getTcellStyle(fg, bg Attribute, omode OutputMode) tcell.Style {
st := tcell.StyleDefault
// extract colors and attributes
if fg != ColorDefault {
st = st.Foreground(getTcellColor(fg, omode))
st = setTcellFontEffectStyle(st, fg)
}
if bg != ColorDefault {
st = st.Background(getTcellColor(bg, omode))
st = setTcellFontEffectStyle(st, bg)
}
return st
}
// setTcellFontEffectStyle add additional attributes to tcell.Style
func setTcellFontEffectStyle(st tcell.Style, attr Attribute) tcell.Style {
if attr&AttrBold != 0 {
st = st.Bold(true)
}
if attr&AttrUnderline != 0 {
st = st.Underline(true)
}
if attr&AttrReverse != 0 {
st = st.Reverse(true)
}
if attr&AttrBlink != 0 {
st = st.Blink(true)
}
if attr&AttrDim != 0 {
st = st.Dim(true)
}
if attr&AttrItalic != 0 {
st = st.Italic(true)
}
if attr&AttrStrikeThrough != 0 {
st = st.StrikeThrough(true)
}
return st
}
// gocuiEventType represents the type of event.
type gocuiEventType uint8
// GocuiEvent represents events like a keys, mouse actions, or window resize.
// The 'Mod', 'Key' and 'Ch' fields are valid if 'Type' is 'eventKey'.
// The 'MouseX' and 'MouseY' fields are valid if 'Type' is 'eventMouse'.
// The 'Width' and 'Height' fields are valid if 'Type' is 'eventResize'.
// The 'Err' field is valid if 'Type' is 'eventError'.
type GocuiEvent struct {
Type gocuiEventType
Mod Modifier
Key Key
Ch rune
Width int
Height int
Err error
MouseX int
MouseY int
N int
}
// Event types.
const (
eventNone gocuiEventType = iota
eventKey
eventResize
eventMouse
eventInterrupt
eventError
eventRaw
)
var (
lastMouseKey tcell.ButtonMask = tcell.ButtonNone
lastMouseMod tcell.ModMask = tcell.ModNone
)
// pollEvent get tcell.Event and transform it into gocuiEvent
func pollEvent() GocuiEvent {
tev := screen.PollEvent()
switch tev := tev.(type) {
case *tcell.EventInterrupt:
return GocuiEvent{Type: eventInterrupt}
case *tcell.EventResize:
w, h := tev.Size()
return GocuiEvent{Type: eventResize, Width: w, Height: h}
case *tcell.EventKey:
k := tev.Key()
ch := rune(0)
if k == tcell.KeyRune {
k = 0 // if rune remove key (so it can match rune instead of key)
ch = tev.Rune()
if ch == ' ' {
// special handling for spacebar
k = 32 // tcell keys ends at 31 or starts at 256
ch = rune(0)
}
}
mod := tev.Modifiers()
// remove control modifier and setup special handling of ctrl+spacebar, etc.
if mod == tcell.ModCtrl && k == 32 {
mod = 0
ch = rune(0)
k = tcell.KeyCtrlSpace
} else if mod == tcell.ModCtrl || mod == tcell.ModShift {
// remove Ctrl or Shift if specified
// - shift - will be translated to the final code of rune
// - ctrl - is translated in the key
mod = 0
}
return GocuiEvent{
Type: eventKey,
Key: Key(k),
Ch: ch,
Mod: Modifier(mod),
}
case *tcell.EventMouse:
x, y := tev.Position()
button := tev.Buttons()
mouseKey := MouseRelease
mouseMod := ModNone
// process mouse wheel
if button&tcell.WheelUp != 0 {
mouseKey = MouseWheelUp
}
if button&tcell.WheelDown != 0 {
mouseKey = MouseWheelDown
}
if button&tcell.WheelLeft != 0 {
mouseKey = MouseWheelLeft
}
if button&tcell.WheelRight != 0 {
mouseKey = MouseWheelRight
}
// process button events (not wheel events)
button &= tcell.ButtonMask(0xff)
if button != tcell.ButtonNone && lastMouseKey == tcell.ButtonNone {
lastMouseKey = button
lastMouseMod = tev.Modifiers()
}
switch tev.Buttons() {
case tcell.ButtonNone:
if lastMouseKey != tcell.ButtonNone {
switch lastMouseKey {
case tcell.ButtonPrimary:
mouseKey = MouseLeft
case tcell.ButtonSecondary:
mouseKey = MouseRight
case tcell.ButtonMiddle:
mouseKey = MouseMiddle
}
mouseMod = Modifier(lastMouseMod)
lastMouseMod = tcell.ModNone
lastMouseKey = tcell.ButtonNone
}
}
return GocuiEvent{
Type: eventMouse,
MouseX: x,
MouseY: y,
Key: mouseKey,
Ch: 0,
Mod: mouseMod,
}
default:
return GocuiEvent{Type: eventNone}
}
}

View File

@ -9,12 +9,10 @@ import (
"io"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/termbox-go"
"github.com/mattn/go-runewidth"
)
@ -26,21 +24,41 @@ const (
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
outMode OutputMode
tainted bool // marks if the viewBuffer must be updated
viewLines []viewLine // internal representation of the view's buffer
// readBuffer is used for storing unread bytes
readBuffer []byte
ei *escapeInterpreter // used to decode ESC sequences on Write
// tained is true if the viewLines must be updated
tainted bool
// internal representation of the view's buffer
viewLines []viewLine
// writeMutex protects locks the write process
writeMutex sync.Mutex
// ei is used to decode ESC sequences on Write
ei *escapeInterpreter
// Visible specifies whether the view is visible.
Visible bool
// BgColor and FgColor allow to configure the background and foreground
// colors of the View.
@ -54,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
@ -69,6 +87,24 @@ type View struct {
// If Frame is true, a border will be drawn around the view.
Frame bool
// FrameColor allow to configure the color of the Frame when it is not highlighted.
FrameColor Attribute
// FrameRunes allows to define custom runes for the frame edges.
// The rune slice can be defined with 3 different lengths.
// If slice doesn't match these lengths, default runes will be used instead of missing one.
//
// 2 runes with only horizontal and vertical edges.
// []rune{'─', '│'}
// []rune{'═','║'}
// 6 runes with horizontal, vertical edges and top-left, top-right, bottom-left, bottom-right cornes.
// []rune{'─', '│', '┌', '┐', '└', '┘'}
// []rune{'═','║','╔','╗','╚','╝'}
// 11 runes which can be used with `gocui.Gui.SupportOverlaps` property.
// []rune{'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼'}
// []rune{'═','║','╔','╗','╚','╝','╠','╣','╦','╩','╬'}
FrameRunes []rune
// If Wrap is true, the content that is written to this View is
// automatically wrapped when it is longer than its width. If true the
// view's x-origin will be ignored.
@ -85,6 +121,8 @@ type View struct {
TabIndex int
// HighlightTabWithoutFocus allows you to show which tab is selected without the view being focused
HighlightSelectedTabWithoutFocus bool
// TitleColor allow to configure the color of title and subtitle for the view.
TitleColor Attribute
// If Frame is true, Subtitle allows to configure a subtitle for the view.
Subtitle string
@ -99,8 +137,6 @@ type View struct {
// If HasLoader is true, the message will be appended with a spinning loader animation
HasLoader bool
writeMutex sync.Mutex
// IgnoreCarriageReturns tells us whether to ignore '\r' characters
IgnoreCarriageReturns bool
@ -113,6 +149,10 @@ type View struct {
// when ContainsList is true, we show the current index and total count in the view
ContainsList bool
// KeybindOnEdit should be set to true when you want to execute keybindings even when the view is editable
// (this is usually not the case)
KeybindOnEdit bool
}
type searcher struct {
@ -280,12 +320,18 @@ 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,
outMode: mode,
ei: newEscapeInterpreter(mode),
searcher: &searcher{},
}
v.FgColor, v.BgColor = ColorDefault, ColorDefault
v.SelFgColor, v.SelBgColor = ColorDefault, ColorDefault
v.TitleColor, v.FrameColor = ColorDefault, ColorDefault
return v
}
@ -310,7 +356,7 @@ 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
@ -336,8 +382,12 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
bgColor = bgColor | v.SelBgColor
}
termbox.SetCell(v.x0+x+1, v.y0+y+1, ch,
termbox.Attribute(fgColor), termbox.Attribute(bgColor))
// Don't display NUL characters
if ch == 0 {
ch = ' '
}
tcellSetCell(v.x0+x+1, v.y0+y+1, ch, fgColor, bgColor, v.outMode)
return nil
}
@ -365,6 +415,9 @@ func (v *View) Cursor() (x, y int) {
// implement Horizontal and Vertical scrolling with just incrementing
// or decrementing ox and oy.
func (v *View) SetOrigin(x, y int) error {
if x < 0 || y < 0 {
return ErrInvalidPoint
}
v.ox = x
v.oy = y
return nil
@ -375,6 +428,91 @@ 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
@ -382,71 +520,48 @@ func (v *View) Origin() (x, y int) {
func (v *View) Write(p []byte) (n int, err error) {
v.tainted = true
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
for _, ch := range bytes.Runes(p) {
switch ch {
case '\n':
v.lines = append(v.lines, nil)
case '\r':
if v.IgnoreCarriageReturns {
continue
}
nl := len(v.lines)
if nl > 0 {
v.lines[nl-1] = nil
} else {
v.lines = make([][]cell, 1)
}
default:
cells := v.parseInput(ch)
if v.ei.instruction.kind != NONE {
switch v.ei.instruction.kind {
case ERASE_IN_LINE:
v.eraseInLine()
}
v.ei.instructionRead()
continue
}
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.makeWriteable(v.wx, v.wy)
v.writeRunes(bytes.Runes(p))
v.writeMutex.Unlock()
return len(p), nil
}
func (v *View) eraseInLine() {
code := v.ei.instruction.param1
switch code {
case 0:
// need to write till end of the line with cells containing the same bg colour as we currently have.
func (v *View) WriteRunes(p []rune) {
v.tainted = true
if len(v.lines) == 0 {
v.lines = append(v.lines, []cell{})
// Fill with empty cells, if writing outside current view buffer
v.makeWriteable(v.wx, v.wy)
v.writeRunes(p)
}
func (v *View) WriteString(s string) {
v.WriteRunes([]rune(s))
}
// writeRunes copies slice of runes into internal lines buffer.
// caller must make sure that writing position is accessable.
func (v *View) writeRunes(p []rune) {
for _, r := range p {
switch r {
case '\n':
v.wy++
if v.wy >= len(v.lines) {
v.lines = append(v.lines, nil)
}
fallthrough
// not valid in every OS, but making runtime OS checks in cycle is bad.
case '\r':
v.wx = 0
default:
cells := v.parseInput(r)
if cells == nil {
continue
}
v.writeCells(v.wx, v.wy, cells)
v.wx += len(cells)
}
nl := len(v.lines)
width, _ := v.Size()
cellCount := width - len(v.lines[nl-1])
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ' ',
}
for i := 0; i < cellCount; i++ {
v.lines[nl-1] = append(v.lines[nl-1], c)
}
default:
// don't recognise sequence. Until we merge the gocui master branch we can't handle going backwards.
}
}
@ -489,26 +604,54 @@ func (v *View) parseInput(ch rune) []cell {
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)
}
}
func containsUpcaseChar(str string) bool {
@ -517,6 +660,7 @@ func containsUpcaseChar(str string) bool {
return true
}
}
return false
}
@ -551,13 +695,18 @@ func (v *View) updateSearchPositions() {
}
}
}
}
// 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 {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
if !v.Visible {
return nil
}
v.updateSearchPositions()
maxX, maxY := v.Size()
@ -630,7 +779,15 @@ func (v *View) draw() error {
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
return err
}
x += runewidth.RuneWidth(c.chr)
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++
}
@ -656,7 +813,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 {
@ -677,17 +834,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()
defer v.writeMutex.Unlock()
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.
@ -695,8 +851,7 @@ func (v *View) clearRunes() {
maxX, maxY := v.Size()
for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ {
termbox.SetCell(v.x0+x+1, v.y0+y+1, ' ',
termbox.Attribute(v.FgColor), termbox.Attribute(v.BgColor))
tcellSetCell(v.x0+x+1, v.y0+y+1, ' ', v.FgColor, v.BgColor, v.outMode)
}
}
}
@ -704,8 +859,6 @@ func (v *View) clearRunes() {
// BufferLines returns the lines in the view's internal
// buffer.
func (v *View) BufferLines() []string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
lines := make([]string, len(v.lines))
for i, l := range v.lines {
str := lineType(l).String()
@ -724,8 +877,6 @@ func (v *View) Buffer() string {
// ViewBufferLines returns the lines in the view's internal
// buffer that is shown to the user.
func (v *View) ViewBufferLines() []string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
lines := make([]string, len(v.viewLines))
for i, l := range v.viewLines {
str := lineType(l.line).String()
@ -765,7 +916,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
@ -780,7 +931,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()
@ -806,6 +957,48 @@ 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)
@ -852,40 +1045,6 @@ func linesToString(lines [][]cell) string {
return strings.Join(str, "\n")
}
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
}
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,
}
}
// IsTainted tells us if the view is tainted
func (v *View) IsTainted() bool {
return v.tainted
}
// GetClickedTabIndex tells us which tab was clicked
func (v *View) GetClickedTabIndex(x int) int {
if len(v.Tabs) <= 1 {