diff --git a/.circleci/config.yml b/.circleci/config.yml index 89d5f0ad..e275579a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,10 @@ version: 2 jobs: build: - working_directory: /go/src/github.com/glvr182/git-profile - + working_directory: /go/src/github.com/awesome-gocui/gocui + docker: - - image: circleci/golang:1.12 + - image: circleci/golang:1.15 steps: - checkout @@ -13,7 +13,7 @@ jobs: command: go mod tidy - run: name: go fmt - command: | + 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; @@ -24,11 +24,11 @@ jobs: cd _examples/ for file in *.go do - go build $file + go build $file done workflows: version: 2 build: jobs: - - build \ No newline at end of file + - build diff --git a/.gitignore b/.gitignore index 1377554e..51f2ba78 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.swp +.idea +.vscode \ No newline at end of file diff --git a/CHANGES_tcell.md b/CHANGES_tcell.md new file mode 100644 index 00000000..7aa55234 --- /dev/null +++ b/CHANGES_tcell.md @@ -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. diff --git a/README.md b/README.md index 3d8a0b29..bd781e34 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![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. +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 @@ -23,8 +23,9 @@ A community fork based on the amazing work of [jroimartin](https://github.com/jr ## About fork -This fork has many improvements over the original work from [jroimartin](https://github.com/jroimartin/gocui). +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 * Support for running in docker container @@ -69,7 +70,7 @@ import ( ) func main() { - g, err := gocui.NewGui(gocui.OutputNormal, false) + g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { log.Panicln(err) } @@ -92,11 +93,14 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - fmt.Fprintln(v, "Hello world!") + if _, err := g.SetCurrentView("hello"); err != nil { return err } + + fmt.Fprintln(v, "Hello world!") } + return nil } diff --git a/_examples/colorstrue.go b/_examples/colorstrue.go new file mode 100644 index 00000000..865a1b4e --- /dev/null +++ b/_examples/colorstrue.go @@ -0,0 +1,109 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + "os" + + "github.com/awesome-gocui/gocui" + colorful "github.com/lucasb-eyer/go-colorful" +) + +var dark = false + +func main() { + os.Setenv("COLORTERM", "truecolor") + g, err := gocui.NewGui(gocui.OutputTrue, true) + + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.SetManagerFunc(layout) + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { + if dark { + dark = false + } else { + dark = true + } + displayHsv(v) + + return nil + }); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { + log.Panicln(err) + } +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + rows := 33 + cols := 182 + if maxY < rows { + rows = maxY + } + if maxX < cols { + cols = maxX + } + + if v, err := g.SetView("colors", 0, 0, cols-1, rows-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + + v.FrameColor = gocui.GetColor("#FFAA55") + displayHsv(v) + + if _, err := g.SetCurrentView("colors"); err != nil { + return err + } + } + return nil +} + +func displayHsv(v *gocui.View) { + v.Clear() + str := "" + // HSV color space (lines are value or saturation) + for i := 50; i > 0; i -= 2 { + // Hue + for j := 0; j < 360; j += 2 { + ir, ig, ib := hsv(j, i-1) + ir2, ig2, ib2 := hsv(j, i) + str += fmt.Sprintf("\x1b[48;2;%d;%d;%dm\x1b[38;2;%d;%d;%dm▀\x1b[0m", ir, ig, ib, ir2, ig2, ib2) + } + str += "\n" + fmt.Fprint(v, str) + str = "" + } + + fmt.Fprintln(v, "\n\x1b[38;5;245mCtrl + R - Switch light/dark mode") + fmt.Fprintln(v, "\nCtrl + C - Exit\n") + fmt.Fprint(v, "Example should enable true color, but if it doesn't work run this command: \x1b[0mexport COLORTERM=truecolor") +} + +func hsv(hue, sv int) (uint32, uint32, uint32) { + if !dark { + ir, ig, ib, _ := colorful.Hsv(float64(hue), float64(sv)/50, float64(1)).RGBA() + return ir >> 8, ig >> 8, ib >> 8 + } + ir, ig, ib, _ := colorful.Hsv(float64(hue), float64(1), float64(sv)/50).RGBA() + return ir >> 8, ig >> 8, ib >> 8 +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} diff --git a/_examples/custom_frame.go b/_examples/custom_frame.go new file mode 100644 index 00000000..ef4ae9f2 --- /dev/null +++ b/_examples/custom_frame.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "log" + + "github.com/awesome-gocui/gocui" +) + +var ( + viewArr = []string{"v1", "v2", "v3", "v4"} + active = 0 +) + +func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { + if _, err := g.SetCurrentView(name); err != nil { + return nil, err + } + return g.SetViewOnTop(name) +} + +func nextView(g *gocui.Gui, v *gocui.View) error { + nextIndex := (active + 1) % len(viewArr) + name := viewArr[nextIndex] + + out, err := g.View("v1") + if err != nil { + return err + } + fmt.Fprintln(out, "Going from view "+v.Name()+" to "+name) + + if _, err := setCurrentViewOnTop(g, name); err != nil { + return err + } + + if nextIndex == 3 { + g.Cursor = true + } else { + g.Cursor = false + } + + active = nextIndex + return nil +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + if v, err := g.SetView("v1", 0, 0, maxX/2-1, maxY/2-1, gocui.RIGHT); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "v1" + v.Autoscroll = true + fmt.Fprintln(v, "View with default frame color") + fmt.Fprintln(v, "It's connected to v2 with overlay RIGHT.\n") + if _, err = setCurrentViewOnTop(g, "v1"); err != nil { + return err + } + } + + if v, err := g.SetView("v2", maxX/2-1, 0, maxX-1, maxY/2-1, gocui.LEFT); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "v2" + v.Wrap = true + v.FrameColor = gocui.ColorMagenta + v.FrameRunes = []rune{'═', '│'} + fmt.Fprintln(v, "View with minimum frame customization and colored frame.") + fmt.Fprintln(v, "It's connected to v1 with overlay LEFT.\n") + fmt.Fprintln(v, "\033[35;1mInstructions:\033[0m") + fmt.Fprintln(v, "Press TAB to change current view") + fmt.Fprintln(v, "Press Ctrl+O to toggle gocui.SupportOverlap\n") + fmt.Fprintln(v, "\033[32;2mSelected frame is highlighted with green color\033[0m") + } + if v, err := g.SetView("v3", 0, maxY/2, maxX/2-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "v3" + v.Wrap = true + v.Autoscroll = true + v.FrameColor = gocui.ColorCyan + v.TitleColor = gocui.ColorCyan + v.FrameRunes = []rune{'═', '║', '╔', '╗', '╚', '╝'} + fmt.Fprintln(v, "View with basic frame customization and colored frame and title") + fmt.Fprintln(v, "It's not connected to any view.") + } + if v, err := g.SetView("v4", maxX/2, maxY/2, maxX-1, maxY-1, gocui.LEFT); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "v4" + v.Subtitle = "(editable)" + v.Editable = true + v.TitleColor = gocui.ColorYellow + v.FrameColor = gocui.ColorRed + v.FrameRunes = []rune{'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬'} + fmt.Fprintln(v, "View with fully customized frame and colored title differently.") + fmt.Fprintln(v, "It's connected to v3 with overlay LEFT.\n") + v.SetCursor(0, 3) + } + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +func toggleOverlap(g *gocui.Gui, v *gocui.View) error { + g.SupportOverlaps = !g.SupportOverlaps + return nil +} + +func main() { + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + log.Panicln(err) + } + defer g.Close() + + g.Highlight = true + g.SelFgColor = gocui.ColorGreen + g.SelFrameColor = gocui.ColorGreen + + g.SetManagerFunc(layout) + + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyCtrlO, gocui.ModNone, toggleOverlap); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + + if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { + log.Panicln(err) + } +} diff --git a/_examples/dynamic.go b/_examples/dynamic.go index d73d1178..b1a39d49 100644 --- a/_examples/dynamic.go +++ b/_examples/dynamic.go @@ -29,6 +29,7 @@ func main() { g.Highlight = true g.SelFgColor = gocui.ColorRed + g.SelFrameColor = gocui.ColorRed g.SetManagerFunc(layout) diff --git a/_examples/hello.go b/_examples/hello.go index 8e30bbf8..7649efcf 100644 --- a/_examples/hello.go +++ b/_examples/hello.go @@ -25,7 +25,7 @@ func main() { } if err := g.MainLoop(); err != nil && !gocui.IsQuit(err) { - log.Panicln(err.Error()) + log.Panicln(err) } } diff --git a/_examples/mouse.go b/_examples/mouse.go index 7226281d..f82ede75 100644 --- a/_examples/mouse.go +++ b/_examples/mouse.go @@ -72,6 +72,12 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("msg", gocui.MouseLeft, gocui.ModNone, delMsg); err != nil { return err } + if err := g.SetKeybinding("", gocui.MouseRight, gocui.ModNone, delMsg); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.MouseMiddle, gocui.ModNone, delMsg); err != nil { + return err + } return nil } @@ -103,8 +109,7 @@ func showMsg(g *gocui.Gui, v *gocui.View) error { } func delMsg(g *gocui.Gui, v *gocui.View) error { - if err := g.DeleteView("msg"); err != nil { - return err - } + // Error check removed, because delete could be called multiple times with the above keybindings + g.DeleteView("msg") return nil } diff --git a/_examples/widgets.go b/_examples/widgets.go index 331bfc28..aaabaa1b 100644 --- a/_examples/widgets.go +++ b/_examples/widgets.go @@ -121,7 +121,7 @@ func main() { defer g.Close() g.Highlight = true - g.SelFgColor = gocui.ColorRed + g.SelFrameColor = gocui.ColorRed help := NewHelpWidget("help", 1, 1, helpText) status := NewStatusbarWidget("status", 1, 7, 50) diff --git a/attribute.go b/attribute.go index 3d986a71..54e39fc2 100644 --- a/attribute.go +++ b/attribute.go @@ -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/awesome-gocui/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) +) + +// Color attributes. These colors are compatible with tcell.Color type and can be expanded like: +// g.FgColor := gocui.Attribute(tcell.ColorLime) +const ( + ColorBlack Attribute = AttrIsValidColor + iota + ColorRed + ColorGreen + ColorYellow + ColorBlue + ColorMagenta + ColorCyan + ColorWhite ) -// Text style attributes. +// 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 = Attribute(termbox.AttrBold) - AttrUnderline = Attribute(termbox.AttrUnderline) - AttrReverse = Attribute(termbox.AttrReverse) + 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 +} diff --git a/doc.go b/doc.go index ca7113fa..b2f8250b 100644 --- a/doc.go +++ b/doc.go @@ -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 } @@ -37,7 +37,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 v, err := g.SetView("viewname", 2, 2, 22, 7, 0); err != nil { if !gocui.IsUnknownView(err) { // handle error } @@ -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 { // ... } diff --git a/escape.go b/escape.go index c88309b0..d0a439bf 100644 --- a/escape.go +++ b/escape.go @@ -5,8 +5,9 @@ package gocui import ( - "github.com/go-errors/errors" "strconv" + + "github.com/go-errors/errors" ) type escapeInterpreter struct { @@ -17,13 +18,26 @@ type escapeInterpreter struct { mode OutputMode } -type escapeState int +type ( + escapeState int + fontEffect 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 ( @@ -125,6 +139,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 @@ -151,22 +167,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 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 case p == 0: ei.curFgColor = ColorDefault ei.curBgColor = ColorDefault + default: + ei.curFgColor |= getFontEffect(p) } } @@ -191,39 +203,130 @@ func (ei *escapeInterpreter) output256() error { return ei.outputNormal() } - fgbg, err := strconv.Atoi(ei.csiParam[0]) - if err != nil { - return errCSIParseError + 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 + } + + 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;;;m`, `\x1b[48;2;;;m` +func (ei *escapeInterpreter) outputTrue() error { + if len(ei.csiParam) < 5 { + return ei.output256() } - color, err := strconv.Atoi(ei.csiParam[2]) + + mode, err := strconv.Atoi(ei.csiParam[1]) if err != nil { return errCSIParseError } + if mode != 2 { + return ei.output256() + } - switch fgbg { - case 38: - ei.curFgColor = Attribute(color + 1) + 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)) - for _, param := range ei.csiParam[3:] { - p, err := strconv.Atoi(param) - if err != nil { - return errCSIParseError - } + switch fontEffect(fgbg) { + case setForegroundColor: + ei.curFgColor = color + + for _, s := range param[5:] { + p, err := strconv.Atoi(s) + if err != nil { + return errCSIParseError + } - switch { - case p == 1: - ei.curFgColor |= AttrBold - case p == 4: - ei.curFgColor |= AttrUnderline - case p == 7: - ei.curFgColor |= AttrReverse + ei.curFgColor |= getFontEffect(p) } + case setBackgroundColor: + ei.curBgColor = color + default: + return errCSIParseError } - case 48: - ei.curBgColor = Attribute(color + 1) - 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 +} diff --git a/go.mod b/go.mod index 5791b4e4..cc2fd06c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ 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 + github.com/gdamore/tcell/v2 v2.0.0 + github.com/go-errors/errors v1.0.2 + github.com/mattn/go-runewidth v0.0.9 ) diff --git a/go.sum b/go.sum index 25f1c037..2a892bec 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,15 @@ -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= +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= +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= diff --git a/gui.go b/gui.go index 78463ff8..b94b9c7a 100644 --- a/gui.go +++ b/gui.go @@ -8,13 +8,13 @@ import ( standardErrors "errors" "runtime" + "github.com/gdamore/tcell/v2/termbox" "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 +// OutputMode represents an output mode, which determines how colors +// are used. +type OutputMode int var ( // ErrAlreadyBlacklisted is returned when the keybinding is already blacklisted. @@ -38,22 +38,29 @@ var ( 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 + + // Output216 provides 216 ansi color terminal mode. + Output216 - // OutputGrayScale provides greyscale terminal mode. - OutputGrayScale = OutputMode(termbox.OutputGrayscale) + // OutputGrayscale provides greyscale terminal mode. + OutputGrayscale - // Output216 provides greyscale terminal mode. - Output216 = OutputMode(termbox.Output216) + // 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 ) // Gui represents the whole User Interface, including the views, layouts // and keybindings. type Gui struct { - tbEvents chan termbox.Event + gEvents chan gocuiEvent userEvents chan userEvent views []*View currentView *View @@ -97,7 +104,7 @@ type Gui struct { // NewGui returns a new Gui object with a given output mode. func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { - err := termbox.Init() + err := tcellInit() if err != nil { return nil, err } @@ -105,11 +112,10 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { g := &Gui{} g.outputMode = mode - termbox.SetOutputMode(termbox.OutputMode(mode)) g.stop = make(chan struct{}) - g.tbEvents = make(chan termbox.Event, 20) + g.gEvents = make(chan gocuiEvent, 20) g.userEvents = make(chan userEvent, 20) if runtime.GOOS != "windows" { @@ -118,11 +124,11 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { return nil, err } } else { - g.maxX, g.maxY = termbox.Size() + g.maxX, g.maxY = screen.Size() } - g.BgColor, g.FgColor = ColorDefault, ColorDefault - g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault + 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 @@ -137,7 +143,7 @@ func (g *Gui) Close() { go func() { g.stop <- struct{}{} }() - termbox.Close() + screen.Fini() } // Size returns the terminal's size. @@ -152,7 +158,7 @@ func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error { if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY { return errors.New("invalid point") } - termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor)) + tcellSetCell(x, y, ch, fgColor, bgColor, g.outputMode) return nil } @@ -162,8 +168,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) @@ -302,6 +308,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, key interface{}, mod Modifier, handler func(*Gui, *View) error) error { var kb *keybinding @@ -392,7 +403,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. @@ -420,7 +438,7 @@ func (g *Gui) SetManager(managers ...Manager) { g.views = nil g.keybindings = 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 @@ -443,26 +461,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 } @@ -484,7 +497,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 } @@ -500,12 +513,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 { +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 } @@ -513,9 +529,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 { @@ -540,9 +556,17 @@ func (g *Gui) flush() error { bgColor = g.SelBgColor frameColor = g.SelFrameColor } else { - fgColor = g.FgColor bgColor = g.BgColor - frameColor = g.FrameColor + 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, frameColor, bgColor); err != nil { @@ -566,15 +590,28 @@ func (g *Gui) flush() error { return err } } - termbox.Flush() + screen.Show() return nil } +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 w, h +} + // drawFrameEdges draws the horizontal and vertical edges of a view. func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error { runeH, runeV := '─', '│' if g.ASCII { runeH, runeV = '-', '|' + } 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++ { @@ -614,8 +651,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) } @@ -634,6 +727,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) @@ -705,12 +801,41 @@ func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error { func (g *Gui) draw(v *View) error { completed := func(hideCursor bool) error { if hideCursor { - termbox.HideCursor() + screen.HideCursor() } v.clearRunes() return v.draw() } + // if g.Cursor { + // if curview := g.currentView; curview != nil { + // vMaxX, vMaxY := curview.Size() + // if curview.cx < 0 { + // curview.cx = 0 + // } else if curview.cx >= vMaxX { + // curview.cx = vMaxX - 1 + // } + // if curview.cy < 0 { + // curview.cy = 0 + // } else if curview.cy >= vMaxY { + // curview.cy = vMaxY - 1 + // } + + // gMaxX, gMaxY := g.Size() + // cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1 + // // 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 { + // screen.ShowCursor(cx, cy) + // } else { + // screen.HideCursor() + // } + // } + // } else { + // screen.HideCursor() + // } + if !g.Cursor { return completed(true) } @@ -742,9 +867,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 @@ -755,7 +880,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 { @@ -774,7 +899,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 for _, kb := range g.keybindings { @@ -790,7 +915,7 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err return g.execKeybinding(v, kb) } - if kb.viewName == "" && ((v != nil && !v.Editable) || kb.ch == 0) { + if kb.viewName == "" && (((v != nil && !v.Editable) || kb.ch == 0) || v == nil) { globalKb = kb } } diff --git a/keybinding.go b/keybinding.go index d294e70d..5f70dd06 100644 --- a/keybinding.go +++ b/keybinding.go @@ -7,15 +7,15 @@ package gocui import ( "strings" - "github.com/awesome-gocui/termbox-go" + "github.com/gdamore/tcell/v2" ) // Key represents special keys or keys combinations. -type Key termbox.Key +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 termbox.Modifier +type Modifier tcell.ModMask // Keybidings are used to link a given key-press event with a handler. type keybinding struct { @@ -111,7 +111,7 @@ 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 the user is typing in a field, ignore char keys - if v == nil || (v.Editable && kb.ch != 0) { + if v == nil || (v.Editable && kb.ch != 0 && !v.KeybindOnEdit) { return false } return kb.viewName == v.name @@ -197,89 +197,97 @@ var translate = map[string]Key{ // 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 + + // 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) + 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) ) diff --git a/tcell_driver.go b/tcell_driver.go new file mode 100644 index 00000000..cea0da90 --- /dev/null +++ b/tcell_driver.go @@ -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} + } +} diff --git a/view.go b/view.go index f7d1d784..0bd184ba 100644 --- a/view.go +++ b/view.go @@ -12,8 +12,6 @@ import ( "unicode/utf8" "github.com/go-errors/errors" - - "github.com/awesome-gocui/termbox-go" "github.com/mattn/go-runewidth" ) @@ -41,6 +39,7 @@ type View struct { rx, ry int // Read() offsets wx, wy int // Write() offsets lines [][]cell // All the data + outMode OutputMode // readBuffer is used for storing unread bytes readBuffer []byte @@ -88,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. @@ -100,6 +117,9 @@ type View struct { // If Frame is true, Title allows to configure a title for the view. Title string + // 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 @@ -113,6 +133,10 @@ type View struct { // If HasLoader is true, the message will be appended with a spinning loader animation HasLoader 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 + // gui contains the view it's gui gui *Gui } @@ -151,9 +175,14 @@ func (g *Gui) newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { Frame: true, Editor: DefaultEditor, tainted: true, + outMode: mode, ei: newEscapeInterpreter(mode), gui: g, } + + v.FgColor, v.BgColor = ColorDefault, ColorDefault + v.SelFgColor, v.SelBgColor = ColorDefault, ColorDefault + v.TitleColor, v.FrameColor = ColorDefault, ColorDefault return v } @@ -185,8 +214,9 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { fgColor = v.FgColor bgColor = v.BgColor ch = v.Mask - } else if v.Highlight && y == v.cy { - fgColor = fgColor | AttrBold + } else if v.Highlight && ry == rcy { + fgColor = v.SelFgColor | AttrBold + bgColor = v.SelBgColor | AttrBold } // Don't display NUL characters @@ -194,8 +224,7 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { ch = ' ' } - termbox.SetCell(v.x0+x+1, v.y0+y+1, ch, - termbox.Attribute(fgColor), termbox.Attribute(bgColor)) + tcellSetCell(v.x0+x+1, v.y0+y+1, ch, fgColor, bgColor, v.outMode) return nil } @@ -653,8 +682,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) } } }