Compare commits

..

4 Commits

Author SHA1 Message Date
Junegunn Choi
52771a6226 0.9.13 2015-06-03 02:09:07 +09:00
Junegunn Choi
b00bcf506e Fix #248 - Premature termination of Reader on long input 2015-06-03 01:48:02 +09:00
Junegunn Choi
fdbfe36c0b Color customization (#245) 2015-06-03 01:46:03 +09:00
Junegunn Choi
446e822723 Update CHANGELOG 2015-05-22 02:37:38 +09:00
10 changed files with 307 additions and 96 deletions

View File

@@ -1,6 +1,18 @@
CHANGELOG
=========
0.9.13
------
### New features
- Color customization with the extended `--color` option
### Bug fixes
- Fixed premature termination of Reader in the presence of a long line which
is longer than 64KB
0.9.12
------
@@ -11,6 +23,7 @@ CHANGELOG
### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize
- Fixed ANSI code offset calculation
0.9.11
------

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
version=0.9.12
version=0.9.13
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "May 2015" "fzf 0.9.12" "fzf - a command-line fuzzy finder"
.TH fzf 1 "June 2015" "fzf 0.9.13" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -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

View File

@@ -8,7 +8,7 @@ import (
const (
// Current version
Version = "0.9.12"
Version = "0.9.13"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -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() {

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})
} 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})
}
}

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -30,9 +30,29 @@ func (r *Reader) ReadSource() {
}
func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil {
for scanner.Scan() {
r.pusher(scanner.Text())
reader := bufio.NewReader(src)
eof := false
Loop:
for !eof {
buf := []byte{}
iter := 0 // TODO: max size?
for {
// "ReadLine either returns a non-nil line or it returns an error, never both"
line, isPrefix, err := reader.ReadLine()
eof = err == io.EOF
if eof {
break
} else if err != nil {
break Loop
}
iter++
buf = append(buf, line...)
if !isPrefix {
break
}
}
if iter > 0 {
r.pusher(string(buf))
r.eventBox.Set(EvtReadNew, nil)
}
}

View File

@@ -525,6 +525,17 @@ class TestGoFZF < TestBase
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.split($/)
end
def test_long_line
tempname = TEMPNAME + Time.now.to_f.to_s
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
ensure
File.unlink tempname
end
private
def writelines path, lines
File.unlink path while File.exists? path