Add support for ANSI color codes

This commit is contained in:
Junegunn Choi 2015-03-19 01:59:14 +09:00
parent d80a41bb6d
commit e70a2a5817
11 changed files with 451 additions and 24 deletions

141
src/ansi.go Normal file
View File

@ -0,0 +1,141 @@
package fzf
import (
"bytes"
"regexp"
"strconv"
"strings"
)
type AnsiOffset struct {
offset [2]int32
color ansiState
}
type ansiState struct {
fg int
bg int
bold bool
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold
}
var ansiRegex *regexp.Regexp
func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*m")
}
func ExtractColor(str *string) (*string, []AnsiOffset) {
offsets := make([]AnsiOffset, 0)
var output bytes.Buffer
var state *ansiState
idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) {
output.WriteString((*str)[idx:offset[0]])
newLen := int32(output.Len())
newState := interpretCode((*str)[offset[0]:offset[1]], state)
if !newState.equals(state) {
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(output.Len())
}
if newState.colored() {
// Append new offset
state = newState
offsets = append(offsets, AnsiOffset{[2]int32{newLen, newLen}, *state})
} else {
// Discard state
state = nil
}
}
idx = offset[1]
}
rest := (*str)[idx:]
if len(rest) > 0 {
output.WriteString(rest)
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(output.Len())
}
}
outputStr := output.String()
return &outputStr, offsets
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State
var state *ansiState
if prevState == nil {
state = &ansiState{-1, -1, false}
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold}
}
ptr := &state.fg
state256 := 0
init := func() {
state.fg = -1
state.bg = -1
state.bold = false
state256 = 0
}
ansiCode = ansiCode[2 : len(ansiCode)-1]
for _, code := range strings.Split(ansiCode, ";") {
if num, err := strconv.Atoi(code); err == nil {
switch state256 {
case 0:
switch num {
case 38:
ptr = &state.fg
state256++
case 48:
ptr = &state.bg
state256++
case 39:
state.fg = -1
case 49:
state.bg = -1
case 1:
state.bold = true
case 0:
init()
default:
if num >= 30 && num <= 37 {
state.fg = num - 30
} else if num >= 40 && num <= 47 {
state.bg = num - 40
}
}
case 1:
switch num {
case 5:
state256++
default:
state256 = 0
}
case 2:
*ptr = num
state256 = 0
}
}
}
return state
}

91
src/ansi_test.go Normal file
View File

@ -0,0 +1,91 @@
package fzf
import (
"fmt"
"testing"
)
func TestExtractColor(t *testing.T) {
assert := func(offset AnsiOffset, b int32, e int32, fg int, bg int, bold bool) {
if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold {
t.Error(offset, b, e, fg, bg, bold)
}
}
src := "hello world"
clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []AnsiOffset)) {
output, ansiOffsets := ExtractColor(&src)
if *output != "hello world" {
t.Errorf("Invalid output: {}", output)
}
fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets)
}
check(func(offsets []AnsiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[0mhello world"
check(func(offsets []AnsiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[1mhello world"
check(func(offsets []AnsiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
})
src = "hello \x1b[34;45;1mworld"
check(func(offsets []AnsiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []AnsiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []AnsiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 9, 4, 5, true)
})
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []AnsiOffset) {
if len(offsets) != 3 {
t.Fail()
}
assert(offsets[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false)
})
// {38,48};5;{38,48}
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []AnsiOffset) {
if len(offsets) != 2 {
t.Fail()
}
assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true)
})
}

View File

@ -63,14 +63,36 @@ func Run(options *Options) {
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor
extractColors := func(data *string) (*string, []AnsiOffset) {
// By default, we do nothing
return data, nil
}
if opts.Ansi {
if opts.Color {
extractColors = func(data *string) (*string, []AnsiOffset) {
return ExtractColor(data)
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
extractColors = func(data *string) (*string, []AnsiOffset) {
trimmed, _ := ExtractColor(data)
return trimmed, nil
}
}
}
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
data, colors := extractColors(data)
return &Item{ return &Item{
text: data, text: data,
index: uint32(index), index: uint32(index),
rank: Rank{0, 0, uint32(index)}} colors: colors,
rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
@ -79,7 +101,12 @@ func Run(options *Options) {
text: Transform(tokens, opts.WithNth).whole, text: Transform(tokens, opts.WithNth).whole,
origText: data, origText: data,
index: uint32(index), index: uint32(index),
colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := extractColors(item.text)
item.text = trimmed
item.colors = colors
return &item return &item
}) })
} }

