summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2015-05-31 16:46:54 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2015-06-03 01:46:03 +0900
commitfdbfe36c0b882a4e948fafd1949956341607b1e5 (patch)
treecda8cbb826626ca84a1f7b460a68ce02e4093959
parent446e8227236c4d15bb17e80db797d1799b05521b (diff)
Color customization (#245)
-rw-r--r--man/man1/fzf.147
-rw-r--r--src/curses/curses.go125
-rw-r--r--src/item.go18
-rw-r--r--src/options.go111
-rw-r--r--src/options_test.go46
5 files changed, 257 insertions, 90 deletions
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 1a9720be..9f652e0c 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 44fab4fb..985dd87f 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 711adbe2..7c2f94d5 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 15317495..63037eaa 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 ad9a6fb5..d3562108 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")
+ }
+}