Color customization (#245)

This commit is contained in:
Junegunn Choi 2015-05-31 16:46:54 +09:00
parent 446e822723
commit fdbfe36c0b
5 changed files with 257 additions and 90 deletions

View File

@ -91,21 +91,38 @@ Enable processing of ANSI color codes
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
.B "--color=COL" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color scheme: [dark|light|16|bw] Color configuration. The name of the base color scheme is followed by custom
.br color mappings. Ansi color code of -1 denotes terminal default
(default: dark on 256-color terminal, otherwise 16) foreground/background color.
.br
.R "" .RS
.br e.g. \fBfzf --color=bg+:24\fR
.BR dark " Color scheme for dark 256-color terminal" \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.br .RE
.BR light " Color scheme for light 256-color terminal"
.br .RS
.BR 16 " Color scheme for 16-color terminal" .B BASE SCHEME:
.br (default: dark on 256-color terminal, otherwise 16)
.BR bw " No colors"
.br \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
.RE
.TP .TP
.B "--black" .B "--black"
Use black background Use black background

View File

@ -106,15 +106,18 @@ const (
) )
type ColorTheme struct { type ColorTheme struct {
darkBg C.short UseDefault bool
prompt C.short Fg int16
match C.short Bg int16
current C.short DarkBg int16
currentMatch C.short Prompt int16
spinner C.short Match int16
info C.short Current int16
cursor C.short CurrentMatch int16
selected C.short Spinner int16
Info int16
Cursor int16
Selected int16
} }
type Event struct { type Event struct {
@ -142,7 +145,10 @@ var (
Default16 *ColorTheme Default16 *ColorTheme
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
DarkBG C.short FG int
CurrentFG int
BG int
DarkBG int
) )
func init() { func init() {
@ -150,35 +156,44 @@ func init() {
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int) _colorMap = make(map[int]int)
Default16 = &ColorTheme{ Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK, UseDefault: true,
prompt: C.COLOR_BLUE, Fg: 15,
match: C.COLOR_GREEN, Bg: 0,
current: C.COLOR_YELLOW, DarkBg: C.COLOR_BLACK,
currentMatch: C.COLOR_GREEN, Prompt: C.COLOR_BLUE,
spinner: C.COLOR_GREEN, Match: C.COLOR_GREEN,
info: C.COLOR_WHITE, Current: C.COLOR_YELLOW,
cursor: C.COLOR_RED, CurrentMatch: C.COLOR_GREEN,
selected: C.COLOR_MAGENTA} Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
darkBg: 236, UseDefault: true,
prompt: 110, Fg: 15,
match: 108, Bg: 0,
current: 254, DarkBg: 236,
currentMatch: 151, Prompt: 110,
spinner: 148, Match: 108,
info: 144, Current: 254,
cursor: 161, CurrentMatch: 151,
selected: 168} Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
darkBg: 251, UseDefault: true,
prompt: 25, Fg: 15,
match: 66, Bg: 0,
current: 237, DarkBg: 251,
currentMatch: 23, Prompt: 25,
spinner: 65, Match: 66,
info: 101, Current: 237,
cursor: 161, CurrentMatch: 23,
selected: 168} Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@ -268,23 +283,35 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
func initPairs(theme *ColorTheme, black bool) { func initPairs(theme *ColorTheme, black bool) {
var bg C.short fg := C.short(theme.Fg)
bg := C.short(theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else { } else if theme.UseDefault {
C.use_default_colors() fg = -1
bg = -1 bg = -1
C.use_default_colors()
}
if theme.UseDefault {
FG = -1
BG = -1
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
} }
DarkBG = theme.darkBg CurrentFG = int(theme.Current)
C.init_pair(ColPrompt, theme.prompt, bg) DarkBG = int(theme.DarkBg)
C.init_pair(ColMatch, theme.match, bg) darkBG := C.short(DarkBG)
C.init_pair(ColCurrent, theme.current, DarkBG) C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) C.init_pair(ColMatch, C.short(theme.Match), bg)
C.init_pair(ColSpinner, theme.spinner, bg) C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
C.init_pair(ColInfo, theme.info, bg) C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
C.init_pair(ColCursor, theme.cursor, DarkBG) C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
C.init_pair(ColSelected, theme.selected, DarkBG) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
} }
func Close() { func Close() {

View File

@ -143,13 +143,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else { } else {
ansi := item.colors[curr-1] ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg bg := ansi.color.bg
if current && bg == -1 { if bg == -1 {
bg = int(curses.DarkBG) if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
} }
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) bold: ansi.color.bold || bold})
} }
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -35,8 +36,7 @@ const usage = `usage: fzf [options]
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COL Color scheme; [dark|light|16|bw] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
(default: dark on 256-color terminal, otherwise 16)
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
@ -121,14 +121,14 @@ type Options struct {
Version bool Version bool
} }
func defaultOptions() *Options { func defaultTheme() *curses.ColorTheme {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") { if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256 return curses.Dark256
} else {
defaultTheme = curses.Default16
} }
return curses.Default16
}
func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
@ -141,7 +141,7 @@ func defaultOptions() *Options {
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme, Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Hscroll: true, Hscroll: true,
@ -187,6 +187,14 @@ func nextString(args []string, i *int, message string) string {
return args[*i] return args[*i]
} }
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
*i++
return args[*i]
}
return ""
}
func optionalNumeric(args []string, i *int) int { func optionalNumeric(args []string, i *int) int {
if len(args) > *i+1 { if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 { if strings.IndexAny(args[*i+1], "0123456789") == 0 {
@ -277,20 +285,72 @@ func parseTiebreak(str string) tiebreak {
return byLength return byLength
} }
func parseTheme(str string) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
switch strings.ToLower(str) { dupe := *theme
case "dark": return &dupe
return curses.Dark256 }
case "light":
return curses.Light256 func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
case "16": theme := dupeTheme(defaultTheme)
return curses.Default16 for _, str := range strings.Split(strings.ToLower(str), ",") {
case "bw", "no": switch str {
return nil case "dark":
default: theme = dupeTheme(curses.Dark256)
errorExit("invalid color scheme: " + str) case "light":
theme = dupeTheme(curses.Light256)
case "16":
theme = dupeTheme(curses.Default16)
case "bw", "no":
theme = nil
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
errorExit("colors disabled; cannot customize colors")
}
pair := strings.Split(str, ":")
if len(pair) != 2 {
fail()
}
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "hl":
theme.Match = ansi
case "hl+":
theme.CurrentMatch = ansi
case "prompt":
theme.Prompt = ansi
case "spinner":
theme.Spinner = ansi
case "info":
theme.Info = ansi
case "pointer":
theme.Cursor = ansi
case "marker":
theme.Selected = ansi
default:
fail()
}
}
} }
return nil return theme
} }
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
@ -400,7 +460,12 @@ func parseOptions(opts *Options, allArgs []string) {
case "--bind": case "--bind":
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = defaultTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
case "--toggle-sort": case "--toggle-sort":
opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true opts.ToggleSort = true
@ -497,7 +562,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value) opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value)
} else { } else {

View File

@ -155,3 +155,49 @@ func TestBind(t *testing.T) {
} }
check(actAbort, keymap[curses.F1]) check(actAbort, keymap[curses.F1])
} }
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
if dark == theme {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}