From 2071fd15bbae8dc7abc8d6a7d7b6e8dee3e3311f Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 13 Jan 2019 21:53:26 -0500 Subject: [PATCH] Support for 256 colors. - Documenting color modes. - Providing helper functions to set colors in different formats. - Allowing users to set other than the 8 system colors. Fixes #66 --- cell/color.go | 54 ++++++- cell/color_test.go | 203 ++++++++++++++++++++++++++ terminal/termbox/cell_options.go | 31 +--- terminal/termbox/cell_options_test.go | 50 +++++++ terminal/termbox/color_mode.go | 2 +- terminal/termbox/termbox.go | 22 +-- terminalapi/color_mode.go | 20 ++- 7 files changed, 331 insertions(+), 51 deletions(-) create mode 100644 cell/color_test.go create mode 100644 terminal/termbox/cell_options_test.go diff --git a/cell/color.go b/cell/color.go index 9e1e04c..94560b7 100644 --- a/cell/color.go +++ b/cell/color.go @@ -14,6 +14,10 @@ package cell +import ( + "fmt" +) + // color.go defines constants for cell colors. // Color is the color of a cell. @@ -24,7 +28,7 @@ func (cc Color) String() string { if n, ok := colorNames[cc]; ok { return n } - return "ColorUnknown" + return fmt.Sprintf("Color:%d", cc) } // colorNames maps Color values to human readable names. @@ -43,6 +47,8 @@ var colorNames = map[Color]string{ // The supported terminal colors. const ( ColorDefault Color = iota + + // 8 "system" colors. ColorBlack ColorRed ColorGreen @@ -52,3 +58,49 @@ const ( ColorCyan ColorWhite ) + +// ColorNumber sets a color using its number. +// Make sure your terminal is set to a terminalapi.ColorMode that supports the +// target color. The provided value must be in the range 0-255. +// Larger or smaller values will be reset to the default color. +// +// For reference on these colors see the Xterm number in: +// https://jonasjacek.github.io/colors/ +func ColorNumber(n int) Color { + if n < 0 || n > 255 { + return ColorDefault + } + return Color(n + 1) // Colors are off-by-one due to ColorDefault being zero. +} + +// ColorRGB6 sets a color using the 6x6x6 terminal color. +// Make sure your terminal is set to the terminalapi.ColorMode256 mode. +// The provided values (r, g, b) must be in the range 0-5. +// Larger or smaller values will be reset to the default color. +// +// For reference on these colors see: +// https://superuser.com/questions/783656/whats-the-deal-with-terminal-colors +func ColorRGB6(r, g, b int) Color { + for _, c := range []int{r, g, b} { + if c < 0 || c > 5 { + return ColorDefault + } + } + return Color(0x10 + 36*r + 6*g + b + 1) // Colors are off-by-one due to ColorDefault being zero. +} + +// ColorRGB24 sets a color using the 24 bit web color scheme. +// Make sure your terminal is set to the terminalapi.ColorMode256 mode. +// The provided values (r, g, b) must be in the range 0-255. +// Larger or smaller values will be reset to the default color. +// +// For reference on these colors see the RGB column in: +// https://jonasjacek.github.io/colors/ +func ColorRGB24(r, g, b int) Color { + for _, c := range []int{r, g, b} { + if c < 0 || c > 255 { + return ColorDefault + } + } + return ColorRGB6(r/51, g/51, b/51) +} diff --git a/cell/color_test.go b/cell/color_test.go new file mode 100644 index 0000000..edc68d1 --- /dev/null +++ b/cell/color_test.go @@ -0,0 +1,203 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cell + +import "testing" + +func TestColorNumber(t *testing.T) { + tests := []struct { + desc string + number int + want Color + }{ + { + desc: "default when too small", + number: -1, + want: ColorDefault, + }, + { + desc: "default when too large", + number: 256, + want: ColorDefault, + }, + { + desc: "translates system color", + number: 0, + want: ColorBlack, + }, + { + desc: "adds one to the value", + number: 42, + want: Color(43), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got := ColorNumber(tc.number) + if got != tc.want { + t.Errorf("ColorNumber(%v) => %v, want %v", tc.number, got, tc.want) + } + }) + } +} + +func TestColorRGB6(t *testing.T) { + tests := []struct { + desc string + r, g, b int + want Color + }{ + { + desc: "default when r too small", + r: -1, + g: 0, + b: 0, + want: ColorDefault, + }, + { + desc: "default when r too large", + r: 6, + g: 0, + b: 0, + want: ColorDefault, + }, + { + desc: "default when g too small", + r: 0, + g: -1, + b: 0, + want: ColorDefault, + }, + { + desc: "default when g too large", + r: 0, + g: 6, + b: 0, + want: ColorDefault, + }, + { + desc: "default when b too small", + r: 0, + g: 0, + b: -1, + want: ColorDefault, + }, + { + desc: "default when b too large", + r: 0, + g: 0, + b: 6, + want: ColorDefault, + }, + { + desc: "translates black", + r: 0, + g: 0, + b: 0, + want: Color(17), + }, + { + desc: "adds one to value", + r: 2, + g: 1, + b: 3, + want: Color(98), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got := ColorRGB6(tc.r, tc.g, tc.b) + if got != tc.want { + t.Errorf("ColorRGB6(%v, %v, %v) => %v, want %v", tc.r, tc.g, tc.b, got, tc.want) + } + }) + } +} + +func TestColorRGB24(t *testing.T) { + tests := []struct { + desc string + r, g, b int + want Color + }{ + { + desc: "default when r too small", + r: -1, + g: 0, + b: 0, + want: ColorDefault, + }, + { + desc: "default when r too large", + r: 256, + g: 0, + b: 0, + want: ColorDefault, + }, + { + desc: "default when g too small", + r: 0, + g: -1, + b: 0, + want: ColorDefault, + }, + { + desc: "default when g too large", + r: 0, + g: 256, + b: 0, + want: ColorDefault, + }, + { + desc: "default when b too small", + r: 0, + g: 0, + b: -1, + want: ColorDefault, + }, + { + desc: "default when b too large", + r: 0, + g: 0, + b: 256, + want: ColorDefault, + }, + { + desc: "translates black", + r: 0, + g: 0, + b: 0, + want: Color(17), + }, + { + desc: "adds one to value", + r: 95, + g: 255, + b: 135, + want: Color(85), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got := ColorRGB24(tc.r, tc.g, tc.b) + if got != tc.want { + t.Errorf("ColorRGB24(%v, %v, %v) => %v, want %v", tc.r, tc.g, tc.b, got, tc.want) + } + }) + } +} diff --git a/terminal/termbox/cell_options.go b/terminal/termbox/cell_options.go index 4e32b91..41ee760 100644 --- a/terminal/termbox/cell_options.go +++ b/terminal/termbox/cell_options.go @@ -17,44 +17,21 @@ package termbox // cell_options.go converts termdash cell options to the termbox format. import ( - "fmt" - "github.com/mum4k/termdash/cell" tbx "github.com/nsf/termbox-go" ) // cellColor converts termdash cell color to the termbox format. -func cellColor(c cell.Color) (tbx.Attribute, error) { - switch c { - case cell.ColorDefault: - return tbx.ColorDefault, nil - case cell.ColorBlack: - return tbx.ColorBlack, nil - case cell.ColorRed: - return tbx.ColorRed, nil - case cell.ColorGreen: - return tbx.ColorGreen, nil - case cell.ColorYellow: - return tbx.ColorYellow, nil - case cell.ColorBlue: - return tbx.ColorBlue, nil - case cell.ColorMagenta: - return tbx.ColorMagenta, nil - case cell.ColorCyan: - return tbx.ColorCyan, nil - case cell.ColorWhite: - return tbx.ColorWhite, nil - default: - return 0, fmt.Errorf("don't know how to convert cell color %v to the termbox format", c) - } +func cellColor(c cell.Color) tbx.Attribute { + return tbx.Attribute(c) } // cellOptsToFg converts the cell options to the termbox foreground attribute. -func cellOptsToFg(opts *cell.Options) (tbx.Attribute, error) { +func cellOptsToFg(opts *cell.Options) tbx.Attribute { return cellColor(opts.FgColor) } // cellOptsToBg converts the cell options to the termbox background attribute. -func cellOptsToBg(opts *cell.Options) (tbx.Attribute, error) { +func cellOptsToBg(opts *cell.Options) tbx.Attribute { return cellColor(opts.BgColor) } diff --git a/terminal/termbox/cell_options_test.go b/terminal/termbox/cell_options_test.go new file mode 100644 index 0000000..f74e9bb --- /dev/null +++ b/terminal/termbox/cell_options_test.go @@ -0,0 +1,50 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package termbox + +import ( + "testing" + + "github.com/mum4k/termdash/cell" + tbx "github.com/nsf/termbox-go" +) + +func TestCellColor(t *testing.T) { + tests := []struct { + color cell.Color + want tbx.Attribute + }{ + {cell.ColorDefault, tbx.ColorDefault}, + {cell.ColorBlack, tbx.ColorBlack}, + {cell.ColorRed, tbx.ColorRed}, + {cell.ColorGreen, tbx.ColorGreen}, + {cell.ColorYellow, tbx.ColorYellow}, + {cell.ColorBlue, tbx.ColorBlue}, + {cell.ColorMagenta, tbx.ColorMagenta}, + {cell.ColorCyan, tbx.ColorCyan}, + {cell.ColorWhite, tbx.ColorWhite}, + {cell.Color(42), tbx.Attribute(42)}, + } + + for _, tc := range tests { + t.Run(tc.color.String(), func(t *testing.T) { + got := cellColor(tc.color) + if got != tc.want { + t.Errorf("cellColor(%v) => got %v, want %v", tc.color, got, tc.want) + } + + }) + } +} diff --git a/terminal/termbox/color_mode.go b/terminal/termbox/color_mode.go index 61627da..0e42ee7 100644 --- a/terminal/termbox/color_mode.go +++ b/terminal/termbox/color_mode.go @@ -24,7 +24,7 @@ import ( // colorMode converts termdash color modes to the termbox format. func colorMode(cm terminalapi.ColorMode) (tbx.OutputMode, error) { switch cm { - case terminalapi.ColorMode8: + case terminalapi.ColorModeNormal: return tbx.OutputNormal, nil case terminalapi.ColorMode256: return tbx.Output256, nil diff --git a/terminal/termbox/termbox.go b/terminal/termbox/termbox.go index 29a5e56..70d232c 100644 --- a/terminal/termbox/termbox.go +++ b/terminal/termbox/termbox.go @@ -95,16 +95,7 @@ func (t *Terminal) Size() image.Point { // Clear implements terminalapi.Terminal.Clear. func (t *Terminal) Clear(opts ...cell.Option) error { o := cell.NewOptions(opts...) - fg, err := cellOptsToFg(o) - if err != nil { - return err - } - - bg, err := cellOptsToBg(o) - if err != nil { - return err - } - return tbx.Clear(fg, bg) + return tbx.Clear(cellOptsToFg(o), cellOptsToBg(o)) } // Flush implements terminalapi.Terminal.Flush. @@ -125,16 +116,7 @@ func (t *Terminal) HideCursor() { // SetCell implements terminalapi.Terminal.SetCell. func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error { o := cell.NewOptions(opts...) - fg, err := cellOptsToFg(o) - if err != nil { - return err - } - - bg, err := cellOptsToBg(o) - if err != nil { - return err - } - tbx.SetCell(p.X, p.Y, r, fg, bg) + tbx.SetCell(p.X, p.Y, r, cellOptsToFg(o), cellOptsToBg(o)) return nil } diff --git a/terminalapi/color_mode.go b/terminalapi/color_mode.go index d31ce43..d7f2feb 100644 --- a/terminalapi/color_mode.go +++ b/terminalapi/color_mode.go @@ -29,7 +29,7 @@ func (cm ColorMode) String() string { // colorModeNames maps ColorMode values to human readable names. var colorModeNames = map[ColorMode]string{ - ColorMode8: "ColorMode8", + ColorModeNormal: "ColorModeNormal", ColorMode256: "ColorMode256", ColorMode216: "ColorMode216", ColorModeGrayscale: "ColorModeGrayscale", @@ -37,8 +37,24 @@ var colorModeNames = map[ColorMode]string{ // Supported color modes. const ( - ColorMode8 ColorMode = iota + // ColorModeNormal supports 8 "system" colors. + // These are defined as constants in the cell package. + ColorModeNormal ColorMode = iota + + // ColorMode256 enables using any of the 256 terminal colors. + // 0-7: the 8 "system" colors accessible in ColorModeNormal. + // 8-15: the 8 "bright system" colors. + // 16-231: the 216 different terminal colors. + // 232-255: the 24 different shades of grey. ColorMode256 + + // ColorMode216 supports only the third range of the ColorMode256, i.e the + // 216 different terminal colors. However in this mode the colors are zero + // based, so the called doesn't need to provide an offset. ColorMode216 + + // ColorModeGrayscale supports only the fourth range of the ColorMode256, + // i.e the 24 different shades of grey. However in this mode the colors are + // zero based, so the called doesn't need to provide an offset. ColorModeGrayscale )