From 8075a290131b189dd74b634ca40883e5b923c500 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Mon, 21 Dec 2020 13:12:03 +0900 Subject: [PATCH] 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. --- config.go | 120 ++++--------------- config_test.go | 116 ++++++++++-------- go.mod | 7 +- interface.go | 20 ++-- internal/util/homedir_darwin.go | 15 --- internal/util/homedir_posix.go | 17 --- internal/util/homedir_windows.go | 11 -- issues_test.go | 2 +- peco.go | 2 +- style.go | 195 +++++++++++++++++++++++++++++++ 10 files changed, 300 insertions(+), 205 deletions(-) delete mode 100644 internal/util/homedir_darwin.go delete mode 100644 internal/util/homedir_posix.go delete mode 100644 internal/util/homedir_windows.go create mode 100644 style.go diff --git a/config.go b/config.go index 423c881..5a025c5 100644 --- a/config.go +++ b/config.go @@ -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 != "" { diff --git a/config_test.go b/config_test.go index 15887fd..3cd2b5c 100644 --- a/config_test.go +++ b/config_test.go @@ -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) } diff --git a/go.mod b/go.mod index 9720af5..593eae5 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/interface.go b/interface.go index 3ae9515..fa22a89 100644 --- a/interface.go +++ b/interface.go @@ -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 { diff --git a/internal/util/homedir_darwin.go b/internal/util/homedir_darwin.go deleted file mode 100644 index e44256f..0000000 --- a/internal/util/homedir_darwin.go +++ /dev/null @@ -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 -} diff --git a/internal/util/homedir_posix.go b/internal/util/homedir_posix.go deleted file mode 100644 index 543d091..0000000 --- a/internal/util/homedir_posix.go +++ /dev/null @@ -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 -} diff --git a/internal/util/homedir_windows.go b/internal/util/homedir_windows.go deleted file mode 100644 index 168a1d3..0000000 --- a/internal/util/homedir_windows.go +++ /dev/null @@ -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 -} diff --git a/issues_test.go b/issues_test.go index a038d0d..8d59d98 100644 --- a/issues_test.go +++ b/issues_test.go @@ -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 diff --git a/peco.go b/peco.go index 026c2ae..dd77c36 100644 --- a/peco.go +++ b/peco.go @@ -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 { diff --git a/style.go b/style.go new file mode 100644 index 0000000..8aa55f1 --- /dev/null +++ b/style.go @@ -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 +}