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
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
var chunkList *ChunkList
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item {
data, colors := extractColors(data)
return &Item{
text: data,
index: uint32(index),
rank: Rank{0, 0, uint32(index)}}
text: data,
index: uint32(index),
colors: colors,
rank: Rank{0, 0, uint32(index)}}
})
} else {
chunkList = NewChunkList(func(data *string, index int) *Item {
@ -79,7 +101,12 @@ func Run(options *Options) {
text: Transform(tokens, opts.WithNth).whole,
origText: data,
index: uint32(index),
colors: nil,
rank: Rank{0, 0, uint32(index)}}
trimmed, colors := extractColors(item.text)
item.text = trimmed
item.colors = colors
return &item
})
}

View File

@ -78,6 +78,7 @@ const (
ColInfo
ColCursor
ColSelected
ColUser
)
const (
@ -103,14 +104,17 @@ var (
_buf []byte
_in *os.File
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_prevDownY int
_clickY []int
DarkBG C.short
)
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]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
}
if color256 {
DarkBG = 236
C.init_pair(ColPrompt, 110, bg)
C.init_pair(ColMatch, 108, bg)
C.init_pair(ColCurrent, 254, 236)
C.init_pair(ColCurrentMatch, 151, 236)
C.init_pair(ColCurrent, 254, DarkBG)
C.init_pair(ColCurrentMatch, 151, DarkBG)
C.init_pair(ColSpinner, 148, bg)
C.init_pair(ColInfo, 144, bg)
C.init_pair(ColCursor, 161, 236)
C.init_pair(ColSelected, 168, 236)
C.init_pair(ColCursor, 161, DarkBG)
C.init_pair(ColSelected, 168, DarkBG)
} else {
DarkBG = C.COLOR_BLACK
C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK)
C.init_pair(ColCurrent, C.COLOR_YELLOW, DarkBG)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, DarkBG)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK)
C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK)
C.init_pair(ColCursor, C.COLOR_RED, DarkBG)
C.init_pair(ColSelected, C.COLOR_MAGENTA, DarkBG)
}
_color = attrColored
} else {
@ -428,3 +434,15 @@ func Endwin() {
func 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
import (
"github.com/junegunn/fzf/src/curses"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
type ColorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line
type Item struct {
text *string
@ -10,6 +20,7 @@ type Item struct {
transformed *Transformed
index uint32
offsets []Offset
colors []AnsiOffset
rank Rank
}
@ -55,6 +66,79 @@ func (i *Item) AsString() string {
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
type ByOrder []Offset

View File

@ -3,6 +3,8 @@ package fzf
import (
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
)
func TestOffsetSort(t *testing.T) {
@ -72,3 +74,31 @@ func TestItemRank(t *testing.T) {
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
-m, --multi Enable multi-select with tab/shift-tab
--ansi Interpret ANSI color codes and remove from output
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
@ -81,6 +82,7 @@ type Options struct {
Sort int
Tac bool
Multi bool
Ansi bool
Mouse bool
Color bool
Color256 bool
@ -106,6 +108,7 @@ func defaultOptions() *Options {
Sort: 1000,
Tac: false,
Multi: false,
Ansi: false,
Mouse: true,
Color: true,
Color256: strings.Contains(os.Getenv("TERM"), "256"),
@ -227,6 +230,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Multi = true
case "+m", "--no-multi":
opts.Multi = false
case "--ansi":
opts.Ansi = true
case "--no-ansi":
opts.Ansi = false
case "--no-mouse":
opts.Mouse = false
case "+c", "--no-color":

View File

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

View File

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

View File

@ -27,6 +27,17 @@ func Max32(first int32, second int32) int32 {
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
func Constrain(val int, min int, max int) int {
if val < min {