First pass at removing termbox from common APIs

Start tackling from styles. This commit still should break
the existing application, as there are still instances where
the termbox-native styles are accessed directly via struct field
lookups.

Because we separated colors from attributes, you can no longer
simply refer to Style.fg without merging them aith the attributes
such as bold, underline, etc.

Meanwhile, in order to fix tests we have dicovered the existance
of os.UserHomeDir(), so we're going to use that.
This commit is contained in:
Daisuke Maki 2020-12-21 13:12:03 +09:00
parent 4045ebad41
commit 8075a29013
10 changed files with 300 additions and 205 deletions

120
config.go
View file

@ -6,21 +6,16 @@ import (
"os"
"path/filepath"
"strings"
"strconv"
"github.com/nsf/termbox-go"
"github.com/peco/peco/filter"
"github.com/peco/peco/internal/util"
"github.com/pkg/errors"
)
var homedirFunc = util.Homedir
// NewConfig creates a new Config
func (c *Config) Init() error {
c.Keymap = make(map[string]string)
c.InitialMatcher = IgnoreCaseMatch
c.Style.Init()
c.Style = NewStyleSet()
c.Prompt = "QUERY>"
c.Layout = LayoutTypeTopDown
c.Use256Color = false
@ -64,105 +59,32 @@ func (c *Config) ReadFilename(filename string) error {
return nil
}
var (
stringToFg = map[string]termbox.Attribute{
"default": termbox.ColorDefault,
"black": termbox.ColorBlack,
"red": termbox.ColorRed,
"green": termbox.ColorGreen,
"yellow": termbox.ColorYellow,
"blue": termbox.ColorBlue,
"magenta": termbox.ColorMagenta,
"cyan": termbox.ColorCyan,
"white": termbox.ColorWhite,
}
stringToBg = map[string]termbox.Attribute{
"on_default": termbox.ColorDefault,
"on_black": termbox.ColorBlack,
"on_red": termbox.ColorRed,
"on_green": termbox.ColorGreen,
"on_yellow": termbox.ColorYellow,
"on_blue": termbox.ColorBlue,
"on_magenta": termbox.ColorMagenta,
"on_cyan": termbox.ColorCyan,
"on_white": termbox.ColorWhite,
}
stringToFgAttr = map[string]termbox.Attribute{
"bold": termbox.AttrBold,
"underline": termbox.AttrUnderline,
"reverse": termbox.AttrReverse,
}
stringToBgAttr = map[string]termbox.Attribute{
"on_bold": termbox.AttrBold,
}
)
// NewStyleSet creates a new StyleSet struct
func NewStyleSet() *StyleSet {
ss := &StyleSet{}
ss := &StyleSet{
Basic: NewStyle(),
Query: NewStyle(),
Matched: NewStyle(),
SavedSelection: NewStyle(),
Selected: NewStyle(),
}
ss.Init()
return ss
}
func (ss *StyleSet) Init() {
ss.Basic.fg = termbox.ColorDefault
ss.Basic.bg = termbox.ColorDefault
ss.Query.fg = termbox.ColorDefault
ss.Query.bg = termbox.ColorDefault
ss.Matched.fg = termbox.ColorCyan
ss.Matched.bg = termbox.ColorDefault
ss.SavedSelection.fg = termbox.ColorBlack | termbox.AttrBold
ss.SavedSelection.bg = termbox.ColorCyan
ss.Selected.fg = termbox.ColorDefault | termbox.AttrUnderline
ss.Selected.bg = termbox.ColorMagenta
}
// UnmarshalJSON satisfies json.RawMessage.
func (s *Style) UnmarshalJSON(buf []byte) error {
raw := []string{}
if err := json.Unmarshal(buf, &raw); err != nil {
return errors.Wrapf(err, "failed to unmarshal Style")
}
return stringsToStyle(s, raw)
}
func stringsToStyle(style *Style, raw []string) error {
style.fg = termbox.ColorDefault
style.bg = termbox.ColorDefault
for _, s := range raw {
fg, ok := stringToFg[s]
if ok {
style.fg = fg
} else {
if fg, err := strconv.ParseUint(s, 10, 8); err == nil {
style.fg = termbox.Attribute(fg+1)
}
}
bg, ok := stringToBg[s]
if ok {
style.bg = bg
} else {
if strings.HasPrefix(s, "on_") {
if bg, err := strconv.ParseUint(s[3:], 10, 8); err == nil {
style.bg = termbox.Attribute(bg+1)
}
}
}
}
for _, s := range raw {
if fgAttr, ok := stringToFgAttr[s]; ok {
style.fg |= fgAttr
}
if bgAttr, ok := stringToBgAttr[s]; ok {
style.bg |= bgAttr
}
}
return nil
ss.Basic.Reset()
ss.Query.Reset()
ss.Matched.Reset().
Foreground(ColorCyan)
ss.SavedSelection.Reset().
Foreground(ColorBlack).
Background(ColorCyan).
Bold(true)
ss.Selected.Reset().
Foreground(ColorDefault).
Background(ColorMagenta).
Underline(true)
}
// This is a variable because we want to change its behavior
@ -187,7 +109,7 @@ func LocateRcfile(locater configLocateFunc) (string, error) {
// $XDG_CONFIG_DIR/peco/config.json (where XDG_CONFIG_DIR is listed in $XDG_CONFIG_DIRS)
// ~/.peco/config.json
home, uErr := homedirFunc()
home, uErr := os.UserHomeDir()
// Try dir supplied via env var
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {

View file

@ -1,4 +1,4 @@
package peco
package peco_test
import (
"encoding/json"
@ -9,7 +9,8 @@ import (
"strings"
"testing"
"github.com/nsf/termbox-go"
"github.com/lestrrat-go/envload"
"github.com/peco/peco"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -30,7 +31,7 @@ func TestReadRC(t *testing.T) {
"Prompt": "[peco]"
}
`
var cfg Config
var cfg peco.Config
if !assert.NoError(t, cfg.Init(), "Config.Init should succeed") {
return
}
@ -39,31 +40,32 @@ func TestReadRC(t *testing.T) {
return
}
expected := Config{
expected := peco.Config{
Keymap: map[string]string{
"C-j": "peco.Finish",
"C-x,C-c": "peco.Finish",
},
InitialMatcher: IgnoreCaseMatch,
Layout: DefaultLayoutType,
InitialMatcher: peco.IgnoreCaseMatch,
Layout: peco.DefaultLayoutType,
Prompt: "[peco]",
Style: StyleSet{
Matched: Style{
fg: termbox.ColorCyan | termbox.AttrBold,
bg: termbox.ColorRed,
},
Query: Style{
fg: termbox.ColorYellow | termbox.AttrBold,
bg: termbox.ColorDefault,
},
Selected: Style{
fg: termbox.ColorBlack | termbox.AttrUnderline,
bg: termbox.ColorCyan,
},
SavedSelection: Style{
fg: termbox.ColorBlack | termbox.AttrBold,
bg: termbox.ColorCyan,
},
Style: &peco.StyleSet{
Basic: peco.NewStyle(),
Matched: peco.NewStyle().
Foreground(peco.ColorCyan).
Background(peco.ColorRed).
Bold(true),
Query: peco.NewStyle().
Foreground(peco.ColorYellow).
Background(peco.ColorDefault).
Bold(true),
Selected: peco.NewStyle().
Foreground(peco.ColorBlack).
Background(peco.ColorCyan).
Underline(true),
SavedSelection: peco.NewStyle().
Foreground(peco.ColorBlack).
Background(peco.ColorCyan).
Bold(true),
},
}
@ -74,48 +76,66 @@ func TestReadRC(t *testing.T) {
type stringsToStyleTest struct {
strings []string
style *Style
style *peco.Style
}
func TestStringsToStyle(t *testing.T) {
tests := []stringsToStyleTest{
stringsToStyleTest{
strings: []string{"on_default", "default"},
style: &Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
style: peco.NewStyle().
Foreground(peco.ColorDefault).
Background(peco.ColorDefault),
},
stringsToStyleTest{
strings: []string{"bold", "on_blue", "yellow"},
style: &Style{fg: termbox.ColorYellow | termbox.AttrBold, bg: termbox.ColorBlue},
style: peco.NewStyle().
Foreground(peco.ColorYellow).
Background(peco.ColorBlue).
Bold(true),
},
stringsToStyleTest{
strings: []string{"underline", "on_cyan", "black"},
style: &Style{fg: termbox.ColorBlack | termbox.AttrUnderline, bg: termbox.ColorCyan},
style: peco.NewStyle().
Foreground(peco.ColorBlack).
Background(peco.ColorCyan).
Underline(true),
},
stringsToStyleTest{
strings: []string{"reverse", "on_red", "white"},
style: &Style{fg: termbox.ColorWhite | termbox.AttrReverse, bg: termbox.ColorRed},
style: peco.NewStyle().
Foreground(peco.ColorWhite).
Background(peco.ColorRed).
Reverse(true),
},
stringsToStyleTest{
strings: []string{"on_bold", "on_magenta", "green"},
style: &Style{fg: termbox.ColorGreen, bg: termbox.ColorMagenta | termbox.AttrBold},
style: peco.NewStyle().
Foreground(peco.ColorGreen).
Background(peco.ColorMagenta).
Bold(true),
},
stringsToStyleTest{
strings: []string{"underline", "on_240", "214"},
style: &Style{fg: (214+1) | termbox.AttrUnderline, bg: 240+1},
style: peco.NewStyle().
Foreground(214 + 1).
Background(240 + 1).
Underline(true),
},
}
t.Logf("Checking strings -> color mapping...")
var a Style
for _, test := range tests {
t.Logf(" checking %s...", test.strings)
if !assert.NoError(t, stringsToStyle(&a, test.strings), "stringsToStyle should succeed") {
return
}
var a peco.Style
for _, tc := range tests {
tc := tc
t.Run(strings.Join(tc.strings, ","), func(t *testing.T) {
if !assert.NoError(t, a.FromStrings(tc.strings...), "stringsToStyle should succeed") {
return
}
if !assert.Equal(t, test.style, &a, "Expected '%s' to be '%#v', but got '%#v'", test.strings, test.style, a) {
return
}
if !assert.Equal(t, tc.style, &a, "Expected '%s' to be '%#v', but got '%#v'", tc.strings, tc.style, a) {
return
}
})
}
}
@ -125,16 +145,20 @@ func TestLocateRcfile(t *testing.T) {
return
}
homedirFunc = func() (string, error) {
return dir, nil
home, err := os.UserHomeDir()
if !assert.NoError(t, err, `could not find user home directory`) {
return
}
el := envload.New()
defer el.Restore()
expected := []string{
filepath.Join(dir, "peco"),
filepath.Join(dir, "1", "peco"),
filepath.Join(dir, "2", "peco"),
filepath.Join(dir, "3", "peco"),
filepath.Join(dir, ".peco"),
filepath.Join(home, ".peco"),
}
i := 0
@ -161,10 +185,10 @@ func TestLocateRcfile(t *testing.T) {
fmt.Sprintf("%c", filepath.ListSeparator),
))
LocateRcfile(locater)
expected[0] = filepath.Join(dir, ".config", "peco")
peco.LocateRcfile(locater)
expected[0] = filepath.Join(home, ".config", "peco")
os.Setenv("XDG_CONFIG_HOME", "")
i = 0
LocateRcfile(locater)
peco.LocateRcfile(locater)
}

7
go.mod
View file

@ -4,11 +4,14 @@ go 1.12
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/gdamore/tcell v1.4.0
github.com/gdamore/tcell/v2 v2.1.0
github.com/google/btree v0.0.0-20161213163243-0c3044bc8bad
github.com/jessevdk/go-flags v1.1.0
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc
github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe
github.com/mattn/go-runewidth v0.0.0-20161012013512-737072b4e32b
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317
github.com/mattn/go-runewidth v0.0.9
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00
github.com/pkg/errors v0.0.0-20161029093637-248dadf4e906
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312

View file

@ -102,7 +102,7 @@ type Peco struct {
singleKeyJumpPrefixMap map[rune]uint
singleKeyJumpShowPrefix bool
skipReadConfig bool
styles StyleSet
styles *StyleSet
use256Color bool
// Source is where we buffer input. It gets reused when a new query is
@ -289,7 +289,7 @@ type Config struct {
Matcher string `json:"Matcher"` // Deprecated.
InitialMatcher string `json:"InitialMatcher"` // Use this instead of Matcher
InitialFilter string `json:"InitialFilter"`
Style StyleSet `json:"Style"`
Style *StyleSet `json:"Style"`
Prompt string `json:"Prompt"`
Layout string `json:"Layout"`
Use256Color bool `json:"Use256Color"`
@ -333,17 +333,11 @@ type CustomFilterConfig struct {
// StyleSet holds styles for various sections
type StyleSet struct {
Basic Style `json:"Basic"`
SavedSelection Style `json:"SavedSelection"`
Selected Style `json:"Selected"`
Query Style `json:"Query"`
Matched Style `json:"Matched"`
}
// Style describes termbox styles
type Style struct {
fg termbox.Attribute
bg termbox.Attribute
Basic *Style `json:"Basic"`
SavedSelection *Style `json:"SavedSelection"`
Selected *Style `json:"Selected"`
Query *Style `json:"Query"`
Matched *Style `json:"Matched"`
}
type Caret struct {

View file

@ -1,15 +0,0 @@
package util
import (
"errors"
"os"
)
func Homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", errors.New("environment variable HOME not set")
}
return home, nil
}

View file

@ -1,17 +0,0 @@
// +build !darwin,!windows
package util
import (
"errors"
"os"
)
func Homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", errors.New("error: Environment variable HOME not set")
}
return home, nil
}

View file

@ -1,11 +0,0 @@
package util
import "os/user"
func Homedir() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
return u.HomeDir, nil
}

View file

@ -35,7 +35,7 @@ func TestIssue212_SanityCheck(t *testing.T) {
return
}
defstyle := StyleSet{}
defstyle := NewStyleSet()
defstyle.Init()
if !assert.Equal(t, state.config.Style, defstyle, "should be default style") {
return

View file

@ -134,7 +134,7 @@ func (p *Peco) Screen() Screen {
}
func (p *Peco) Styles() *StyleSet {
return &p.styles
return p.styles
}
func (p *Peco) Use256Color() bool {

195
style.go Normal file
View file

@ -0,0 +1,195 @@
package peco
import (
"encoding/json"
"strconv"
"strings"
"sync"
"github.com/nsf/termbox-go"
"github.com/pkg/errors"
)
// Color represents a color. In termbox terms, colors are part of attributes
// so there is really no point in having a different type, but we do this
// so that the API is easier to understand
type Color = termbox.Attribute
const (
ColorDefault = termbox.ColorDefault
ColorBlack = termbox.ColorBlack
ColorRed = termbox.ColorRed
ColorGreen = termbox.ColorGreen
ColorYellow = termbox.ColorYellow
ColorBlue = termbox.ColorBlue
ColorMagenta = termbox.ColorMagenta
ColorCyan = termbox.ColorCyan
ColorWhite = termbox.ColorWhite
ColorDarkGray = termbox.ColorDarkGray
ColorLightRed = termbox.ColorLightRed
ColorLightGreen = termbox.ColorLightGreen
ColorLightYellow = termbox.ColorLightYellow
ColorLightBlue = termbox.ColorLightBlue
ColorLightMagenta = termbox.ColorLightMagenta
ColorLightCyan = termbox.ColorLightCyan
ColorLightGray = termbox.ColorLightGray
)
// Style represents the set of styles to be applied when printing to the
// terminal.
type Style struct {
// TODO: separate tcell/termbox versions so we can hide termbox from the
// rest of the peco codebase
bg termbox.Attribute
fg termbox.Attribute
attrs termbox.Attribute
}
var (
stringToFg = map[string]termbox.Attribute{
"default": termbox.ColorDefault,
"black": termbox.ColorBlack,
"red": termbox.ColorRed,
"green": termbox.ColorGreen,
"yellow": termbox.ColorYellow,
"blue": termbox.ColorBlue,
"magenta": termbox.ColorMagenta,
"cyan": termbox.ColorCyan,
"white": termbox.ColorWhite,
}
stringToBg = map[string]termbox.Attribute{
"on_default": termbox.ColorDefault,
"on_black": termbox.ColorBlack,
"on_red": termbox.ColorRed,
"on_green": termbox.ColorGreen,
"on_yellow": termbox.ColorYellow,
"on_blue": termbox.ColorBlue,
"on_magenta": termbox.ColorMagenta,
"on_cyan": termbox.ColorCyan,
"on_white": termbox.ColorWhite,
}
stringToFgAttr = map[string]termbox.Attribute{
"bold": termbox.AttrBold,
"underline": termbox.AttrUnderline,
"reverse": termbox.AttrReverse,
}
stringToBgAttr = map[string]termbox.Attribute{
"on_bold": termbox.AttrBold,
}
)
var stylePool = sync.Pool{
New: func() interface{} { return NewStyle() },
}
func NewStyle() *Style {
return &Style{}
}
func FetchStyle() *Style {
return stylePool.Get().(*Style)
}
func (s *Style) Release() {
s.Reset()
stylePool.Put(s)
}
func (s *Style) Reset() *Style {
s.bg = 0
s.fg = 0
s.attrs = 0
return s
}
func (s *Style) Foreground(c Color) *Style {
s.fg = c
return s
}
func (s *Style) Background(c Color) *Style {
s.bg = c
return s
}
func (s *Style) setAttr(attr termbox.Attribute, on bool) *Style {
if on {
s.attrs |= attr
} else {
s.attrs &^= attr
}
return s
}
func (s *Style) Bold(v bool) *Style {
return s.setAttr(termbox.AttrBold, v)
}
func (s *Style) Blink(v bool) *Style {
return s.setAttr(termbox.AttrBlink, v)
}
func (s *Style) Hidden(v bool) *Style {
return s.setAttr(termbox.AttrHidden, v)
}
func (s *Style) Dim(v bool) *Style {
return s.setAttr(termbox.AttrDim, v)
}
func (s *Style) Underline(v bool) *Style {
return s.setAttr(termbox.AttrUnderline, v)
}
func (s *Style) Cursive(v bool) *Style {
return s.setAttr(termbox.AttrCursive, v)
}
func (s *Style) Reverse(v bool) *Style {
return s.setAttr(termbox.AttrReverse, v)
}
// UnmarshalJSON satisfies json.RawMessage.
func (s *Style) UnmarshalJSON(buf []byte) error {
raw := []string{}
if err := json.Unmarshal(buf, &raw); err != nil {
return errors.Wrapf(err, "failed to unmarshal Style")
}
return s.FromStrings(raw...)
}
func (style *Style) FromStrings(raw ...string) error {
style.Reset()
for _, s := range raw {
fg, ok := stringToFg[s]
if ok {
style.Foreground(fg)
} else {
if fg, err := strconv.ParseUint(s, 10, 8); err == nil {
style.Foreground(termbox.Attribute(fg + 1))
}
}
bg, ok := stringToBg[s]
if ok {
style.Background(bg)
} else {
if strings.HasPrefix(s, "on_") {
if bg, err := strconv.ParseUint(s[3:], 10, 8); err == nil {
style.Background(termbox.Attribute(bg + 1))
}
}
}
}
for _, s := range raw {
if fgAttr, ok := stringToFgAttr[s]; ok {
style.setAttr(fgAttr, true)
} else if bgAttr, ok := stringToBgAttr[s]; ok {
style.setAttr(bgAttr, true)
}
}
return nil
}