mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
extract config package
This commit is contained in:
parent
7b54585c95
commit
11754d722b
|
|
@ -12,6 +12,7 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/lestrrat-go/pdebug"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/internal/keyseq"
|
||||
"github.com/peco/peco/internal/util"
|
||||
|
|
@ -444,7 +445,7 @@ func doCancel(ctx context.Context, state *Peco, e Event) {
|
|||
|
||||
// peco.Cancel -> end program, exit with failure
|
||||
err := makeIgnorable(errors.New("user canceled"))
|
||||
if state.onCancel == OnCancelError {
|
||||
if state.onCancel == config.OnCancelError {
|
||||
err = setExitStatus(err, 1)
|
||||
}
|
||||
state.Exit(err)
|
||||
|
|
@ -461,7 +462,7 @@ func batchAction(ctx context.Context, state *Peco, fn func(context.Context)) {
|
|||
func doToggleSelectionAndSelectNext(ctx context.Context, state *Peco, e Event) {
|
||||
batchAction(ctx, state, func(ctx context.Context) {
|
||||
doToggleSelection(ctx, state, e)
|
||||
if state.LayoutType() != LayoutTypeBottomUp {
|
||||
if state.LayoutType() != config.LayoutTypeBottomUp {
|
||||
state.Hub().SendPaging(ctx, hub.ToLineBelow)
|
||||
} else {
|
||||
state.Hub().SendPaging(ctx, hub.ToLineAbove)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package peco
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
|
|
@ -93,61 +92,11 @@ type CustomFilterConfig struct {
|
|||
BufferThreshold int `json:"BufferThreshold" yaml:"BufferThreshold"`
|
||||
}
|
||||
|
||||
// StyleSet holds styles for various sections
|
||||
type StyleSet struct {
|
||||
Basic Style `json:"Basic" yaml:"Basic"`
|
||||
SavedSelection Style `json:"SavedSelection" yaml:"SavedSelection"`
|
||||
Selected Style `json:"Selected" yaml:"Selected"`
|
||||
Query Style `json:"Query" yaml:"Query"`
|
||||
QueryCursor Style `json:"QueryCursor" yaml:"QueryCursor"`
|
||||
Matched Style `json:"Matched" yaml:"Matched"`
|
||||
Prompt Style `json:"Prompt" yaml:"Prompt"`
|
||||
Context Style `json:"Context" yaml:"Context"`
|
||||
}
|
||||
|
||||
// Attribute represents terminal display attributes such as colors
|
||||
// and text styling (bold, underline, reverse). It is a uint32 bitfield:
|
||||
//
|
||||
// Bits 0-8: Palette color index (0=default, 1-256 for 256-color palette)
|
||||
// Bits 0-23: RGB color value (when AttrTrueColor flag is set)
|
||||
// Bit 24: AttrTrueColor flag — distinguishes true color from palette
|
||||
// Bit 25: AttrBold
|
||||
// Bit 26: AttrUnderline
|
||||
// Bit 27: AttrReverse
|
||||
// Bits 28-31: Reserved
|
||||
type Attribute uint32
|
||||
|
||||
// Named palette color constants (values 0-8).
|
||||
const (
|
||||
ColorDefault Attribute = 0x0000
|
||||
ColorBlack Attribute = 0x0001
|
||||
ColorRed Attribute = 0x0002
|
||||
ColorGreen Attribute = 0x0003
|
||||
ColorYellow Attribute = 0x0004
|
||||
ColorBlue Attribute = 0x0005
|
||||
ColorMagenta Attribute = 0x0006
|
||||
ColorCyan Attribute = 0x0007
|
||||
ColorWhite Attribute = 0x0008
|
||||
)
|
||||
|
||||
const (
|
||||
AttrTrueColor Attribute = 0x01000000
|
||||
AttrBold Attribute = 0x02000000
|
||||
AttrUnderline Attribute = 0x04000000
|
||||
AttrReverse Attribute = 0x08000000
|
||||
)
|
||||
|
||||
// Style describes display attributes for foreground and background.
|
||||
type Style struct {
|
||||
fg Attribute
|
||||
bg Attribute
|
||||
}
|
||||
|
||||
var homedirFunc = util.Homedir
|
||||
|
||||
// DefaultPrompt is the default prompt string shown in the query line.
|
||||
const DefaultPrompt = "QUERY>"
|
||||
|
||||
var homedirFunc = util.Homedir
|
||||
|
||||
// Init initializes the Config with default values
|
||||
func (c *Config) Init() error {
|
||||
c.Keymap = make(map[string]string)
|
||||
|
|
@ -179,157 +128,31 @@ func (c *Config) ReadFilename(filename string) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !IsValidLayoutType(LayoutType(c.Layout)) {
|
||||
if !IsValidLayoutType(c.Layout) {
|
||||
return fmt.Errorf("invalid layout type: %s", c.Layout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
stringToFg = map[string]Attribute{
|
||||
"default": ColorDefault,
|
||||
"black": ColorBlack,
|
||||
"red": ColorRed,
|
||||
"green": ColorGreen,
|
||||
"yellow": ColorYellow,
|
||||
"blue": ColorBlue,
|
||||
"magenta": ColorMagenta,
|
||||
"cyan": ColorCyan,
|
||||
"white": ColorWhite,
|
||||
}
|
||||
stringToBg = map[string]Attribute{
|
||||
"on_default": ColorDefault,
|
||||
"on_black": ColorBlack,
|
||||
"on_red": ColorRed,
|
||||
"on_green": ColorGreen,
|
||||
"on_yellow": ColorYellow,
|
||||
"on_blue": ColorBlue,
|
||||
"on_magenta": ColorMagenta,
|
||||
"on_cyan": ColorCyan,
|
||||
"on_white": ColorWhite,
|
||||
}
|
||||
stringToFgAttr = map[string]Attribute{
|
||||
"bold": AttrBold,
|
||||
"underline": AttrUnderline,
|
||||
"reverse": AttrReverse,
|
||||
}
|
||||
stringToBgAttr = map[string]Attribute{
|
||||
"on_bold": AttrBold,
|
||||
}
|
||||
)
|
||||
|
||||
// NewStyleSet creates a new StyleSet struct
|
||||
func NewStyleSet() *StyleSet {
|
||||
ss := &StyleSet{}
|
||||
ss.Init()
|
||||
return ss
|
||||
}
|
||||
|
||||
// Init initializes the StyleSet with default foreground and background colors
|
||||
// for each UI element (basic, query, matched, selected, prompt, context, etc.).
|
||||
func (ss *StyleSet) Init() {
|
||||
ss.Basic.fg = ColorDefault
|
||||
ss.Basic.bg = ColorDefault
|
||||
ss.Query.fg = ColorDefault
|
||||
ss.Query.bg = ColorDefault
|
||||
ss.Matched.fg = ColorCyan
|
||||
ss.Matched.bg = ColorDefault
|
||||
ss.SavedSelection.fg = ColorBlack | AttrBold
|
||||
ss.SavedSelection.bg = ColorCyan
|
||||
ss.Selected.fg = ColorDefault | AttrUnderline
|
||||
ss.Selected.bg = ColorMagenta
|
||||
ss.Prompt.fg = ColorDefault
|
||||
ss.Prompt.bg = ColorDefault
|
||||
ss.Context.fg = ColorDefault | AttrBold
|
||||
ss.Context.bg = ColorDefault
|
||||
}
|
||||
|
||||
// UnmarshalJSON satisfies json.RawMessage.
|
||||
func (s *Style) UnmarshalJSON(buf []byte) error {
|
||||
raw := []string{}
|
||||
if err := json.Unmarshal(buf, &raw); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal Style: %w", err)
|
||||
}
|
||||
return stringsToStyle(s, raw)
|
||||
}
|
||||
|
||||
// UnmarshalYAML decodes a YAML array of strings into a Style.
|
||||
func (s *Style) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var raw []string
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal Style from YAML: %w", err)
|
||||
}
|
||||
return stringsToStyle(s, raw)
|
||||
}
|
||||
|
||||
// stringsToStyle parses an array of color and attribute strings (e.g. "red",
|
||||
// "on_blue", "bold", "#ff00ff") into a Style's foreground and background Attributes.
|
||||
func stringsToStyle(style *Style, raw []string) error {
|
||||
style.fg = ColorDefault
|
||||
style.bg = ColorDefault
|
||||
|
||||
for _, s := range raw {
|
||||
fg, ok := stringToFg[s]
|
||||
if ok {
|
||||
style.fg = fg
|
||||
} else if strings.HasPrefix(s, "#") && len(s) == 7 {
|
||||
if rgb, err := strconv.ParseUint(s[1:], 16, 32); err == nil {
|
||||
style.fg = Attribute(rgb) | AttrTrueColor
|
||||
}
|
||||
} else {
|
||||
if fg, err := strconv.ParseUint(s, 10, 8); err == nil {
|
||||
style.fg = Attribute(fg + 1)
|
||||
}
|
||||
}
|
||||
|
||||
bg, ok := stringToBg[s]
|
||||
if ok {
|
||||
style.bg = bg
|
||||
} else if strings.HasPrefix(s, "on_#") && len(s) == 10 {
|
||||
if rgb, err := strconv.ParseUint(s[4:], 16, 32); err == nil {
|
||||
style.bg = Attribute(rgb) | AttrTrueColor
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(s, "on_") {
|
||||
if bg, err := strconv.ParseUint(s[3:], 10, 8); err == nil {
|
||||
style.bg = 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
|
||||
}
|
||||
|
||||
// ConfigLocator locates a config file in a given directory.
|
||||
type ConfigLocator interface {
|
||||
// Locator locates a config file in a given directory.
|
||||
type Locator interface {
|
||||
Locate(string) (string, error)
|
||||
}
|
||||
|
||||
// ConfigLocatorFunc is a function that implements ConfigLocator.
|
||||
type ConfigLocatorFunc func(string) (string, error)
|
||||
// LocatorFunc is a function that implements Locator.
|
||||
type LocatorFunc func(string) (string, error)
|
||||
|
||||
// Locate calls the underlying function.
|
||||
func (f ConfigLocatorFunc) Locate(dir string) (string, error) {
|
||||
func (f LocatorFunc) Locate(dir string) (string, error) {
|
||||
return f(dir)
|
||||
}
|
||||
|
||||
var configFilenames = []string{"config.json", "config.yaml", "config.yml"}
|
||||
|
||||
// defaultConfigLocator searches for a config file with one of the known
|
||||
// DefaultConfigLocator searches for a config file with one of the known
|
||||
// filenames (config.json, config.yaml, config.yml) in the given directory.
|
||||
var defaultConfigLocator = ConfigLocatorFunc(func(dir string) (string, error) {
|
||||
var DefaultConfigLocator = LocatorFunc(func(dir string) (string, error) {
|
||||
for _, basename := range configFilenames {
|
||||
file := filepath.Join(dir, basename)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
|
|
@ -340,7 +163,7 @@ var defaultConfigLocator = ConfigLocatorFunc(func(dir string) (string, error) {
|
|||
})
|
||||
|
||||
// LocateRcfile attempts to find the config file in various locations
|
||||
func LocateRcfile(locater ConfigLocator) (string, error) {
|
||||
func LocateRcfile(locater Locator) (string, error) {
|
||||
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
//
|
||||
// Try in this order:
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package peco
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -22,28 +22,28 @@ var expectedConfig = Config{
|
|||
Prompt: "[peco]",
|
||||
Style: StyleSet{
|
||||
Matched: Style{
|
||||
fg: ColorCyan | AttrBold,
|
||||
bg: ColorRed,
|
||||
Fg: ColorCyan | AttrBold,
|
||||
Bg: ColorRed,
|
||||
},
|
||||
Query: Style{
|
||||
fg: ColorYellow | AttrBold,
|
||||
bg: ColorDefault,
|
||||
Fg: ColorYellow | AttrBold,
|
||||
Bg: ColorDefault,
|
||||
},
|
||||
Selected: Style{
|
||||
fg: ColorBlack | AttrUnderline,
|
||||
bg: ColorCyan,
|
||||
Fg: ColorBlack | AttrUnderline,
|
||||
Bg: ColorCyan,
|
||||
},
|
||||
SavedSelection: Style{
|
||||
fg: ColorBlack | AttrBold,
|
||||
bg: ColorCyan,
|
||||
Fg: ColorBlack | AttrBold,
|
||||
Bg: ColorCyan,
|
||||
},
|
||||
Prompt: Style{
|
||||
fg: ColorGreen | AttrBold,
|
||||
bg: ColorDefault,
|
||||
Fg: ColorGreen | AttrBold,
|
||||
Bg: ColorDefault,
|
||||
},
|
||||
Context: Style{
|
||||
fg: ColorDefault | AttrBold,
|
||||
bg: ColorDefault,
|
||||
Fg: ColorDefault | AttrBold,
|
||||
Bg: ColorDefault,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -111,39 +111,39 @@ func TestStringsToStyle(t *testing.T) {
|
|||
tests := []stringsToStyleTest{
|
||||
{
|
||||
strings: []string{"on_default", "default"},
|
||||
style: &Style{fg: ColorDefault, bg: ColorDefault},
|
||||
style: &Style{Fg: ColorDefault, Bg: ColorDefault},
|
||||
},
|
||||
{
|
||||
strings: []string{"bold", "on_blue", "yellow"},
|
||||
style: &Style{fg: ColorYellow | AttrBold, bg: ColorBlue},
|
||||
style: &Style{Fg: ColorYellow | AttrBold, Bg: ColorBlue},
|
||||
},
|
||||
{
|
||||
strings: []string{"underline", "on_cyan", "black"},
|
||||
style: &Style{fg: ColorBlack | AttrUnderline, bg: ColorCyan},
|
||||
style: &Style{Fg: ColorBlack | AttrUnderline, Bg: ColorCyan},
|
||||
},
|
||||
{
|
||||
strings: []string{"reverse", "on_red", "white"},
|
||||
style: &Style{fg: ColorWhite | AttrReverse, bg: ColorRed},
|
||||
style: &Style{Fg: ColorWhite | AttrReverse, Bg: ColorRed},
|
||||
},
|
||||
{
|
||||
strings: []string{"on_bold", "on_magenta", "green"},
|
||||
style: &Style{fg: ColorGreen, bg: ColorMagenta | AttrBold},
|
||||
style: &Style{Fg: ColorGreen, Bg: ColorMagenta | AttrBold},
|
||||
},
|
||||
{
|
||||
strings: []string{"underline", "on_240", "214"},
|
||||
style: &Style{fg: Attribute(214+1) | AttrUnderline, bg: Attribute(240 + 1)},
|
||||
style: &Style{Fg: Attribute(214+1) | AttrUnderline, Bg: Attribute(240 + 1)},
|
||||
},
|
||||
{
|
||||
strings: []string{"#ff8800", "on_#0088ff"},
|
||||
style: &Style{fg: Attribute(0xff8800) | AttrTrueColor, bg: Attribute(0x0088ff) | AttrTrueColor},
|
||||
style: &Style{Fg: Attribute(0xff8800) | AttrTrueColor, Bg: Attribute(0x0088ff) | AttrTrueColor},
|
||||
},
|
||||
{
|
||||
strings: []string{"bold", "#00ff00", "on_#000000"},
|
||||
style: &Style{fg: Attribute(0x00ff00) | AttrTrueColor | AttrBold, bg: Attribute(0x000000) | AttrTrueColor},
|
||||
style: &Style{Fg: Attribute(0x00ff00) | AttrTrueColor | AttrBold, Bg: Attribute(0x000000) | AttrTrueColor},
|
||||
},
|
||||
{
|
||||
strings: []string{"#000000"},
|
||||
style: &Style{fg: Attribute(0x000000) | AttrTrueColor, bg: ColorDefault},
|
||||
style: &Style{Fg: Attribute(0x000000) | AttrTrueColor, Bg: ColorDefault},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ func TestStringsToStyle(t *testing.T) {
|
|||
var a Style
|
||||
for _, test := range tests {
|
||||
t.Logf(" checking %s...", test.strings)
|
||||
require.NoError(t, stringsToStyle(&a, test.strings), "stringsToStyle should succeed")
|
||||
require.NoError(t, StringsToStyle(&a, test.strings), "StringsToStyle should succeed")
|
||||
require.Equal(t, test.style, &a, "Expected '%s' to be '%#v', but got '%#v'", test.strings, test.style, a)
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ func TestLocateRcfile(t *testing.T) {
|
|||
}
|
||||
|
||||
i := 0
|
||||
locater := ConfigLocatorFunc(func(dir string) (string, error) {
|
||||
locater := LocatorFunc(func(dir string) (string, error) {
|
||||
t.Logf("looking for file in %s", dir)
|
||||
require.True(t, i <= len(expected)-1, "Got %d directories, only have %d", i+1, len(expected))
|
||||
require.Equal(t, expected[i], dir, "Expected %s, got %s", expected[i], dir)
|
||||
|
|
@ -213,7 +213,7 @@ func TestLocateRcfileYAML(t *testing.T) {
|
|||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
t.Setenv("XDG_CONFIG_DIRS", "")
|
||||
|
||||
file, err := LocateRcfile(defaultConfigLocator)
|
||||
file, err := LocateRcfile(DefaultConfigLocator)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, filepath.Join(pecoDir, "config.yaml"), file)
|
||||
}
|
||||
|
|
@ -265,15 +265,6 @@ func TestOnCancelBehavior(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "bogus")
|
||||
})
|
||||
|
||||
t.Run("invalid CLI option rejected", func(t *testing.T) {
|
||||
p := newPeco()
|
||||
var opts CLIOptions
|
||||
opts.OptOnCancel = "bogus"
|
||||
err := p.ApplyConfig(opts)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "bogus")
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadFilenameYAML(t *testing.T) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package peco
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -49,11 +49,11 @@ func ParseHeightSpec(s string) (HeightSpec, error) {
|
|||
return HeightSpec{Value: v, IsPercent: false}, nil
|
||||
}
|
||||
|
||||
// chromLines is the number of lines used by the prompt and status bar.
|
||||
const chromLines = 2
|
||||
// ChromLines is the number of lines used by the prompt and status bar.
|
||||
const ChromLines = 2
|
||||
|
||||
// Resolve converts the HeightSpec to an absolute number of screen rows,
|
||||
// clamped to [chromLines+1, termHeight].
|
||||
// clamped to [ChromLines+1, termHeight].
|
||||
//
|
||||
// For absolute values, Value is the number of result lines — the prompt
|
||||
// and status bar are added automatically (total = Value + 2).
|
||||
|
|
@ -64,10 +64,10 @@ func (h HeightSpec) Resolve(termHeight int) int {
|
|||
if h.IsPercent {
|
||||
height = termHeight * h.Value / 100
|
||||
} else {
|
||||
height = h.Value + chromLines
|
||||
height = h.Value + ChromLines
|
||||
}
|
||||
|
||||
minHeight := chromLines + 1 // at least 1 result line
|
||||
minHeight := ChromLines + 1 // at least 1 result line
|
||||
if height < minHeight {
|
||||
height = minHeight
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package peco
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -77,7 +77,7 @@ func TestParseHeightSpec(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHeightSpecResolve(t *testing.T) {
|
||||
// For absolute values: Value is result lines, total = Value + chromLines(2)
|
||||
// For absolute values: Value is result lines, total = Value + ChromLines(2)
|
||||
t.Run("absolute adds chrome", func(t *testing.T) {
|
||||
spec := HeightSpec{Value: 10, IsPercent: false}
|
||||
// 10 result lines + 2 chrome = 12 total
|
||||
|
|
@ -107,7 +107,7 @@ func TestHeightSpecResolve(t *testing.T) {
|
|||
// but Resolve handles it defensively.
|
||||
spec := HeightSpec{Value: 0, IsPercent: false}
|
||||
// 0 + 2 = 2, clamped to min 3
|
||||
require.Equal(t, chromLines+1, spec.Resolve(24))
|
||||
require.Equal(t, ChromLines+1, spec.Resolve(24))
|
||||
})
|
||||
|
||||
// For percentages: Value is percentage of total terminal height
|
||||
|
|
@ -123,8 +123,8 @@ func TestHeightSpecResolve(t *testing.T) {
|
|||
|
||||
t.Run("percentage clamp to min", func(t *testing.T) {
|
||||
spec := HeightSpec{Value: 1, IsPercent: true}
|
||||
// 1% of 24 = 0, clamped to chromLines+1 = 3
|
||||
require.Equal(t, chromLines+1, spec.Resolve(24))
|
||||
// 1% of 24 = 0, clamped to ChromLines+1 = 3
|
||||
require.Equal(t, ChromLines+1, spec.Resolve(24))
|
||||
})
|
||||
|
||||
t.Run("percentage clamp to terminal height", func(t *testing.T) {
|
||||
24
config/layout.go
Normal file
24
config/layout.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package config
|
||||
|
||||
// LayoutType describes the types of layout that peco can take
|
||||
type LayoutType = string
|
||||
|
||||
const (
|
||||
DefaultLayoutType = LayoutTypeTopDown // LayoutTypeTopDown makes the layout so the items read from top to bottom
|
||||
LayoutTypeTopDown = "top-down" // LayoutTypeTopDown displays prompt at top, list top-to-bottom
|
||||
LayoutTypeBottomUp = "bottom-up" // LayoutTypeBottomUp displays prompt at bottom, list bottom-to-top
|
||||
LayoutTypeTopDownQueryBottom = "top-down-query-bottom" // LayoutTypeTopDownQueryBottom displays list top-to-bottom, prompt at bottom
|
||||
)
|
||||
|
||||
// validLayoutTypes enumerates all recognized layout type values.
|
||||
var validLayoutTypes = map[LayoutType]struct{}{
|
||||
LayoutTypeTopDown: {},
|
||||
LayoutTypeBottomUp: {},
|
||||
LayoutTypeTopDownQueryBottom: {},
|
||||
}
|
||||
|
||||
// IsValidLayoutType checks if a string is a supported layout type
|
||||
func IsValidLayoutType(v LayoutType) bool {
|
||||
_, ok := validLayoutTypes[v]
|
||||
return ok
|
||||
}
|
||||
184
config/style.go
Normal file
184
config/style.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StyleSet holds styles for various sections
|
||||
type StyleSet struct {
|
||||
Basic Style `json:"Basic" yaml:"Basic"`
|
||||
SavedSelection Style `json:"SavedSelection" yaml:"SavedSelection"`
|
||||
Selected Style `json:"Selected" yaml:"Selected"`
|
||||
Query Style `json:"Query" yaml:"Query"`
|
||||
QueryCursor Style `json:"QueryCursor" yaml:"QueryCursor"`
|
||||
Matched Style `json:"Matched" yaml:"Matched"`
|
||||
Prompt Style `json:"Prompt" yaml:"Prompt"`
|
||||
Context Style `json:"Context" yaml:"Context"`
|
||||
}
|
||||
|
||||
// Attribute represents terminal display attributes such as colors
|
||||
// and text styling (bold, underline, reverse). It is a uint32 bitfield:
|
||||
//
|
||||
// Bits 0-8: Palette color index (0=default, 1-256 for 256-color palette)
|
||||
// Bits 0-23: RGB color value (when AttrTrueColor flag is set)
|
||||
// Bit 24: AttrTrueColor flag — distinguishes true color from palette
|
||||
// Bit 25: AttrBold
|
||||
// Bit 26: AttrUnderline
|
||||
// Bit 27: AttrReverse
|
||||
// Bits 28-31: Reserved
|
||||
type Attribute uint32
|
||||
|
||||
// Named palette color constants (values 0-8).
|
||||
const (
|
||||
ColorDefault Attribute = 0x0000
|
||||
ColorBlack Attribute = 0x0001
|
||||
ColorRed Attribute = 0x0002
|
||||
ColorGreen Attribute = 0x0003
|
||||
ColorYellow Attribute = 0x0004
|
||||
ColorBlue Attribute = 0x0005
|
||||
ColorMagenta Attribute = 0x0006
|
||||
ColorCyan Attribute = 0x0007
|
||||
ColorWhite Attribute = 0x0008
|
||||
)
|
||||
|
||||
const (
|
||||
AttrTrueColor Attribute = 0x01000000
|
||||
AttrBold Attribute = 0x02000000
|
||||
AttrUnderline Attribute = 0x04000000
|
||||
AttrReverse Attribute = 0x08000000
|
||||
)
|
||||
|
||||
// Style describes display attributes for foreground and background.
|
||||
type Style struct {
|
||||
Fg Attribute
|
||||
Bg Attribute
|
||||
}
|
||||
|
||||
var (
|
||||
StringToFg = map[string]Attribute{
|
||||
"default": ColorDefault,
|
||||
"black": ColorBlack,
|
||||
"red": ColorRed,
|
||||
"green": ColorGreen,
|
||||
"yellow": ColorYellow,
|
||||
"blue": ColorBlue,
|
||||
"magenta": ColorMagenta,
|
||||
"cyan": ColorCyan,
|
||||
"white": ColorWhite,
|
||||
}
|
||||
StringToBg = map[string]Attribute{
|
||||
"on_default": ColorDefault,
|
||||
"on_black": ColorBlack,
|
||||
"on_red": ColorRed,
|
||||
"on_green": ColorGreen,
|
||||
"on_yellow": ColorYellow,
|
||||
"on_blue": ColorBlue,
|
||||
"on_magenta": ColorMagenta,
|
||||
"on_cyan": ColorCyan,
|
||||
"on_white": ColorWhite,
|
||||
}
|
||||
StringToFgAttr = map[string]Attribute{
|
||||
"bold": AttrBold,
|
||||
"underline": AttrUnderline,
|
||||
"reverse": AttrReverse,
|
||||
}
|
||||
StringToBgAttr = map[string]Attribute{
|
||||
"on_bold": AttrBold,
|
||||
}
|
||||
)
|
||||
|
||||
// NewStyleSet creates a new StyleSet struct
|
||||
func NewStyleSet() *StyleSet {
|
||||
ss := &StyleSet{}
|
||||
ss.Init()
|
||||
return ss
|
||||
}
|
||||
|
||||
// Init initializes the StyleSet with default foreground and background colors
|
||||
// for each UI element (basic, query, matched, selected, prompt, context, etc.).
|
||||
func (ss *StyleSet) Init() {
|
||||
ss.Basic.Fg = ColorDefault
|
||||
ss.Basic.Bg = ColorDefault
|
||||
ss.Query.Fg = ColorDefault
|
||||
ss.Query.Bg = ColorDefault
|
||||
ss.Matched.Fg = ColorCyan
|
||||
ss.Matched.Bg = ColorDefault
|
||||
ss.SavedSelection.Fg = ColorBlack | AttrBold
|
||||
ss.SavedSelection.Bg = ColorCyan
|
||||
ss.Selected.Fg = ColorDefault | AttrUnderline
|
||||
ss.Selected.Bg = ColorMagenta
|
||||
ss.Prompt.Fg = ColorDefault
|
||||
ss.Prompt.Bg = ColorDefault
|
||||
ss.Context.Fg = ColorDefault | AttrBold
|
||||
ss.Context.Bg = ColorDefault
|
||||
}
|
||||
|
||||
// UnmarshalJSON satisfies json.RawMessage.
|
||||
func (s *Style) UnmarshalJSON(buf []byte) error {
|
||||
raw := []string{}
|
||||
if err := json.Unmarshal(buf, &raw); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal Style: %w", err)
|
||||
}
|
||||
return StringsToStyle(s, raw)
|
||||
}
|
||||
|
||||
// UnmarshalYAML decodes a YAML array of strings into a Style.
|
||||
func (s *Style) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var raw []string
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal Style from YAML: %w", err)
|
||||
}
|
||||
return StringsToStyle(s, raw)
|
||||
}
|
||||
|
||||
// StringsToStyle parses an array of color and attribute strings (e.g. "red",
|
||||
// "on_blue", "bold", "#ff00ff") into a Style's foreground and background Attributes.
|
||||
func StringsToStyle(style *Style, raw []string) error {
|
||||
style.Fg = ColorDefault
|
||||
style.Bg = ColorDefault
|
||||
|
||||
for _, s := range raw {
|
||||
fg, ok := StringToFg[s]
|
||||
if ok {
|
||||
style.Fg = fg
|
||||
} else if strings.HasPrefix(s, "#") && len(s) == 7 {
|
||||
if rgb, err := strconv.ParseUint(s[1:], 16, 32); err == nil {
|
||||
style.Fg = Attribute(rgb) | AttrTrueColor
|
||||
}
|
||||
} else {
|
||||
if fg, err := strconv.ParseUint(s, 10, 8); err == nil {
|
||||
style.Fg = Attribute(fg + 1)
|
||||
}
|
||||
}
|
||||
|
||||
bg, ok := StringToBg[s]
|
||||
if ok {
|
||||
style.Bg = bg
|
||||
} else if strings.HasPrefix(s, "on_#") && len(s) == 10 {
|
||||
if rgb, err := strconv.ParseUint(s[4:], 16, 32); err == nil {
|
||||
style.Bg = Attribute(rgb) | AttrTrueColor
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(s, "on_") {
|
||||
if bg, err := strconv.ParseUint(s[3:], 10, 8); err == nil {
|
||||
style.Bg = 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
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"context"
|
||||
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/filter"
|
||||
"github.com/peco/peco/internal/keyseq"
|
||||
"github.com/peco/peco/line"
|
||||
|
|
@ -25,7 +26,7 @@ func TestIssue212_SanityCheck(t *testing.T) {
|
|||
require.Equal(t, state.config.Layout, "top-down", "Default layout type should be 'top-down', got '%s'", state.config.Layout)
|
||||
require.Equal(t, len(state.config.Keymap), 0, "Default keymap should be empty, but got '%#v'", state.config.Keymap)
|
||||
|
||||
defstyle := StyleSet{}
|
||||
defstyle := config.StyleSet{}
|
||||
defstyle.Init()
|
||||
require.Equal(t, state.config.Style, defstyle, "should be default style")
|
||||
require.Equal(t, state.config.Prompt, "QUERY>", "Default prompt should be 'QUERY>', but got '%s'", state.config.Prompt)
|
||||
|
|
|
|||
123
layout.go
123
layout.go
|
|
@ -12,21 +12,12 @@ import (
|
|||
|
||||
"github.com/lestrrat-go/pdebug"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/internal/ansi"
|
||||
linepkg "github.com/peco/peco/line"
|
||||
)
|
||||
|
||||
// LayoutType describes the types of layout that peco can take
|
||||
type LayoutType string
|
||||
|
||||
const (
|
||||
DefaultLayoutType = LayoutTypeTopDown // LayoutTypeTopDown makes the layout so the items read from top to bottom
|
||||
LayoutTypeTopDown = "top-down" // LayoutTypeTopDown displays prompt at top, list top-to-bottom
|
||||
LayoutTypeBottomUp = "bottom-up" // LayoutTypeBottomUp displays prompt at bottom, list bottom-to-top
|
||||
LayoutTypeTopDownQueryBottom = "top-down-query-bottom" // LayoutTypeTopDownQueryBottom displays list top-to-bottom, prompt at bottom
|
||||
)
|
||||
|
||||
// VerticalAnchor describes the direction to which elements in the
|
||||
// layout are anchored to
|
||||
type VerticalAnchor int
|
||||
|
|
@ -59,7 +50,7 @@ type UserPrompt struct {
|
|||
*AnchorSettings
|
||||
prompt string
|
||||
promptLen int
|
||||
styles *StyleSet
|
||||
styles *config.StyleSet
|
||||
}
|
||||
|
||||
// StatusBar is the interface for printing status messages
|
||||
|
|
@ -71,7 +62,7 @@ type StatusBar interface {
|
|||
type screenStatusBar struct {
|
||||
*AnchorSettings
|
||||
clearTimer *time.Timer
|
||||
styles *StyleSet
|
||||
styles *config.StyleSet
|
||||
timerMutex sync.Mutex
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +76,7 @@ type ListArea struct {
|
|||
sortTopDown bool
|
||||
displayCache []linepkg.Line
|
||||
dirty bool
|
||||
styles *StyleSet
|
||||
styles *config.StyleSet
|
||||
}
|
||||
|
||||
// BasicLayout is... the basic layout :) At this point this is the
|
||||
|
|
@ -122,25 +113,19 @@ func (f LayoutBuilderFunc) Build(state *Peco) (*BasicLayout, error) {
|
|||
return f(state)
|
||||
}
|
||||
|
||||
var layoutRegistry = map[LayoutType]LayoutBuilder{}
|
||||
var layoutRegistry = map[string]LayoutBuilder{}
|
||||
|
||||
// RegisterLayout registers a layout builder under the given name.
|
||||
func RegisterLayout(name LayoutType, builder LayoutBuilder) {
|
||||
func RegisterLayout(name string, builder LayoutBuilder) {
|
||||
layoutRegistry[name] = builder
|
||||
}
|
||||
|
||||
// NewLayout creates a layout by looking up the registry. Falls back to top-down.
|
||||
func NewLayout(layoutType LayoutType, state *Peco) (*BasicLayout, error) {
|
||||
func NewLayout(layoutType string, state *Peco) (*BasicLayout, error) {
|
||||
if builder, ok := layoutRegistry[layoutType]; ok {
|
||||
return builder.Build(state)
|
||||
}
|
||||
return layoutRegistry[LayoutTypeTopDown].Build(state)
|
||||
}
|
||||
|
||||
// IsValidLayoutType checks if a string is a supported layout type
|
||||
func IsValidLayoutType(v LayoutType) bool {
|
||||
_, ok := layoutRegistry[v]
|
||||
return ok
|
||||
return layoutRegistry[config.LayoutTypeTopDown].Build(state)
|
||||
}
|
||||
|
||||
// IsValidVerticalAnchor checks if the specified anchor is supported
|
||||
|
|
@ -152,8 +137,8 @@ func IsValidVerticalAnchor(anchor VerticalAnchor) bool {
|
|||
// If either has a default color (0), the non-default color wins via OR.
|
||||
// Otherwise, color bits are merged using OR on 0-based indices.
|
||||
// Attribute flags (bold, underline, reverse, true color) are always OR'd.
|
||||
func mergeAttribute(a, b Attribute) Attribute {
|
||||
const flagMask = AttrTrueColor | AttrBold | AttrUnderline | AttrReverse
|
||||
func mergeAttribute(a, b config.Attribute) config.Attribute {
|
||||
const flagMask = config.AttrTrueColor | config.AttrBold | config.AttrUnderline | config.AttrReverse
|
||||
aColor := a &^ flagMask
|
||||
bColor := b &^ flagMask
|
||||
flags := (a | b) & flagMask
|
||||
|
|
@ -192,9 +177,9 @@ func (as AnchorSettings) AnchorPosition() int {
|
|||
}
|
||||
|
||||
// NewUserPrompt creates a new UserPrompt struct
|
||||
func NewUserPrompt(screen Screen, anchor VerticalAnchor, anchorOffset int, prompt string, styles *StyleSet) (*UserPrompt, error) {
|
||||
func NewUserPrompt(screen Screen, anchor VerticalAnchor, anchorOffset int, prompt string, styles *config.StyleSet) (*UserPrompt, error) {
|
||||
if prompt == "" { // default
|
||||
prompt = DefaultPrompt
|
||||
prompt = config.DefaultPrompt
|
||||
}
|
||||
promptLen := runewidth.StringWidth(prompt)
|
||||
|
||||
|
|
@ -215,16 +200,16 @@ func NewUserPrompt(screen Screen, anchor VerticalAnchor, anchorOffset int, promp
|
|||
// If QueryCursor is explicitly configured, it is used directly.
|
||||
// Otherwise, the Query style's fg/bg are swapped. If both are
|
||||
// ColorDefault, AttrReverse is used as a fallback.
|
||||
func (u UserPrompt) cursorStyle() (Attribute, Attribute) {
|
||||
func (u UserPrompt) cursorStyle() (config.Attribute, config.Attribute) {
|
||||
qc := u.styles.QueryCursor
|
||||
if qc.fg != ColorDefault || qc.bg != ColorDefault {
|
||||
return qc.fg, qc.bg
|
||||
if qc.Fg != config.ColorDefault || qc.Bg != config.ColorDefault {
|
||||
return qc.Fg, qc.Bg
|
||||
}
|
||||
qfg, qbg := u.styles.Query.fg, u.styles.Query.bg
|
||||
if qfg != ColorDefault || qbg != ColorDefault {
|
||||
qfg, qbg := u.styles.Query.Fg, u.styles.Query.Bg
|
||||
if qfg != config.ColorDefault || qbg != config.ColorDefault {
|
||||
return qbg, qfg
|
||||
}
|
||||
return ColorDefault | AttrReverse, ColorDefault | AttrReverse
|
||||
return config.ColorDefault | config.AttrReverse, config.ColorDefault | config.AttrReverse
|
||||
}
|
||||
|
||||
// Draw draws the query prompt
|
||||
|
|
@ -239,8 +224,8 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
// print "QUERY>"
|
||||
u.screen.Print(PrintArgs{
|
||||
Y: location,
|
||||
Fg: u.styles.Prompt.fg,
|
||||
Bg: u.styles.Prompt.bg,
|
||||
Fg: u.styles.Prompt.Fg,
|
||||
Bg: u.styles.Prompt.Bg,
|
||||
Msg: u.prompt,
|
||||
})
|
||||
|
||||
|
|
@ -256,8 +241,8 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
c.SetPos(ql)
|
||||
}
|
||||
|
||||
fg := u.styles.Query.fg
|
||||
bg := u.styles.Query.bg
|
||||
fg := u.styles.Query.Fg
|
||||
bg := u.styles.Query.Bg
|
||||
|
||||
// Used to notify the screen where our cursor is
|
||||
var posX int
|
||||
|
|
@ -317,8 +302,8 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
cfgCursor, cbgCursor := u.cursorStyle()
|
||||
prev := int(0)
|
||||
for i, r := range q.RuneSlice() {
|
||||
fg := u.styles.Query.fg
|
||||
bg := u.styles.Query.bg
|
||||
fg := u.styles.Query.Fg
|
||||
bg := u.styles.Query.Bg
|
||||
if i == c.Pos() {
|
||||
fg = cfgCursor
|
||||
bg = cbgCursor
|
||||
|
|
@ -326,8 +311,8 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
u.screen.SetCell(queryStartX+prev, location, r, fg, bg)
|
||||
prev += runewidth.RuneWidth(r)
|
||||
}
|
||||
fg := u.styles.Query.fg
|
||||
bg := u.styles.Query.bg
|
||||
fg := u.styles.Query.Fg
|
||||
bg := u.styles.Query.Bg
|
||||
u.screen.Print(PrintArgs{
|
||||
X: queryStartX + prev,
|
||||
Y: location,
|
||||
|
|
@ -346,8 +331,8 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
u.screen.Print(PrintArgs{
|
||||
X: width - runewidth.StringWidth(pmsg),
|
||||
Y: location,
|
||||
Fg: u.styles.Basic.fg,
|
||||
Bg: u.styles.Basic.bg,
|
||||
Fg: u.styles.Basic.Fg,
|
||||
Bg: u.styles.Basic.Bg,
|
||||
Msg: pmsg,
|
||||
})
|
||||
|
||||
|
|
@ -355,7 +340,7 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
}
|
||||
|
||||
// newScreenStatusBar creates a new screenStatusBar struct
|
||||
func newScreenStatusBar(screen Screen, anchor VerticalAnchor, anchorOffset int, styles *StyleSet) (*screenStatusBar, error) {
|
||||
func newScreenStatusBar(screen Screen, anchor VerticalAnchor, anchorOffset int, styles *config.StyleSet) (*screenStatusBar, error) {
|
||||
as, err := NewAnchorSettings(screen, anchor, anchorOffset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create status bar: %w", err)
|
||||
|
|
@ -413,8 +398,8 @@ func (s *screenStatusBar) PrintStatus(msg string, clearDelay time.Duration) {
|
|||
}
|
||||
}
|
||||
|
||||
fgAttr := s.styles.Basic.fg
|
||||
bgAttr := s.styles.Basic.bg
|
||||
fgAttr := s.styles.Basic.Fg
|
||||
bgAttr := s.styles.Basic.Bg
|
||||
|
||||
if w > width {
|
||||
s.screen.Print(PrintArgs{
|
||||
|
|
@ -429,8 +414,8 @@ func (s *screenStatusBar) PrintStatus(msg string, clearDelay time.Duration) {
|
|||
s.screen.Print(PrintArgs{
|
||||
X: w - width,
|
||||
Y: location,
|
||||
Fg: fgAttr | AttrBold | AttrReverse,
|
||||
Bg: bgAttr | AttrReverse,
|
||||
Fg: fgAttr | config.AttrBold | config.AttrReverse,
|
||||
Bg: bgAttr | config.AttrReverse,
|
||||
Msg: msg,
|
||||
})
|
||||
}
|
||||
|
|
@ -454,7 +439,7 @@ func (l *BasicLayout) PrintStatus(msg string, delay time.Duration) {
|
|||
}
|
||||
|
||||
// NewListArea creates a new ListArea struct
|
||||
func NewListArea(screen Screen, anchor VerticalAnchor, anchorOffset int, sortTopDown bool, styles *StyleSet) (*ListArea, error) {
|
||||
func NewListArea(screen Screen, anchor VerticalAnchor, anchorOffset int, sortTopDown bool, styles *config.StyleSet) (*ListArea, error) {
|
||||
as, err := NewAnchorSettings(screen, anchor, anchorOffset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create list area: %w", err)
|
||||
|
|
@ -510,7 +495,7 @@ func adjustPageForRunningQuery(loc *Location, linebuf Buffer, parent Layout, sta
|
|||
|
||||
// renderMatchedLine renders a line with match highlighting, interleaving
|
||||
// matched and non-matched segments with their respective styles.
|
||||
func (l *ListArea) renderMatchedLine(ml *linepkg.Matched, line string, lineANSIAttrs []ansi.AttrSpan, x, y, xOffset int, fgAttr, bgAttr Attribute) {
|
||||
func (l *ListArea) renderMatchedLine(ml *linepkg.Matched, line string, lineANSIAttrs []ansi.AttrSpan, x, y, xOffset int, fgAttr, bgAttr config.Attribute) {
|
||||
matches := ml.Indices()
|
||||
prev := x
|
||||
index := 0
|
||||
|
|
@ -544,8 +529,8 @@ func (l *ListArea) renderMatchedLine(ml *linepkg.Matched, line string, lineANSIA
|
|||
X: prev,
|
||||
Y: y,
|
||||
XOffset: xOffset,
|
||||
Fg: l.styles.Matched.fg,
|
||||
Bg: mergeAttribute(bgAttr, l.styles.Matched.bg),
|
||||
Fg: l.styles.Matched.Fg,
|
||||
Bg: mergeAttribute(bgAttr, l.styles.Matched.Bg),
|
||||
Msg: c,
|
||||
})
|
||||
prev += n
|
||||
|
|
@ -648,14 +633,14 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
}
|
||||
l.screen.Print(PrintArgs{
|
||||
Y: y,
|
||||
Fg: l.styles.Basic.fg,
|
||||
Bg: l.styles.Basic.bg,
|
||||
Fg: l.styles.Basic.Fg,
|
||||
Bg: l.styles.Basic.Bg,
|
||||
Fill: true,
|
||||
})
|
||||
}
|
||||
|
||||
var cached, written int
|
||||
var fgAttr, bgAttr Attribute
|
||||
var fgAttr, bgAttr config.Attribute
|
||||
selectionPrefix := state.SelectionPrefix()
|
||||
var prefix string
|
||||
|
||||
|
|
@ -679,14 +664,14 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
} else {
|
||||
switch {
|
||||
case n+loc.Offset() == loc.LineNumber():
|
||||
fgAttr = l.styles.Selected.fg
|
||||
bgAttr = l.styles.Selected.bg
|
||||
fgAttr = l.styles.Selected.Fg
|
||||
bgAttr = l.styles.Selected.Bg
|
||||
case selectionContains(state, n+loc.Offset()):
|
||||
fgAttr = l.styles.SavedSelection.fg
|
||||
bgAttr = l.styles.SavedSelection.bg
|
||||
fgAttr = l.styles.SavedSelection.Fg
|
||||
bgAttr = l.styles.SavedSelection.Bg
|
||||
default:
|
||||
fgAttr = l.styles.Basic.fg
|
||||
bgAttr = l.styles.Basic.bg
|
||||
fgAttr = l.styles.Basic.Fg
|
||||
bgAttr = l.styles.Basic.Bg
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -717,9 +702,9 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
|
||||
// Apply Context style for non-matched surrounding lines
|
||||
if _, isCtx := target.(*ContextLine); isCtx {
|
||||
if fgAttr == l.styles.Basic.fg && bgAttr == l.styles.Basic.bg {
|
||||
fgAttr = l.styles.Context.fg
|
||||
bgAttr = l.styles.Context.bg
|
||||
if fgAttr == l.styles.Basic.Fg && bgAttr == l.styles.Basic.Bg {
|
||||
fgAttr = l.styles.Context.Fg
|
||||
bgAttr = l.styles.Context.Bg
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -731,7 +716,7 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
// non-selected (basic) lines so selection/savedSelection
|
||||
// styling takes precedence.
|
||||
var lineANSIAttrs []ansi.AttrSpan
|
||||
isBasicStyle := (fgAttr == l.styles.Basic.fg && bgAttr == l.styles.Basic.bg)
|
||||
isBasicStyle := (fgAttr == l.styles.Basic.Fg && bgAttr == l.styles.Basic.Bg)
|
||||
if isBasicStyle {
|
||||
if al, ok := target.(ansiLiner); ok {
|
||||
lineANSIAttrs = al.ANSIAttrs()
|
||||
|
|
@ -756,7 +741,7 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
X: x,
|
||||
Y: y,
|
||||
XOffset: xOffset,
|
||||
Fg: fgAttr | AttrBold | AttrReverse,
|
||||
Fg: fgAttr | config.AttrBold | config.AttrReverse,
|
||||
Bg: bgAttr,
|
||||
Msg: string(prefixes[n]),
|
||||
})
|
||||
|
|
@ -886,9 +871,9 @@ func (l *BasicLayout) SortTopDown() bool {
|
|||
}
|
||||
|
||||
func init() {
|
||||
RegisterLayout(LayoutTypeTopDown, LayoutBuilderFunc(DefaultLayout))
|
||||
RegisterLayout(LayoutTypeBottomUp, LayoutBuilderFunc(BottomUpLayout))
|
||||
RegisterLayout(LayoutTypeTopDownQueryBottom, LayoutBuilderFunc(TopDownQueryBottomLayout))
|
||||
RegisterLayout(config.LayoutTypeTopDown, LayoutBuilderFunc(DefaultLayout))
|
||||
RegisterLayout(config.LayoutTypeBottomUp, LayoutBuilderFunc(BottomUpLayout))
|
||||
RegisterLayout(config.LayoutTypeTopDownQueryBottom, LayoutBuilderFunc(TopDownQueryBottomLayout))
|
||||
}
|
||||
|
||||
func (l *BasicLayout) PurgeDisplayCache() {
|
||||
|
|
|
|||
135
layout_test.go
135
layout_test.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/filter"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/line"
|
||||
|
|
@ -13,16 +14,16 @@ import (
|
|||
|
||||
func TestLayoutType(t *testing.T) {
|
||||
layouts := []struct {
|
||||
value LayoutType
|
||||
value string
|
||||
expectOK bool
|
||||
}{
|
||||
{LayoutTypeTopDown, true},
|
||||
{LayoutTypeBottomUp, true},
|
||||
{LayoutTypeTopDownQueryBottom, true},
|
||||
{config.LayoutTypeTopDown, true},
|
||||
{config.LayoutTypeBottomUp, true},
|
||||
{config.LayoutTypeTopDownQueryBottom, true},
|
||||
{"foobar", false},
|
||||
}
|
||||
for _, l := range layouts {
|
||||
valid := IsValidLayoutType(l.value)
|
||||
valid := config.IsValidLayoutType(l.value)
|
||||
if valid != l.expectOK {
|
||||
t.Errorf("LayoutType %s, expected IsValidLayoutType to return %t, but got %t",
|
||||
l.value,
|
||||
|
|
@ -44,8 +45,8 @@ func TestPrintScreen(t *testing.T) {
|
|||
screen.Print(PrintArgs{
|
||||
X: initX,
|
||||
Y: initY,
|
||||
Fg: ColorDefault,
|
||||
Bg: ColorDefault,
|
||||
Fg: config.ColorDefault,
|
||||
Bg: config.ColorDefault,
|
||||
Msg: msg,
|
||||
Fill: fill,
|
||||
})
|
||||
|
|
@ -86,7 +87,7 @@ func TestPrintScreen(t *testing.T) {
|
|||
|
||||
func TestScreenStatusBar(t *testing.T) {
|
||||
screen := NewDummyScreen()
|
||||
st, err := newScreenStatusBar(screen, AnchorBottom, 0, NewStyleSet())
|
||||
st, err := newScreenStatusBar(screen, AnchorBottom, 0, config.NewStyleSet())
|
||||
require.NoError(t, err)
|
||||
st.PrintStatus("Hello, World!", 0)
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ func TestNullStatusBar(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMergeAttribute(t *testing.T) {
|
||||
colors := stringToFg
|
||||
colors := config.StringToFg
|
||||
|
||||
// merge colors
|
||||
tests := [][]string{
|
||||
|
|
@ -138,20 +139,20 @@ func TestMergeAttribute(t *testing.T) {
|
|||
}
|
||||
|
||||
// merge attributes
|
||||
if m := mergeAttribute(AttrBold|colors["red"], AttrUnderline|colors["cyan"]); m != AttrBold|AttrUnderline|colors["white"] {
|
||||
t.Errorf("expected %d, got %d", AttrBold|AttrUnderline|colors["white"], m)
|
||||
if m := mergeAttribute(config.AttrBold|colors["red"], config.AttrUnderline|colors["cyan"]); m != config.AttrBold|config.AttrUnderline|colors["white"] {
|
||||
t.Errorf("expected %d, got %d", config.AttrBold|config.AttrUnderline|colors["white"], m)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGHIssue294_PromptStyleUsedForPromptPrefix verifies that UserPrompt.Draw
|
||||
// uses the Prompt style (not Basic) when rendering the prompt prefix string.
|
||||
func TestGHIssue294_PromptStyleUsedForPromptPrefix(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
styles.Prompt.fg = ColorGreen | AttrBold
|
||||
styles.Prompt.bg = ColorBlue
|
||||
styles := config.NewStyleSet()
|
||||
styles.Prompt.Fg = config.ColorGreen | config.AttrBold
|
||||
styles.Prompt.Bg = config.ColorBlue
|
||||
// Make sure Basic is different so we can distinguish them.
|
||||
styles.Basic.fg = ColorDefault
|
||||
styles.Basic.bg = ColorDefault
|
||||
styles.Basic.Fg = config.ColorDefault
|
||||
styles.Basic.Bg = config.ColorDefault
|
||||
|
||||
screen := NewDummyScreen()
|
||||
prompt, err := NewUserPrompt(screen, AnchorTop, 0, "QUERY>", styles)
|
||||
|
|
@ -177,23 +178,23 @@ func TestGHIssue294_PromptStyleUsedForPromptPrefix(t *testing.T) {
|
|||
ev := events[i]
|
||||
x := ev[0].(int)
|
||||
ch := ev[2].(rune)
|
||||
fg := ev[3].(Attribute)
|
||||
bg := ev[4].(Attribute)
|
||||
fg := ev[3].(config.Attribute)
|
||||
bg := ev[4].(config.Attribute)
|
||||
|
||||
require.Equal(t, i, x, "expected x=%d", i)
|
||||
require.Equal(t, rune(promptStr[i]), ch, "expected character %c at position %d", promptStr[i], i)
|
||||
require.Equal(t, styles.Prompt.fg, fg,
|
||||
"cell at x=%d should use Prompt.fg, got %v", i, fg)
|
||||
require.Equal(t, styles.Prompt.bg, bg,
|
||||
"cell at x=%d should use Prompt.bg, got %v", i, bg)
|
||||
require.Equal(t, styles.Prompt.Fg, fg,
|
||||
"cell at x=%d should use Prompt.Fg, got %v", i, fg)
|
||||
require.Equal(t, styles.Prompt.Bg, bg,
|
||||
"cell at x=%d should use Prompt.Bg, got %v", i, bg)
|
||||
}
|
||||
|
||||
// The cells after the prompt should NOT use the Prompt style —
|
||||
// they should use the Query style (for the query text area).
|
||||
if len(events) > promptLen {
|
||||
ev := events[promptLen]
|
||||
fg := ev[3].(Attribute)
|
||||
require.NotEqual(t, styles.Prompt.fg, fg,
|
||||
fg := ev[3].(config.Attribute)
|
||||
require.NotEqual(t, styles.Prompt.Fg, fg,
|
||||
"cell after prompt should not use Prompt style")
|
||||
}
|
||||
}
|
||||
|
|
@ -201,12 +202,12 @@ func TestGHIssue294_PromptStyleUsedForPromptPrefix(t *testing.T) {
|
|||
// TestGHIssue460_MatchedStyleDoesNotBleedToEndOfLine verifies that matched
|
||||
// text highlighting in ListArea.Draw does not extend to the screen edge.
|
||||
func TestGHIssue460_MatchedStyleDoesNotBleedToEndOfLine(t *testing.T) {
|
||||
// Use a distinct Matched.bg so we can detect it in SetCell events.
|
||||
styles := NewStyleSet()
|
||||
styles.Matched.bg = ColorBlue
|
||||
// Use a distinct Matched.Bg so we can detect it in SetCell events.
|
||||
styles := config.NewStyleSet()
|
||||
styles.Matched.Bg = config.ColorBlue
|
||||
|
||||
matchedBg := mergeAttribute(styles.Basic.bg, styles.Matched.bg) // ColorBlue
|
||||
basicBg := styles.Basic.bg // ColorDefault
|
||||
matchedBg := mergeAttribute(styles.Basic.Bg, styles.Matched.Bg) // config.ColorBlue
|
||||
basicBg := styles.Basic.Bg // config.ColorDefault
|
||||
|
||||
// Helper: set up a Peco state with one matched line and draw it,
|
||||
// returning the SetCell events for the line's row (y=0).
|
||||
|
|
@ -256,7 +257,7 @@ func TestGHIssue460_MatchedStyleDoesNotBleedToEndOfLine(t *testing.T) {
|
|||
|
||||
for _, ev := range row {
|
||||
x := ev[0].(int)
|
||||
bg := ev[4].(Attribute)
|
||||
bg := ev[4].(config.Attribute)
|
||||
if x >= 6 && x <= 10 {
|
||||
require.Equal(t, matchedBg, bg,
|
||||
"cell at x=%d should have matched bg", x)
|
||||
|
|
@ -277,7 +278,7 @@ func TestGHIssue460_MatchedStyleDoesNotBleedToEndOfLine(t *testing.T) {
|
|||
|
||||
for _, ev := range row {
|
||||
x := ev[0].(int)
|
||||
bg := ev[4].(Attribute)
|
||||
bg := ev[4].(config.Attribute)
|
||||
if x >= 6 && x <= 10 {
|
||||
require.Equal(t, matchedBg, bg,
|
||||
"cell at x=%d should have matched bg", x)
|
||||
|
|
@ -383,7 +384,7 @@ func TestNewLayout(t *testing.T) {
|
|||
|
||||
t.Run("top-down", func(t *testing.T) {
|
||||
state := makeState()
|
||||
layout, err := NewLayout(LayoutTypeTopDown, state)
|
||||
layout, err := NewLayout(config.LayoutTypeTopDown, state)
|
||||
require.NoError(t, err)
|
||||
require.True(t, layout.SortTopDown(), "top-down layout should sort top-down")
|
||||
require.Equal(t, AnchorTop, layout.prompt.anchor, "top-down prompt should be anchored at top")
|
||||
|
|
@ -391,7 +392,7 @@ func TestNewLayout(t *testing.T) {
|
|||
|
||||
t.Run("bottom-up", func(t *testing.T) {
|
||||
state := makeState()
|
||||
layout, err := NewLayout(LayoutTypeBottomUp, state)
|
||||
layout, err := NewLayout(config.LayoutTypeBottomUp, state)
|
||||
require.NoError(t, err)
|
||||
require.False(t, layout.SortTopDown(), "bottom-up layout should not sort top-down")
|
||||
require.Equal(t, AnchorBottom, layout.prompt.anchor, "bottom-up prompt should be anchored at bottom")
|
||||
|
|
@ -399,7 +400,7 @@ func TestNewLayout(t *testing.T) {
|
|||
|
||||
t.Run("top-down-query-bottom", func(t *testing.T) {
|
||||
state := makeState()
|
||||
layout, err := NewLayout(LayoutTypeTopDownQueryBottom, state)
|
||||
layout, err := NewLayout(config.LayoutTypeTopDownQueryBottom, state)
|
||||
require.NoError(t, err)
|
||||
require.True(t, layout.SortTopDown(), "top-down-query-bottom layout should sort top-down")
|
||||
require.Equal(t, AnchorBottom, layout.prompt.anchor, "top-down-query-bottom prompt should be anchored at bottom")
|
||||
|
|
@ -443,7 +444,7 @@ func TestTopDownQueryBottomLayout(t *testing.T) {
|
|||
// TestCursorStyle verifies that UserPrompt.cursorStyle returns the correct
|
||||
// fg/bg attributes depending on QueryCursor and Query style configuration.
|
||||
func TestCursorStyle(t *testing.T) {
|
||||
makePrompt := func(t *testing.T, styles *StyleSet) UserPrompt {
|
||||
makePrompt := func(t *testing.T, styles *config.StyleSet) UserPrompt {
|
||||
t.Helper()
|
||||
screen := NewDummyScreen()
|
||||
p, err := NewUserPrompt(screen, AnchorTop, 0, "QUERY>", styles)
|
||||
|
|
@ -451,57 +452,57 @@ func TestCursorStyle(t *testing.T) {
|
|||
return *p
|
||||
}
|
||||
|
||||
t.Run("both default falls back to AttrReverse", func(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
// Query and QueryCursor are both ColorDefault (zero value)
|
||||
t.Run("both default falls back to config.AttrReverse", func(t *testing.T) {
|
||||
styles := config.NewStyleSet()
|
||||
// Query and QueryCursor are both config.ColorDefault (zero value)
|
||||
p := makePrompt(t, styles)
|
||||
fg, bg := p.cursorStyle()
|
||||
require.Equal(t, ColorDefault|AttrReverse, fg)
|
||||
require.Equal(t, ColorDefault|AttrReverse, bg)
|
||||
require.Equal(t, config.ColorDefault|config.AttrReverse, fg)
|
||||
require.Equal(t, config.ColorDefault|config.AttrReverse, bg)
|
||||
})
|
||||
|
||||
t.Run("Query has custom colors and QueryCursor is default swaps fg/bg", func(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
styles.Query.fg = ColorYellow
|
||||
styles.Query.bg = ColorBlue
|
||||
styles := config.NewStyleSet()
|
||||
styles.Query.Fg = config.ColorYellow
|
||||
styles.Query.Bg = config.ColorBlue
|
||||
p := makePrompt(t, styles)
|
||||
fg, bg := p.cursorStyle()
|
||||
require.Equal(t, ColorBlue, fg, "should use Query.bg as cursor fg")
|
||||
require.Equal(t, ColorYellow, bg, "should use Query.fg as cursor bg")
|
||||
require.Equal(t, config.ColorBlue, fg, "should use Query.Bg as cursor fg")
|
||||
require.Equal(t, config.ColorYellow, bg, "should use Query.Fg as cursor bg")
|
||||
})
|
||||
|
||||
t.Run("QueryCursor explicitly set takes precedence", func(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
styles.Query.fg = ColorYellow
|
||||
styles.Query.bg = ColorBlue
|
||||
styles.QueryCursor.fg = ColorWhite
|
||||
styles.QueryCursor.bg = ColorRed
|
||||
styles := config.NewStyleSet()
|
||||
styles.Query.Fg = config.ColorYellow
|
||||
styles.Query.Bg = config.ColorBlue
|
||||
styles.QueryCursor.Fg = config.ColorWhite
|
||||
styles.QueryCursor.Bg = config.ColorRed
|
||||
p := makePrompt(t, styles)
|
||||
fg, bg := p.cursorStyle()
|
||||
require.Equal(t, ColorWhite, fg)
|
||||
require.Equal(t, ColorRed, bg)
|
||||
require.Equal(t, config.ColorWhite, fg)
|
||||
require.Equal(t, config.ColorRed, bg)
|
||||
})
|
||||
|
||||
t.Run("QueryCursor with only fg set takes precedence", func(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
styles.Query.fg = ColorYellow
|
||||
styles.Query.bg = ColorBlue
|
||||
styles.QueryCursor.fg = ColorGreen
|
||||
// bg remains ColorDefault
|
||||
styles := config.NewStyleSet()
|
||||
styles.Query.Fg = config.ColorYellow
|
||||
styles.Query.Bg = config.ColorBlue
|
||||
styles.QueryCursor.Fg = config.ColorGreen
|
||||
// bg remains config.ColorDefault
|
||||
p := makePrompt(t, styles)
|
||||
fg, bg := p.cursorStyle()
|
||||
require.Equal(t, ColorGreen, fg)
|
||||
require.Equal(t, ColorDefault, bg)
|
||||
require.Equal(t, config.ColorGreen, fg)
|
||||
require.Equal(t, config.ColorDefault, bg)
|
||||
})
|
||||
|
||||
t.Run("Query has only fg set swaps correctly", func(t *testing.T) {
|
||||
styles := NewStyleSet()
|
||||
styles.Query.fg = ColorRed
|
||||
// Query.bg remains ColorDefault
|
||||
styles := config.NewStyleSet()
|
||||
styles.Query.Fg = config.ColorRed
|
||||
// Query.Bg remains config.ColorDefault
|
||||
p := makePrompt(t, styles)
|
||||
fg, bg := p.cursorStyle()
|
||||
require.Equal(t, ColorDefault, fg, "should use Query.bg (default) as cursor fg")
|
||||
require.Equal(t, ColorRed, bg, "should use Query.fg as cursor bg")
|
||||
require.Equal(t, config.ColorDefault, fg, "should use Query.Bg (default) as cursor fg")
|
||||
require.Equal(t, config.ColorRed, bg, "should use Query.Fg as cursor bg")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -518,17 +519,17 @@ func TestInvalidAnchorReturnsError(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("NewUserPrompt", func(t *testing.T) {
|
||||
_, err := NewUserPrompt(screen, invalidAnchor, 0, "QUERY>", NewStyleSet())
|
||||
_, err := NewUserPrompt(screen, invalidAnchor, 0, "QUERY>", config.NewStyleSet())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("NewListArea", func(t *testing.T) {
|
||||
_, err := NewListArea(screen, invalidAnchor, 0, true, NewStyleSet())
|
||||
_, err := NewListArea(screen, invalidAnchor, 0, true, config.NewStyleSet())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("newScreenStatusBar", func(t *testing.T) {
|
||||
_, err := newScreenStatusBar(screen, invalidAnchor, 0, NewStyleSet())
|
||||
_, err := newScreenStatusBar(screen, invalidAnchor, 0, config.NewStyleSet())
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/peco/peco/config"
|
||||
)
|
||||
|
||||
// CLIOptions holds the command-line flags parsed by go-flags.
|
||||
|
|
@ -53,7 +54,7 @@ func (options *CLIOptions) parse(s []string) ([]string, error) {
|
|||
// Validate checks the parsed CLI options for correctness (e.g., layout type).
|
||||
func (options CLIOptions) Validate() error {
|
||||
if options.OptLayout != "" {
|
||||
if !IsValidLayoutType(LayoutType(options.OptLayout)) {
|
||||
if !config.IsValidLayoutType(options.OptLayout) {
|
||||
return errors.New("unknown layout: '" + options.OptLayout + "'")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
peco.go
29
peco.go
|
|
@ -17,6 +17,7 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/lestrrat-go/pdebug"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/filter"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/internal/util"
|
||||
|
|
@ -53,7 +54,7 @@ type Peco struct {
|
|||
bufferSize int
|
||||
caret query.Caret
|
||||
// Config contains the values read in from config file
|
||||
config Config
|
||||
config config.Config
|
||||
currentLineBuffer Buffer
|
||||
enableSep bool // Enable parsing on separators
|
||||
execOnFinish string
|
||||
|
|
@ -67,7 +68,7 @@ type Peco struct {
|
|||
location Location
|
||||
maxScanBufferSize int
|
||||
mutex sync.Mutex
|
||||
onCancel OnCancelBehavior
|
||||
onCancel config.OnCancelBehavior
|
||||
printQuery bool
|
||||
prompt string
|
||||
query query.Text
|
||||
|
|
@ -83,9 +84,9 @@ type Peco struct {
|
|||
selectOneTriggered atomic.Bool
|
||||
selectAllAndExit bool // True if --select-all is enabled
|
||||
singleKeyJump SingleKeyJumpState
|
||||
heightSpec *HeightSpec
|
||||
heightSpec *config.HeightSpec
|
||||
configReader ConfigReader
|
||||
styles StyleSet
|
||||
styles config.StyleSet
|
||||
enableANSI bool // Enable ANSI color code support
|
||||
fuzzyLongestSort bool
|
||||
|
||||
|
|
@ -225,7 +226,7 @@ func (p *Peco) Screen() Screen {
|
|||
return p.screen
|
||||
}
|
||||
|
||||
func (p *Peco) Styles() *StyleSet {
|
||||
func (p *Peco) Styles() *config.StyleSet {
|
||||
return &p.styles
|
||||
}
|
||||
|
||||
|
|
@ -625,7 +626,7 @@ func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string)
|
|||
}
|
||||
|
||||
if opts.OptRcfile == "" {
|
||||
if file, err := LocateRcfile(defaultConfigLocator); err == nil {
|
||||
if file, err := config.LocateRcfile(config.DefaultConfigLocator); err == nil {
|
||||
opts.OptRcfile = file
|
||||
}
|
||||
}
|
||||
|
|
@ -687,23 +688,23 @@ func (p *Peco) SetupSource(ctx context.Context) (s *Source, err error) {
|
|||
|
||||
// ConfigReader reads configuration from a file into a Config struct.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(*Config, string) error
|
||||
ReadConfig(*config.Config, string) error
|
||||
}
|
||||
|
||||
// ConfigReaderFunc is a function that implements ConfigReader.
|
||||
type ConfigReaderFunc func(*Config, string) error
|
||||
type ConfigReaderFunc func(*config.Config, string) error
|
||||
|
||||
// ReadConfig calls the underlying function.
|
||||
func (f ConfigReaderFunc) ReadConfig(cfg *Config, filename string) error {
|
||||
func (f ConfigReaderFunc) ReadConfig(cfg *config.Config, filename string) error {
|
||||
return f(cfg, filename)
|
||||
}
|
||||
|
||||
// nopConfigReader is a ConfigReader that does nothing.
|
||||
var nopConfigReader = ConfigReaderFunc(func(*Config, string) error { return nil })
|
||||
var nopConfigReader = ConfigReaderFunc(func(*config.Config, string) error { return nil })
|
||||
|
||||
// defaultConfigReader loads the configuration from the given filename into cfg.
|
||||
// If filename is empty, no file is read and nil is returned.
|
||||
var defaultConfigReader = ConfigReaderFunc(func(cfg *Config, filename string) error {
|
||||
var defaultConfigReader = ConfigReaderFunc(func(cfg *config.Config, filename string) error {
|
||||
if filename != "" {
|
||||
if err := cfg.ReadFilename(filename); err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
|
|
@ -721,7 +722,7 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
|
|||
if v := p.config.Layout; v != "" {
|
||||
p.layoutType = v
|
||||
} else {
|
||||
p.layoutType = DefaultLayoutType
|
||||
p.layoutType = config.DefaultLayoutType
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -754,7 +755,7 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
|
|||
|
||||
p.onCancel = p.config.OnCancel
|
||||
if p.onCancel == "" {
|
||||
p.onCancel = OnCancelSuccess
|
||||
p.onCancel = config.OnCancelSuccess
|
||||
}
|
||||
if opts.OptOnCancel != "" {
|
||||
if err := p.onCancel.UnmarshalText([]byte(opts.OptOnCancel)); err != nil {
|
||||
|
|
@ -786,7 +787,7 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
|
|||
heightStr = v
|
||||
}
|
||||
if heightStr != "" {
|
||||
spec, err := ParseHeightSpec(heightStr)
|
||||
spec, err := config.ParseHeightSpec(heightStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse height specification: %w", err)
|
||||
}
|
||||
|
|
|
|||
22
peco_test.go
22
peco_test.go
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/lestrrat-go/pdebug"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/internal/keyseq"
|
||||
"github.com/peco/peco/internal/util"
|
||||
|
|
@ -151,7 +152,7 @@ func NewDummyScreen() *SimScreen {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SimScreen) Init(_ *Config) error {
|
||||
func (s *SimScreen) Init(_ *config.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +221,7 @@ func (s *SimScreen) SendEvent(e Event) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SimScreen) SetCell(x, y int, ch rune, fg, bg Attribute) {
|
||||
func (s *SimScreen) SetCell(x, y int, ch rune, fg, bg config.Attribute) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
|
|
@ -242,7 +243,7 @@ func (s *SimScreen) Flush() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *SimScreen) PollEvent(ctx context.Context, _ *Config) chan Event {
|
||||
func (s *SimScreen) PollEvent(ctx context.Context, _ *config.Config) chan Event {
|
||||
evCh := make(chan Event)
|
||||
go func() {
|
||||
defer func() {
|
||||
|
|
@ -330,6 +331,15 @@ func TestPecoHelp(t *testing.T) {
|
|||
require.True(t, util.IsIgnorableError(err), "p.Run() should return error with Ignorable() method, and it should return true")
|
||||
}
|
||||
|
||||
func TestOnCancelInvalidCLIOption(t *testing.T) {
|
||||
p := newPeco()
|
||||
var opts CLIOptions
|
||||
opts.OptOnCancel = "bogus"
|
||||
err := p.ApplyConfig(opts)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "bogus")
|
||||
}
|
||||
|
||||
func TestGHIssue331(t *testing.T) {
|
||||
// Verify fields are populated when Run() initializes config.
|
||||
state, _ := setupPecoTest(t)
|
||||
|
|
@ -392,7 +402,7 @@ func TestApplyConfig(t *testing.T) {
|
|||
require.Equal(t, opts.OptSelect1, p.selectOneAndExit, "p.selectOneAndExit should be equal to opts.OptSelect1")
|
||||
require.Equal(t, opts.OptExitZero, p.exitZeroAndExit, "p.exitZeroAndExit should be equal to opts.OptExitZero")
|
||||
require.Equal(t, opts.OptSelectAll, p.selectAllAndExit, "p.selectAllAndExit should be equal to opts.OptSelectAll")
|
||||
require.Equal(t, OnCancelBehavior(opts.OptOnCancel), p.onCancel, "p.onCancel should be equal to opts.OptOnCancel")
|
||||
require.Equal(t, config.OnCancelBehavior(opts.OptOnCancel), p.onCancel, "p.onCancel should be equal to opts.OptOnCancel")
|
||||
require.Equal(t, opts.OptSelectionPrefix, p.selectionPrefix, "p.selectionPrefix should be equal to opts.OptSelectionPrefix")
|
||||
require.Equal(t, opts.OptPrintQuery, p.printQuery, "p.printQuery should be equal to opts.OptPrintQuery")
|
||||
require.Equal(t, opts.OptExec, p.execOnFinish, "p.execOnFinish should be equal to opts.OptExec")
|
||||
|
|
@ -464,12 +474,12 @@ func TestApplyConfig(t *testing.T) {
|
|||
|
||||
t.Run("Config OnCancel used when CLI option absent", func(t *testing.T) {
|
||||
p := newPeco()
|
||||
p.config.OnCancel = OnCancelError
|
||||
p.config.OnCancel = config.OnCancelError
|
||||
|
||||
var opts CLIOptions
|
||||
require.NoError(t, p.ApplyConfig(opts), "p.ApplyConfig should succeed")
|
||||
|
||||
require.Equal(t, OnCancelError, p.onCancel, "p.onCancel should come from config when CLI option is absent")
|
||||
require.Equal(t, config.OnCancelError, p.onCancel, "p.onCancel should come from config when CLI option is absent")
|
||||
})
|
||||
|
||||
t.Run("Config SelectionPrefix used when CLI option absent", func(t *testing.T) {
|
||||
|
|
|
|||
37
screen.go
37
screen.go
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/gdamore/tcell/v2"
|
||||
pdebug "github.com/lestrrat-go/pdebug"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/internal/ansi"
|
||||
"github.com/peco/peco/internal/keyseq"
|
||||
)
|
||||
|
|
@ -19,13 +20,13 @@ import (
|
|||
// Screen hides the terminal library from the consuming code so that
|
||||
// it can be swapped out for testing
|
||||
type Screen interface {
|
||||
Init(*Config) error
|
||||
Init(*config.Config) error
|
||||
Close() error
|
||||
Flush() error
|
||||
PollEvent(context.Context, *Config) chan Event
|
||||
PollEvent(context.Context, *config.Config) chan Event
|
||||
Print(PrintArgs) int
|
||||
Resume(context.Context) error
|
||||
SetCell(int, int, rune, Attribute, Attribute)
|
||||
SetCell(int, int, rune, config.Attribute, config.Attribute)
|
||||
SetCursor(int, int)
|
||||
Size() (int, int)
|
||||
SendEvent(Event)
|
||||
|
|
@ -212,8 +213,8 @@ func tcellEventToEvent(tev tcell.Event) Event {
|
|||
}
|
||||
|
||||
// attributeToTcellColor converts a peco Attribute to a tcell.Color.
|
||||
func attributeToTcellColor(attr Attribute) tcell.Color {
|
||||
if attr&AttrTrueColor != 0 {
|
||||
func attributeToTcellColor(attr config.Attribute) tcell.Color {
|
||||
if attr&config.AttrTrueColor != 0 {
|
||||
rgb := attr & 0x00FFFFFF
|
||||
return tcell.NewHexColor(int32(rgb))
|
||||
}
|
||||
|
|
@ -225,27 +226,27 @@ func attributeToTcellColor(attr Attribute) tcell.Color {
|
|||
}
|
||||
|
||||
// attributeToTcellStyle converts peco Attribute fg/bg values to a tcell.Style.
|
||||
func attributeToTcellStyle(fg, bg Attribute) tcell.Style {
|
||||
func attributeToTcellStyle(fg, bg config.Attribute) tcell.Style {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(attributeToTcellColor(fg)).
|
||||
Background(attributeToTcellColor(bg))
|
||||
|
||||
// Extract style attributes from both fg and bg
|
||||
attrs := fg | bg
|
||||
if attrs&AttrBold != 0 {
|
||||
if attrs&config.AttrBold != 0 {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
if attrs&AttrUnderline != 0 {
|
||||
if attrs&config.AttrUnderline != 0 {
|
||||
style = style.Underline(true)
|
||||
}
|
||||
if attrs&AttrReverse != 0 {
|
||||
if attrs&config.AttrReverse != 0 {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
func (t *TcellScreen) Init(_ *Config) error {
|
||||
func (t *TcellScreen) Init(_ *config.Config) error {
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tcell screen: %w", err)
|
||||
|
|
@ -338,7 +339,7 @@ func (t *TcellScreen) Sync() {
|
|||
// PollEvent returns a channel that you can listen to for
|
||||
// terminal events. The actual polling is done in a
|
||||
// separate goroutine
|
||||
func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
|
||||
func (t *TcellScreen) PollEvent(ctx context.Context, cfg *config.Config) chan Event {
|
||||
evCh := make(chan Event)
|
||||
|
||||
go func() {
|
||||
|
|
@ -449,7 +450,7 @@ func (t *TcellScreen) Resume(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// SetCell writes to the terminal
|
||||
func (t *TcellScreen) SetCell(x, y int, ch rune, fg, bg Attribute) {
|
||||
func (t *TcellScreen) SetCell(x, y int, ch rune, fg, bg config.Attribute) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
if t.screen == nil {
|
||||
|
|
@ -473,8 +474,8 @@ type PrintArgs struct {
|
|||
X int
|
||||
XOffset int
|
||||
Y int
|
||||
Fg Attribute
|
||||
Bg Attribute
|
||||
Fg config.Attribute
|
||||
Bg config.Attribute
|
||||
Msg string
|
||||
Fill bool
|
||||
ANSIAttrs []ansi.AttrSpan // per-character ANSI attributes for this segment
|
||||
|
|
@ -512,11 +513,11 @@ func screenPrint(t Screen, args PrintArgs) int {
|
|||
efg, ebg := fg, bg
|
||||
if ansiAttrs != nil && spanIdx < len(ansiAttrs) {
|
||||
span := ansiAttrs[spanIdx]
|
||||
if Attribute(span.Fg) != ColorDefault {
|
||||
efg = Attribute(span.Fg)
|
||||
if config.Attribute(span.Fg) != config.ColorDefault {
|
||||
efg = config.Attribute(span.Fg)
|
||||
}
|
||||
if Attribute(span.Bg) != ColorDefault {
|
||||
ebg = Attribute(span.Bg)
|
||||
if config.Attribute(span.Bg) != config.ColorDefault {
|
||||
ebg = config.Attribute(span.Bg)
|
||||
}
|
||||
spanPos++
|
||||
if spanPos >= span.Length {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
pdebug "github.com/lestrrat-go/pdebug"
|
||||
"github.com/peco/peco/config"
|
||||
)
|
||||
|
||||
// InlineScreen implements the Screen interface for rendering peco in a
|
||||
|
|
@ -18,7 +19,7 @@ import (
|
|||
type InlineScreen struct {
|
||||
mutex sync.Mutex
|
||||
screen tcell.Screen
|
||||
heightSpec HeightSpec
|
||||
heightSpec config.HeightSpec
|
||||
height int // resolved line count
|
||||
yOffset int // physical row where inline region starts
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ type InlineScreen struct {
|
|||
}
|
||||
|
||||
// NewInlineScreen creates a new InlineScreen with the given height spec.
|
||||
func NewInlineScreen(spec HeightSpec) *InlineScreen {
|
||||
func NewInlineScreen(spec config.HeightSpec) *InlineScreen {
|
||||
return &InlineScreen{
|
||||
heightSpec: spec,
|
||||
errWriter: os.Stderr,
|
||||
|
|
@ -38,7 +39,7 @@ func NewInlineScreen(spec HeightSpec) *InlineScreen {
|
|||
}
|
||||
|
||||
// Init initializes the tcell screen for inline mode, disabling the alternate screen buffer.
|
||||
func (s *InlineScreen) Init(_ *Config) error {
|
||||
func (s *InlineScreen) Init(_ *config.Config) error {
|
||||
// Save and override TCELL_ALTSCREEN to prevent alternate screen buffer
|
||||
s.savedAltscreen = os.Getenv("TCELL_ALTSCREEN")
|
||||
os.Setenv("TCELL_ALTSCREEN", "disable")
|
||||
|
|
@ -119,7 +120,7 @@ func (s *InlineScreen) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *InlineScreen) SetCell(x, y int, ch rune, fg, bg Attribute) {
|
||||
func (s *InlineScreen) SetCell(x, y int, ch rune, fg, bg config.Attribute) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if s.screen == nil {
|
||||
|
|
@ -173,7 +174,7 @@ func (s *InlineScreen) Size() (int, int) {
|
|||
}
|
||||
|
||||
// PollEvent creates an event channel and polls for terminal events with special resize handling.
|
||||
func (s *InlineScreen) PollEvent(ctx context.Context, _ *Config) chan Event {
|
||||
func (s *InlineScreen) PollEvent(ctx context.Context, _ *config.Config) chan Event {
|
||||
evCh := make(chan Event)
|
||||
|
||||
go func() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ func newTestInlineScreen(termWidth, termHeight, inlineHeight int) (*InlineScreen
|
|||
sim.SetSize(termWidth, termHeight)
|
||||
|
||||
s := &InlineScreen{
|
||||
heightSpec: HeightSpec{Value: inlineHeight, IsPercent: false},
|
||||
heightSpec: config.HeightSpec{Value: inlineHeight, IsPercent: false},
|
||||
screen: sim,
|
||||
height: inlineHeight,
|
||||
yOffset: termHeight - inlineHeight,
|
||||
|
|
@ -39,7 +40,7 @@ func TestInlineScreenSetCell(t *testing.T) {
|
|||
defer s.screen.Fini()
|
||||
|
||||
// SetCell at virtual y=0 should map to physical y=14 (24-10)
|
||||
s.SetCell(5, 0, 'A', ColorDefault, ColorDefault)
|
||||
s.SetCell(5, 0, 'A', config.ColorDefault, config.ColorDefault)
|
||||
s.Flush()
|
||||
|
||||
// Read back from simulation screen at physical coordinates
|
||||
|
|
@ -47,7 +48,7 @@ func TestInlineScreenSetCell(t *testing.T) {
|
|||
require.Equal(t, "A", str)
|
||||
|
||||
// SetCell at virtual y=9 (last line) should map to physical y=23
|
||||
s.SetCell(10, 9, 'Z', ColorDefault, ColorDefault)
|
||||
s.SetCell(10, 9, 'Z', config.ColorDefault, config.ColorDefault)
|
||||
s.Flush()
|
||||
|
||||
str, _, _ = sim.Get(10, 23)
|
||||
|
|
@ -110,7 +111,7 @@ func TestInlineScreenNilSafety(t *testing.T) {
|
|||
require.Equal(t, 0, w)
|
||||
require.Equal(t, 0, h)
|
||||
|
||||
s.SetCell(0, 0, 'X', ColorDefault, ColorDefault)
|
||||
s.SetCell(0, 0, 'X', config.ColorDefault, config.ColorDefault)
|
||||
s.SetCursor(0, 0)
|
||||
require.NoError(t, s.Flush())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/peco/peco/config"
|
||||
"github.com/peco/peco/internal/keyseq"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -25,18 +26,18 @@ type recordingScreen struct {
|
|||
w, h int
|
||||
}
|
||||
|
||||
func (s *recordingScreen) Init(*Config) error { return nil }
|
||||
func (s *recordingScreen) Close() error { return nil }
|
||||
func (s *recordingScreen) Flush() error { return nil }
|
||||
func (s *recordingScreen) PollEvent(context.Context, *Config) chan Event { return nil }
|
||||
func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) }
|
||||
func (s *recordingScreen) Resume(context.Context) error { return nil }
|
||||
func (s *recordingScreen) SetCursor(int, int) {}
|
||||
func (s *recordingScreen) SendEvent(Event) {}
|
||||
func (s *recordingScreen) Suspend() {}
|
||||
func (s *recordingScreen) Sync() {}
|
||||
func (s *recordingScreen) Size() (int, int) { return s.w, s.h }
|
||||
func (s *recordingScreen) SetCell(x, y int, ch rune, _, _ Attribute) {
|
||||
func (s *recordingScreen) Init(*config.Config) error { return nil }
|
||||
func (s *recordingScreen) Close() error { return nil }
|
||||
func (s *recordingScreen) Flush() error { return nil }
|
||||
func (s *recordingScreen) PollEvent(context.Context, *config.Config) chan Event { return nil }
|
||||
func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) }
|
||||
func (s *recordingScreen) Resume(context.Context) error { return nil }
|
||||
func (s *recordingScreen) SetCursor(int, int) {}
|
||||
func (s *recordingScreen) SendEvent(Event) {}
|
||||
func (s *recordingScreen) Suspend() {}
|
||||
func (s *recordingScreen) Sync() {}
|
||||
func (s *recordingScreen) Size() (int, int) { return s.w, s.h }
|
||||
func (s *recordingScreen) SetCell(x, y int, ch rune, _, _ config.Attribute) {
|
||||
s.cells = append(s.cells, setCellCall{x: x, y: y, ch: ch})
|
||||
}
|
||||
|
||||
|
|
|
|||
2
view.go
2
view.go
|
|
@ -15,7 +15,7 @@ type View struct {
|
|||
|
||||
// NewView creates a new View with the given state and its configured layout.
|
||||
func NewView(state *Peco) (*View, error) {
|
||||
layout, err := NewLayout(LayoutType(state.LayoutType()), state)
|
||||
layout, err := NewLayout(state.LayoutType(), state)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create layout: %w", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue