From fdbfe36c0b882a4e948fafd1949956341607b1e5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 31 May 2015 16:46:54 +0900 Subject: [PATCH] Color customization (#245) --- man/man1/fzf.1 | 47 ++++++++++------ src/curses/curses.go | 125 ++++++++++++++++++++++++++----------------- src/item.go | 18 +++++-- src/options.go | 111 ++++++++++++++++++++++++++++++-------- src/options_test.go | 46 ++++++++++++++++ 5 files changed, 257 insertions(+), 90 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1a9720b..9f652e0 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -91,21 +91,38 @@ Enable processing of ANSI color codes .B "--no-mouse" Disable mouse .TP -.B "--color=COL" -Color scheme: [dark|light|16|bw] -.br -(default: dark on 256-color terminal, otherwise 16) -.br -.R "" -.br -.BR dark " Color scheme for dark 256-color terminal" -.br -.BR light " Color scheme for light 256-color terminal" -.br -.BR 16 " Color scheme for 16-color terminal" -.br -.BR bw " No colors" -.br +.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" +Color configuration. The name of the base color scheme is followed by custom +color mappings. Ansi color code of -1 denotes terminal default +foreground/background color. + +.RS +e.g. \fBfzf --color=bg+:24\fR + \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR +.RE + +.RS +.B BASE SCHEME: + (default: dark on 256-color terminal, otherwise 16) + + \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 .B "--black" Use black background diff --git a/src/curses/curses.go b/src/curses/curses.go index 44fab4f..985dd87 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -106,15 +106,18 @@ const ( ) type ColorTheme struct { - darkBg C.short - prompt C.short - match C.short - current C.short - currentMatch C.short - spinner C.short - info C.short - cursor C.short - selected C.short + UseDefault bool + Fg int16 + Bg int16 + DarkBg int16 + Prompt int16 + Match int16 + Current int16 + CurrentMatch int16 + Spinner int16 + Info int16 + Cursor int16 + Selected int16 } type Event struct { @@ -142,7 +145,10 @@ var ( Default16 *ColorTheme Dark256 *ColorTheme Light256 *ColorTheme - DarkBG C.short + FG int + CurrentFG int + BG int + DarkBG int ) func init() { @@ -150,35 +156,44 @@ func init() { _clickY = []int{} _colorMap = make(map[int]int) Default16 = &ColorTheme{ - darkBg: C.COLOR_BLACK, - prompt: C.COLOR_BLUE, - match: C.COLOR_GREEN, - current: C.COLOR_YELLOW, - currentMatch: C.COLOR_GREEN, - spinner: C.COLOR_GREEN, - info: C.COLOR_WHITE, - cursor: C.COLOR_RED, - selected: C.COLOR_MAGENTA} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: C.COLOR_BLACK, + Prompt: C.COLOR_BLUE, + Match: C.COLOR_GREEN, + Current: C.COLOR_YELLOW, + CurrentMatch: C.COLOR_GREEN, + Spinner: C.COLOR_GREEN, + Info: C.COLOR_WHITE, + Cursor: C.COLOR_RED, + Selected: C.COLOR_MAGENTA} Dark256 = &ColorTheme{ - darkBg: 236, - prompt: 110, - match: 108, - current: 254, - currentMatch: 151, - spinner: 148, - info: 144, - cursor: 161, - selected: 168} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: 236, + Prompt: 110, + Match: 108, + Current: 254, + CurrentMatch: 151, + Spinner: 148, + Info: 144, + Cursor: 161, + Selected: 168} Light256 = &ColorTheme{ - darkBg: 251, - prompt: 25, - match: 66, - current: 237, - currentMatch: 23, - spinner: 65, - info: 101, - cursor: 161, - selected: 168} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: 251, + Prompt: 25, + Match: 66, + Current: 237, + CurrentMatch: 23, + Spinner: 65, + Info: 101, + Cursor: 161, + Selected: 168} } 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) { - var bg C.short + fg := C.short(theme.Fg) + bg := C.short(theme.Bg) if black { bg = C.COLOR_BLACK - } else { - C.use_default_colors() + } else if theme.UseDefault { + fg = -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 - C.init_pair(ColPrompt, theme.prompt, bg) - C.init_pair(ColMatch, theme.match, bg) - C.init_pair(ColCurrent, theme.current, DarkBG) - C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) - C.init_pair(ColSpinner, theme.spinner, bg) - C.init_pair(ColInfo, theme.info, bg) - C.init_pair(ColCursor, theme.cursor, DarkBG) - C.init_pair(ColSelected, theme.selected, DarkBG) + CurrentFG = int(theme.Current) + DarkBG = int(theme.DarkBg) + darkBG := C.short(DarkBG) + C.init_pair(ColPrompt, C.short(theme.Prompt), bg) + C.init_pair(ColMatch, C.short(theme.Match), bg) + C.init_pair(ColCurrent, C.short(theme.Current), darkBG) + C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG) + C.init_pair(ColSpinner, C.short(theme.Spinner), bg) + 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() { diff --git a/src/item.go b/src/item.go index 711adbe..7c2f94d 100644 --- a/src/item.go +++ b/src/item.go @@ -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}) } else { 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 - if current && bg == -1 { - bg = int(curses.DarkBG) + if bg == -1 { + if current { + bg = curses.DarkBG + } else { + bg = curses.BG + } } offsets = append(offsets, colorOffset{ offset: Offset{int32(start), int32(idx)}, - color: curses.PairFor(ansi.color.fg, bg), + color: curses.PairFor(fg, bg), bold: ansi.color.bold || bold}) } } diff --git a/src/options.go b/src/options.go index 1531749..63037ea 100644 --- a/src/options.go +++ b/src/options.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" "unicode/utf8" @@ -35,8 +36,7 @@ const usage = `usage: fzf [options] -m, --multi Enable multi-select with tab/shift-tab --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - --color=COL Color scheme; [dark|light|16|bw] - (default: dark on 256-color terminal, otherwise 16) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --black Use black background --reverse Reverse orientation --no-hscroll Disable horizontal scroll @@ -121,14 +121,14 @@ type Options struct { Version bool } -func defaultOptions() *Options { - var defaultTheme *curses.ColorTheme +func defaultTheme() *curses.ColorTheme { if strings.Contains(os.Getenv("TERM"), "256") { - defaultTheme = curses.Dark256 - } else { - defaultTheme = curses.Default16 + return curses.Dark256 } + return curses.Default16 +} +func defaultOptions() *Options { return &Options{ Mode: ModeFuzzy, Case: CaseSmart, @@ -141,7 +141,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Theme: defaultTheme, + Theme: defaultTheme(), Black: false, Reverse: false, Hscroll: true, @@ -187,6 +187,14 @@ func nextString(args []string, i *int, message string) string { 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 { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { @@ -277,20 +285,72 @@ func parseTiebreak(str string) tiebreak { return byLength } -func parseTheme(str string) *curses.ColorTheme { - switch strings.ToLower(str) { - case "dark": - return curses.Dark256 - case "light": - return curses.Light256 - case "16": - return curses.Default16 - case "bw", "no": - return nil - default: - errorExit("invalid color scheme: " + str) +func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { + dupe := *theme + return &dupe +} + +func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { + theme := dupeTheme(defaultTheme) + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "dark": + theme = dupeTheme(curses.Dark256) + 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) { @@ -400,7 +460,12 @@ func parseOptions(opts *Options, allArgs []string) { case "--bind": opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) 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": opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) opts.ToggleSort = true @@ -497,7 +562,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) } 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 { opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) } else { diff --git a/src/options_test.go b/src/options_test.go index ad9a6fb..d356210 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -155,3 +155,49 @@ func TestBind(t *testing.T) { } 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") + } +}