View File

@ -78,6 +78,7 @@ const (
ColInfo ColInfo
ColCursor ColCursor
ColSelected ColSelected
ColUser
) )
const ( const (
@ -103,14 +104,17 @@ var (
_buf []byte _buf []byte
_in *os.File _in *os.File
_color func(int, bool) C.int _color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time _prevDownTime time.Time
_prevDownY int _prevDownY int
_clickY []int _clickY []int
DarkBG C.short
) )
func init() { func init() {
_prevDownTime = time.Unix(0, 0) _prevDownTime = time.Unix(0, 0)
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int)
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@ -200,23 +204,25 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
bg = -1 bg = -1
} }
if color256 { if color256 {
DarkBG = 236
C.init_pair(ColPrompt, 110, bg) C.init_pair(ColPrompt, 110, bg)
C.init_pair(ColMatch, 108, bg) C.init_pair(ColMatch, 108, bg)
C.init_pair(ColCurrent, 254, 236) C.init_pair(ColCurrent, 254, DarkBG)
C.init_pair(ColCurrentMatch, 151, 236) C.init_pair(ColCurrentMatch, 151, DarkBG)
C.init_pair(ColSpinner, 148, bg) C.init_pair(ColSpinner, 148, bg)
C.init_pair(ColInfo, 144, bg) C.init_pair(ColInfo, 144, bg)
C.init_pair(ColCursor, 161, 236) C.init_pair(ColCursor, 161, DarkBG)
C.init_pair(ColSelected, 168, 236) C.init_pair(ColSelected, 168, DarkBG)
} else { } else {
DarkBG = C.COLOR_BLACK
C.init_pair(ColPrompt, C.COLOR_BLUE, bg) C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg) C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK) C.init_pair(ColCurrent, C.COLOR_YELLOW, DarkBG)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK) C.init_pair(ColCurrentMatch, C.COLOR_GREEN, DarkBG)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg) C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg) C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK) C.init_pair(ColCursor, C.COLOR_RED, DarkBG)
C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK) C.init_pair(ColSelected, C.COLOR_MAGENTA, DarkBG)
} }
_color = attrColored _color = attrColored
} else { } else {
@ -428,3 +434,15 @@ func Endwin() {
func Refresh() { func Refresh() {
C.refresh() C.refresh()
} }
func PairFor(fg int, bg int) int {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

14
src/curses/curses_test.go Normal file
View File

@ -0,0 +1,14 @@
package curses
import (
"testing"
)
func TestPairFor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) {
t.Fail()
}
if PairFor(-1, 10) != PairFor(-1, 10) {
t.Fail()
}
}

View File

@ -1,8 +1,18 @@
package fzf package fzf
import (
"github.com/junegunn/fzf/src/curses"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring // Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32 type Offset [2]int32
type ColorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text *string text *string
@ -10,6 +20,7 @@ type Item struct {
transformed *Transformed transformed *Transformed
index uint32 index uint32
offsets []Offset offsets []Offset
colors []AnsiOffset
rank Rank rank Rank
} }
@ -55,6 +66,79 @@ func (i *Item) AsString() string {
return *i.text return *i.text
} }
func (item *Item) ColorOffsets(color int, bold bool, current bool) []ColorOffset {
if len(item.colors) == 0 {
offsets := make([]ColorOffset, 0)
for _, off := range item.offsets {
offsets = append(offsets, ColorOffset{offset: off, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32 = 0
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
offsets := make([]ColorOffset, 0)
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, ColorOffset{
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
bg := ansi.color.bg
if current {
bg = int(curses.DarkBG)
}
offsets = append(offsets, ColorOffset{
offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets // ByOrder is for sorting substring offsets
type ByOrder []Offset type ByOrder []Offset

View File

@ -3,6 +3,8 @@ package fzf
import ( import (
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses"
) )
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
@ -72,3 +74,31 @@ func TestItemRank(t *testing.T) {
t.Error(items) t.Error(items)
} }
} }
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []AnsiOffset{
AnsiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
AnsiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
AnsiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
AnsiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.ColorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@ -29,6 +29,7 @@ const usage = `usage: fzf [options]
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Interpret ANSI color codes and remove from output
--no-mouse Disable mouse --no-mouse Disable mouse
+c, --no-color Disable colors +c, --no-color Disable colors
+2, --no-256 Disable 256-color +2, --no-256 Disable 256-color
@ -81,6 +82,7 @@ type Options struct {
Sort int Sort int
Tac bool Tac bool
Multi bool Multi bool
Ansi bool
Mouse bool Mouse bool
Color bool Color bool
Color256 bool Color256 bool
@ -106,6 +108,7 @@ func defaultOptions() *Options {
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Multi: false, Multi: false,
Ansi: false,
Mouse: true, Mouse: true,
Color: true, Color: true,
Color256: strings.Contains(os.Getenv("TERM"), "256"), Color256: strings.Contains(os.Getenv("TERM"), "256"),
@ -227,6 +230,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Multi = true opts.Multi = true
case "+m", "--no-multi": case "+m", "--no-multi":
opts.Multi = false opts.Multi = false
case "--ansi":
opts.Ansi = true
case "--no-ansi":
opts.Ansi = false
case "--no-mouse": case "--no-mouse":
opts.Mouse = false opts.Mouse = false
case "+c", "--no-color": case "+c", "--no-color":

View File

@ -264,6 +264,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
transformed: item.transformed, transformed: item.transformed,
index: item.index, index: item.index,
offsets: offsets, offsets: offsets,
colors: item.colors,
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }

View File

@ -251,7 +251,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else { } else {
C.CPrint(C.ColCurrent, true, " ") C.CPrint(C.ColCurrent, true, " ")
} }
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch) t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
C.CPrint(C.ColCursor, true, " ") C.CPrint(C.ColCursor, true, " ")
if selected { if selected {
@ -259,7 +259,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else { } else {
C.Print(" ") C.Print(" ")
} }
t.printHighlighted(item, false, 0, C.ColMatch) t.printHighlighted(item, false, 0, C.ColMatch, false)
} }
} }
@ -299,7 +299,7 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed return runes, trimmed
} }
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32 var maxe int32
for _, offset := range item.offsets { for _, offset := range item.offsets {
if offset[1] > maxe { if offset[1] > maxe {
@ -309,7 +309,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
// Overflow // Overflow
text := []rune(*item.text) text := []rune(*item.text)
offsets := item.offsets offsets := item.ColorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
@ -328,37 +328,40 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
text, diff = trimLeft(text, maxWidth-2) text, diff = trimLeft(text, maxWidth-2)
// Transform offsets // Transform offsets
offsets = make([]Offset, len(item.offsets)) for idx, offset := range offsets {
for idx, offset := range item.offsets { b, e := offset.offset[0], offset.offset[1]
b, e := offset[0], offset[1]
b += 2 - diff b += 2 - diff
e += 2 - diff e += 2 - diff
b = util.Max32(b, 2) b = util.Max32(b, 2)
if b < e { if b < e {
offsets[idx] = Offset{b, e} offsets[idx].offset[0] = b
offsets[idx].offset[1] = e
} }
} }
text = append([]rune(".."), text...) text = append([]rune(".."), text...)
} }
} }
sort.Sort(ByOrder(offsets))
var index int32 var index int32
var substr string var substr string
var prefixWidth int var prefixWidth int
maxOffset := int32(len(text))
for _, offset := range offsets { for _, offset := range offsets {
b := util.Max32(index, offset[0]) b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Max32(index, offset[1]) e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr) C.CPrint(col1, bold, substr)
substr, prefixWidth = processTabs(text[b:e], prefixWidth) substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(col2, bold, substr) C.CPrint(offset.color, bold, substr)
index = e index = e
if index >= maxOffset {
break
}
} }
if index < int32(len(text)) { if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr) C.CPrint(col1, bold, substr)
} }

View File

@ -27,6 +27,17 @@ func Max32(first int32, second int32) int32 {
return second return second
} }
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 {
if val < min {
return min
}
if val > max {
return max
}
return val
}
// Constrain limits the given integer with the upper and lower bounds // Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int { func Constrain(val int, min int, max int) int {
if val < min { if val < min {