diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2020-10-25 19:29:37 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2020-10-25 19:30:41 +0900 |
commit | 11841f688bd12e56c11dc2ae71911a1e25213951 (patch) | |
tree | 25732a5add40e7d70ac01450ab18e1e133a8e15f /src | |
parent | 03c4f042463b2cf96dbb05fdbd06cfbb4903e6c8 (diff) |
Add support for text styling using --color
Close #1663
Diffstat (limited to 'src')
-rw-r--r-- | src/core.go | 4 | ||||
-rw-r--r-- | src/options.go | 104 | ||||
-rw-r--r-- | src/options_test.go | 14 | ||||
-rw-r--r-- | src/result.go | 64 | ||||
-rw-r--r-- | src/result_test.go | 67 | ||||
-rw-r--r-- | src/terminal.go | 84 | ||||
-rw-r--r-- | src/tui/dummy.go | 19 | ||||
-rw-r--r-- | src/tui/light.go | 43 | ||||
-rw-r--r-- | src/tui/tcell.go | 41 | ||||
-rw-r--r-- | src/tui/tui.go | 374 |
10 files changed, 475 insertions, 339 deletions
diff --git a/src/core.go b/src/core.go index 99dde536..bd45df69 100644 --- a/src/core.go +++ b/src/core.go @@ -66,7 +66,7 @@ func Run(opts *Options, revision string) { var lineAnsiState, prevLineAnsiState *ansiState if opts.Ansi { - if opts.Theme != nil { + if opts.Theme.Colored { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { prevLineAnsiState = lineAnsiState trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil) @@ -102,7 +102,7 @@ func Run(opts *Options, revision string) { } else { chunkList = NewChunkList(func(item *Item, data []byte) bool { tokens := Tokenize(string(data), opts.Delimiter) - if opts.Ansi && opts.Theme != nil && len(tokens) > 1 { + if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { var ansiState *ansiState if prevLineAnsiState != nil { ansiStateDup := *prevLineAnsiState diff --git a/src/options.go b/src/options.go index ea6e420e..ce000a03 100644 --- a/src/options.go +++ b/src/options.go @@ -590,11 +590,8 @@ func parseTiebreak(str string) []criterion { } func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { - if theme != nil { - dupe := *theme - return &dupe - } - return nil + dupe := *theme + return &dupe } func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { @@ -619,54 +616,76 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { continue } - pair := strings.Split(str, ":") - if len(pair) != 2 { + components := strings.Split(str, ":") + if len(components) < 2 { fail() } - var ansi tui.Color - if rrggbb.MatchString(pair[1]) { - ansi = tui.HexToColor(pair[1]) - } else { - ansi32, err := strconv.Atoi(pair[1]) - if err != nil || ansi32 < -1 || ansi32 > 255 { - fail() + cattr := tui.NewColorAttr() + for _, component := range components[1:] { + switch component { + case "regular": + cattr.Attr = tui.AttrRegular + case "bold", "strong": + cattr.Attr |= tui.Bold + case "dim": + cattr.Attr |= tui.Dim + case "italic": + cattr.Attr |= tui.Italic + case "underline": + cattr.Attr |= tui.Underline + case "blink": + cattr.Attr |= tui.Blink + case "reverse": + cattr.Attr |= tui.Reverse + case "": + default: + if rrggbb.MatchString(component) { + cattr.Color = tui.HexToColor(component) + } else { + ansi32, err := strconv.Atoi(component) + if err != nil || ansi32 < -1 || ansi32 > 255 { + fail() + } + cattr.Color = tui.Color(ansi32) + } } - ansi = tui.Color(ansi32) } - switch pair[0] { + switch components[0] { + case "input": + theme.Input = cattr case "fg": - theme.Fg = ansi + theme.Fg = cattr case "bg": - theme.Bg = ansi + theme.Bg = cattr case "preview-fg": - theme.PreviewFg = ansi + theme.PreviewFg = cattr case "preview-bg": - theme.PreviewBg = ansi + theme.PreviewBg = cattr case "fg+": - theme.Current = ansi + theme.Current = cattr case "bg+": - theme.DarkBg = ansi + theme.DarkBg = cattr case "gutter": - theme.Gutter = ansi + theme.Gutter = cattr case "hl": - theme.Match = ansi + theme.Match = cattr case "hl+": - theme.CurrentMatch = ansi + theme.CurrentMatch = cattr case "border": - theme.Border = ansi + theme.Border = cattr case "prompt": - theme.Prompt = ansi + theme.Prompt = cattr case "spinner": - theme.Spinner = ansi + theme.Spinner = cattr case "info": - theme.Info = ansi + theme.Info = cattr case "pointer": - theme.Cursor = ansi + theme.Cursor = cattr case "marker": - theme.Selected = ansi + theme.Selected = cattr case "header": - theme.Header = ansi + theme.Header = cattr default: fail() } @@ -1180,7 +1199,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-mouse": opts.Mouse = false case "+c", "--no-color": - opts.Theme = nil + opts.Theme = tui.NoColorTheme() case "+2", "--no-256": opts.Theme = tui.Default16 case "--black": @@ -1478,6 +1497,25 @@ func postProcessOptions(opts *Options) { } } } + + if opts.Bold { + theme := opts.Theme + boldify := func(c tui.ColorAttr) tui.ColorAttr { + dup := c + if !theme.Colored { + dup.Attr |= tui.Bold + } else if (c.Attr & tui.AttrRegular) == 0 { + dup.Attr |= tui.Bold + } + return dup + } + theme.Current = boldify(theme.Current) + theme.CurrentMatch = boldify(theme.CurrentMatch) + theme.Prompt = boldify(theme.Prompt) + theme.Input = boldify(theme.Input) + theme.Cursor = boldify(theme.Cursor) + theme.Spinner = boldify(theme.Spinner) + } } // ParseOptions parses command-line options diff --git a/src/options_test.go b/src/options_test.go index 78207275..a8fc75bd 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -295,7 +295,7 @@ func TestColorSpec(t *testing.T) { } customized := parseTheme(theme, "fg:231,bg:232") - if customized.Fg != 231 || customized.Bg != 232 { + if customized.Fg.Color != 231 || customized.Bg.Color != 232 { t.Errorf("color not customized") } if *tui.Dark256 == *customized { @@ -313,18 +313,6 @@ func TestColorSpec(t *testing.T) { } } -func TestParseNilTheme(t *testing.T) { - var theme *tui.ColorTheme - newTheme := parseTheme(theme, "prompt:12") - if newTheme != nil { - t.Errorf("color is disabled. keep it that way.") - } - newTheme = parseTheme(theme, "prompt:12,dark,prompt:13") - if newTheme.Prompt != 13 { - t.Errorf("color should now be enabled and customized") - } -} - func TestDefaultCtrlNP(t *testing.T) { check := func(words []string, key int, expected actionType) { opts := defaultOptions() diff --git a/src/result.go b/src/result.go index be325cbb..b3971f44 100644 --- a/src/result.go +++ b/src/result.go @@ -15,7 +15,6 @@ type Offset [2]int32 type colorOffset struct { offset [2]int32 color tui.ColorPair - attr tui.Attr } type Result struct { @@ -87,14 +86,14 @@ func minRank() Result { return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset { itemColors := result.item.Colors() - // No ANSI code, or --color=no + // No ANSI codes if len(itemColors) == 0 { var offsets []colorOffset for _, off := range matchOffsets { - offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr}) + offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch}) } return offsets } @@ -111,17 +110,19 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, maxCol = ansi.offset[1] } } - cols := make([]int, maxCol) + cols := make([]int, maxCol) for colorIndex, ansi := range itemColors { for i := ansi.offset[0]; i < ansi.offset[1]; i++ { - cols[i] = colorIndex + 1 // XXX + cols[i] = colorIndex + 1 // 1-based index of itemColors } } for _, off := range matchOffsets { for i := off[0]; i < off[1]; i++ { - cols[i] = -1 + // Negative of 1-based index of itemColors + // - The extra -1 means highlighted + cols[i] = cols[i]*-1 - 1 } } @@ -133,36 +134,41 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, // --++++++++-- --++++++++++--- curr := 0 start := 0 + ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { + fg := ansi.color.fg + bg := ansi.color.bg + if fg == -1 { + if current { + fg = theme.Current.Color + } else { + fg = theme.Fg.Color + } + } + if bg == -1 { + if current { + bg = theme.DarkBg.Color + } else { + bg = theme.Bg.Color + } + } + return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base) + } var colors []colorOffset add := func(idx int) { if curr != 0 && idx > start { - if curr == -1 { + if curr < 0 { + color := colMatch + if curr < -1 && theme.Colored { + origColor := ansiToColorPair(itemColors[-curr-2], colMatch) + color = origColor.MergeNonDefault(color) + } colors = append(colors, colorOffset{ - offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr}) + offset: [2]int32{int32(start), int32(idx)}, color: color}) } else { ansi := itemColors[curr-1] - fg := ansi.color.fg - bg := ansi.color.bg - if theme != nil { - if fg == -1 { - if current { - fg = theme.Current - } else { - fg = theme.Fg - } - } - if bg == -1 { - if current { - bg = theme.DarkBg - } else { - bg = theme.Bg - } - } - } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: tui.NewColorPair(fg, bg), - attr: ansi.color.attr.Merge(attr)}) + color: ansiToColorPair(ansi, colBase)}) } } } diff --git a/src/result_test.go b/src/result_test.go index afd17307..7b21ba54 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -105,32 +105,55 @@ func TestColorOffset(t *testing.T) { // ++++++++ ++++++++++ // --++++++++-- --++++++++++--- - offsets := []Offset{Offset{5, 15}, Offset{25, 35}} + offsets := []Offset{{5, 15}, {25, 35}} item := Result{ item: &Item{ colors: &[]ansiOffset{ - ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, - ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}}, - ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}}, - ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} - // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - - pair := tui.NewColorPair(99, 199) - colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true) - assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { - var attr tui.Attr - if bold { - attr = tui.Bold - } + {[2]int32{0, 20}, ansiState{1, 5, 0}}, + {[2]int32{22, 27}, ansiState{2, 6, tui.Bold}}, + {[2]int32{30, 32}, ansiState{3, 7, 0}}, + {[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} + + colBase := tui.NewColorPair(89, 189, tui.AttrUndefined) + colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined) + colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true) + assert := func(idx int, b int32, e int32, c tui.ColorPair) { o := colors[idx] - if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { - t.Error(o) + if o.offset[0] != b || o.offset[1] != e || o.color != c { + t.Error(o, b, e, c) } } - assert(0, 0, 5, tui.NewColorPair(1, 5), false) - assert(1, 5, 15, pair, false) - assert(2, 15, 20, tui.NewColorPair(1, 5), false) - assert(3, 22, 25, tui.NewColorPair(2, 6), true) - assert(4, 25, 35, pair, false) - assert(5, 35, 40, tui.NewColorPair(4, 8), true) + // [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}} + // {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}} + // {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}} + // {[35 40] {4 8 1}}] + assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(1, 5, 15, colMatch) + assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) + assert(4, 25, 27, colMatch.WithAttr(tui.Bold)) + assert(5, 27, 30, colMatch) + assert(6, 30, 32, colMatch) + assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks? + assert(8, 33, 35, colMatch.WithAttr(tui.Bold)) + assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) + + colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined) + colUnderline := tui.NewColorPair(-1, -1, tui.Underline) + colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true) + + // [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}} + // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} + // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} + // {[35 40] {4 8 1}}] + assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) + assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) + assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) + assert(5, 27, 30, colUnderline) + assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline)) + assert(7, 32, 33, colUnderline) + assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) + assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) } diff --git a/src/terminal.go b/src/terminal.go index 9801872f..d896d1de 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -23,6 +23,7 @@ import ( var placeholder *regexp.Regexp var numericPrefix *regexp.Regexp +var whiteSuffix *regexp.Regexp var activeTempFiles []string const ellipsis string = ".." @@ -31,6 +32,7 @@ const clearCode string = "\x1b[2J" func init() { placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`) + whiteSuffix = regexp.MustCompile(`\s*$`) activeTempFiles = []string{} } @@ -515,9 +517,29 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) { var state *ansiState trimmed, colors, _ := extractColor(prompt, state, nil) item := &Item{text: util.ToChars([]byte(trimmed)), colors: colors} + + // "Prompt> " + // ------- // Do not apply ANSI attributes to the trailing whitespaces + // // unless the part has a non-default ANSI state + loc := whiteSuffix.FindStringIndex(trimmed) + if loc != nil { + blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear}} + if item.colors != nil { + lastColor := (*item.colors)[len(*item.colors)-1] + fmt.Println(lastColor.offset[1], int32(loc[1])) + if lastColor.offset[1] < int32(loc[1]) { + blankState.offset[0] = lastColor.offset[1] + colors := append(*item.colors, blankState) + item.colors = &colors + } + } else { + colors := []ansiOffset{blankState} + item.colors = &colors + } + } output := func() { t.printHighlighted( - Result{item: item}, t.strong, tui.ColPrompt, tui.ColPrompt, false, false) + Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false) } _, promptLen := t.processTabs([]rune(trimmed), 0) @@ -839,8 +861,8 @@ func (t *Terminal) printPrompt() { t.prompt() before, after := t.updatePromptOffset() - t.window.CPrint(tui.ColNormal, t.strong, string(before)) - t.window.CPrint(tui.ColNormal, t.strong, string(after)) + t.window.CPrint(tui.ColInput, string(before)) + t.window.CPrint(tui.ColInput, string(after)) } func (t *Terminal) trimMessage(message string, maxWidth int) string { @@ -859,7 +881,7 @@ func (t *Terminal) printInfo() { if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration - t.window.CPrint(tui.ColSpinner, t.strong, t.spinner[idx]) + t.window.CPrint(tui.ColSpinner, t.spinner[idx]) } t.move(1, 2, false) pos = 2 @@ -870,9 +892,9 @@ func (t *Terminal) printInfo() { } t.move(0, pos, true) if t.reading { - t.window.CPrint(tui.ColSpinner, t.strong, " < ") + t.window.CPrint(tui.ColSpinner, " < ") } else { - t.window.CPrint(tui.ColPrompt, t.strong, " < ") + t.window.CPrint(tui.ColPrompt, " < ") } pos += len(" < ") case infoHidden: @@ -903,7 +925,7 @@ func (t *Terminal) printInfo() { output = fmt.Sprintf("[Command failed: %s]", *t.failed) } output = t.trimMessage(output, t.window.Width()-pos) - t.window.CPrint(tui.ColInfo, 0, output) + t.window.CPrint(tui.ColInfo, output) } func (t *Terminal) printHeader() { @@ -928,7 +950,7 @@ func (t *Terminal) printHeader() { t.move(line, 2, true) t.printHighlighted(Result{item: item}, - tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false) + tui.ColHeader, tui.ColHeader, false, false) } } @@ -958,7 +980,7 @@ func (t *Terminal) printList() { func (t *Terminal) printItem(result Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] - label := t.pointerEmpty + label := "" if t.jumping != jumpDisabled { if i < len(t.jumpLabels) { // Striped @@ -983,21 +1005,29 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) { t.move(line, 0, false) if current { - t.window.CPrint(tui.ColCurrentCursor, t.strong, label) + if len(label) == 0 { + t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty) + } else { + t.window.CPrint(tui.ColCurrentCursor, label) + } if selected { - t.window.CPrint(tui.ColCurrentSelected, t.strong, t.marker) + t.window.CPrint(tui.ColCurrentSelected, t.marker) } else { - t.window.CPrint(tui.ColCurrentSelected, t.strong, t.markerEmpty) + t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty) } - newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) + newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { - t.window.CPrint(tui.ColCursor, t.strong, label) + if len(label) == 0 { + t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty) + } else { + t.window.CPrint(tui.ColCursor, label) + } if selected { - t.window.CPrint(tui.ColSelected, t.strong, t.marker) + t.window.CPrint(tui.ColSelected, t.marker) } else { t.window.Print(t.markerEmpty) } - newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) + newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true) } fillSpaces := prevLine.width - newLine.width if fillSpaces > 0 { @@ -1051,7 +1081,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool { return t.displayWidthWithLimit(runes, 0, max) > max } -func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { +func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int { item := result.item // Overflow @@ -1076,7 +1106,7 @@ func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.Color maxe = util.Max(maxe, int(offset[1])) } - offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) + offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) @@ -1134,11 +1164,11 @@ func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.Color e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) - t.window.CPrint(col1, attr, substr) + t.window.CPrint(colBase, substr) if b < e { substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) - t.window.CPrint(offset.color, offset.attr, substr) + t.window.CPrint(offset.color, substr) } index = e @@ -1148,7 +1178,7 @@ func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.Color } if index < maxOffset { substr, _ = t.processTabs(text[index:], prefixWidth) - t.window.CPrint(col1, attr, substr) + t.window.CPrint(colBase, substr) } return displayWidth } @@ -1161,7 +1191,7 @@ func (t *Terminal) renderPreviewSpinner() { if !t.previewer.scrollable { if maxWidth > 0 { t.pwindow.Move(0, maxWidth-1) - t.pwindow.CPrint(tui.ColSpinner, t.strong, spin) + t.pwindow.CPrint(tui.ColSpinner, spin) } } else { offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines) @@ -1173,8 +1203,8 @@ func (t *Terminal) renderPreviewSpinner() { pos := maxWidth - t.displayWidth(offsetRunes) t.pwindow.Move(0, pos) if maxWidth > 0 { - t.pwindow.CPrint(tui.ColSpinner, t.strong, spin) - t.pwindow.CPrint(tui.ColInfo, tui.Reverse, string(offsetRunes)) + t.pwindow.CPrint(tui.ColSpinner, spin) + t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), string(offsetRunes)) } } } @@ -1185,7 +1215,7 @@ func (t *Terminal) renderPreviewText(unchanged bool) { lineNo := -t.previewer.offset height := t.pwindow.Height() if unchanged { - t.pwindow.Move(0, 0) + t.pwindow.MoveAndClear(0, 0) } else { t.previewed.filled = false t.pwindow.Erase() @@ -1205,7 +1235,7 @@ func (t *Terminal) renderPreviewText(unchanged bool) { } str, width := t.processTabs(trimmed, prefixWidth) prefixWidth += width - if t.theme != nil && ansi != nil && ansi.colored() { + if t.theme.Colored && ansi != nil && ansi.colored() { fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } else { fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str) @@ -1258,7 +1288,7 @@ func (t *Terminal) printPreviewDelayed() { message := t.trimMessage("Loading ..", t.pwindow.Width()) pos := t.pwindow.Width() - len(message) t.pwindow.Move(0, pos) - t.pwindow.CPrint(tui.ColInfo, tui.Reverse, message) + t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), message) } func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { diff --git a/src/tui/dummy.go b/src/tui/dummy.go index a6df8550..ca50e655 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -15,14 +15,17 @@ func (a Attr) Merge(b Attr) Attr { } const ( - AttrRegular Attr = Attr(0) - Bold = Attr(1) - Dim = Attr(1 << 1) - Italic = Attr(1 << 2) - Underline = Attr(1 << 3) - Blink = Attr(1 << 4) - Blink2 = Attr(1 << 5) - Reverse = Attr(1 << 6) + AttrUndefined = Attr(0) + AttrRegular = Attr(1 << 7) + AttrClear = Attr(1 << 8) + + Bold = Attr(1) + Dim = Attr(1 << 1) + Italic = Attr(1 << 2) + Underline = Attr(1 << 3) + Blink = Attr(1 << 4) + Blink2 = Attr(1 << 5) + Reverse = Attr(1 << 6) ) func (r *FullscreenRenderer) Init() {} diff --git a/src/tui/light.go b/src/tui/light.go index d051e67a..6bd08216 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -627,7 +627,7 @@ func (r *LightRenderer) MaxY() int { func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { w := &LightWindow{ renderer: r, - colored: r.theme != nil, + colored: r.theme.Colored, preview: preview, border: borderStyle, top: top, @@ -637,14 +637,12 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev tabstop: r.tabstop, fg: colDefault, bg: colDefault} - if r.theme != nil { - if preview { - w.fg = r.theme.PreviewFg - w.bg = r.theme.PreviewBg - } else { - w.fg = r.theme.Fg - w.bg = r.theme.Bg - } + if preview { + w.fg = r.theme.PreviewFg.Color + w.bg = r.theme.PreviewBg.Color + } else { + w.fg = r.theme.Fg.Color + w.bg = r.theme.Bg.Color } w.drawBorder() return w @@ -661,9 +659,9 @@ func (w *LightWindow) drawBorder() { func (w *LightWindow) drawBorderHorizontal() { w.Move(0, 0) - w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width)) + w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) w.Move(w.height-1, 0) - w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width)) + w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) } func (w *LightWindow) drawBorderAround() { @@ -672,17 +670,15 @@ func (w *LightWindow) drawBorderAround() { if w.preview { color = ColPreviewBorder } - w.CPrint(color, AttrRegular, - string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) + w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) for y := 1; y < w.height-1; y++ { w.Move(y, 0) - w.CPrint(color, AttrRegular, string(w.border.vertical)) - w.CPrint(color, AttrRegular, repeat(' ', w.width-2)) - w.CPrint(color, AttrRegular, string(w.border.vertical)) + w.CPrint(color, string(w.border.vertical)) + w.CPrint(color, repeat(' ', w.width-2)) + w.CPrint(color, string(w.border.vertical)) } w.Move(w.height-1, 0) - w.CPrint(color, AttrRegular, - string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) + w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) } func (w *LightWindow) csi(code string) { @@ -745,6 +741,9 @@ func (w *LightWindow) MoveAndClear(y int, x int) { func attrCodes(attr Attr) []string { codes := []string{} + if (attr & AttrClear) > 0 { + return codes + } if (attr & Bold) > 0 { codes = append(codes, "1") } @@ -804,12 +803,8 @@ func cleanse(str string) string { return strings.Replace(str, "\x1b", "", -1) } -func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { - if !w.colored { - w.csiColor(colDefault, colDefault, attrFor(pair, attr)) - } else { - w.csiColor(pair.Fg(), pair.Bg(), attr) - } +func (w *LightWindow) CPrint(pair ColorPair, text string) { + w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) w.stderrInternal(cleanse(text), false) w.csi("m") } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index a4c599bb..e37b5ef8 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -77,11 +77,13 @@ const ( Blink = Attr(tcell.AttrBlink) Reverse = Attr(tcell.AttrReverse) Underline = Attr(tcell.AttrUnderline) - Italic = Attr(tcell.AttrNone) // Not supported + Italic = Attr(tcell.AttrItalic) ) const ( - AttrRegular Attr = 0 + AttrUndefined = Attr(0) + AttrRegular = Attr(1 << 7) + AttrClear = Attr(1 << 8) ) func (r *FullscreenRenderer) defaultTheme() *ColorTheme { @@ -414,7 +416,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, normal = ColPreview } return &TcellWindow{ - color: r.theme != nil, + color: r.theme.Colored, preview: preview, top: top, left: left, @@ -460,27 +462,23 @@ func (w *TcellWindow) MoveAndClear(y int, x int) { } func (w *TcellWindow) Print(text string) { - w.printString(text, w.normal, 0) + w.printString(text, w.normal) } -func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { +func (w *TcellWindow) printString(text string, pair ColorPair) { t := text lx := 0 + a := pair.Attr() - var style tcell.Style - if w.color { - style = pair.style(). + style := pair.style() + if a&AttrClear == 0 { + style = style. Reverse(a&Attr(tcell.AttrReverse) != 0). - Underline(a&Attr(tcell.AttrUnderline) != 0) - } else { - style = w.normal.style(). - Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). - Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) + Underline(a&Attr(tcell.AttrUnderline) != 0). + Italic(a&Attr(tcell.AttrItalic) != 0). + Blink(a&Attr(tcell.AttrBlink) != 0). + Dim(a&Attr(tcell.AttrDim) != 0) } - style = style. - Blink(a&Attr(tcell.AttrBlink) != 0). - Bold(a&Attr(tcell.AttrBold) != 0). - Dim(a&Attr(tcell.AttrDim) != 0) for { if len(t) == 0 { @@ -513,8 +511,8 @@ func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { w.lastX += lx } -func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { - w.printString(text, pair, attr) +func (w *TcellWindow) CPrint(pair ColorPair, text string) { + w.printString(text, pair) } func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn { @@ -531,7 +529,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn Bold(a&Attr(tcell.AttrBold) != 0). Dim(a&Attr(tcell.AttrDim) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0). - Underline(a&Attr(tcell.AttrUnderline) != 0) + Underline(a&Attr(tcell.AttrUnderline) != 0). + Italic(a&Attr(tcell.AttrItalic) != 0) for _, r := range text { if r == '\n' { @@ -574,7 +573,7 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { if bg == colDefault { bg = w.normal.Bg() } - return w.fillString(str, NewColorPair(fg, bg), a) + return w.fillString(str, NewColorPair(fg, bg, a), a) } func (w *TcellWindow) drawBorder() { diff --git a/src/tui/tui.go b/src/tui/tui.go index 146aafac..40c75112 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -123,6 +123,15 @@ func (c Color) is24() bool { return c > 0 && (c&(1<<24)) > 0 } +type ColorAttr struct { + Color Color + Attr Attr +} + +func NewColorAttr() ColorAttr { + return ColorAttr{Color: colUndefined, Attr: AttrUndefined} +} + const ( colUndefined Color = -2 colDefault Color = -1 @@ -148,9 +157,9 @@ const ( ) type ColorPair struct { - fg Color - bg Color - id int + fg Color + bg Color + attr Attr } func HexToColor(rrggbb string) Color { @@ -160,8 +169,8 @@ func HexToColor(rrggbb string) Color { return Color((1 << 24) + (r << 16) + (g << 8) + b) } -func NewColorPair(fg Color, bg Color) ColorPair { - return ColorPair{fg, bg, -1} +func NewColorPair(fg Color, bg Color, attr Attr) ColorPair { + return ColorPair{fg, bg, attr} } func (p ColorPair) Fg() Color { @@ -172,23 +181,59 @@ func (p ColorPair) Bg() Color { return p.bg } +func (p ColorPair) Attr() Attr { + return p.attr +} + +func (p ColorPair) merge(other ColorPair, except Color) ColorPair { + dup := p + dup.attr = dup.attr.Merge(other.attr) + if other.fg != except { + dup.fg = other.fg + } + if other.bg != except { + dup.bg = other.bg + } + return dup +} + +func (p ColorPair) WithAttr(attr Attr) ColorPair { + dup := p + dup.attr = dup.attr.Merge(attr) + return dup +} + +func (p ColorPair) MergeAttr(other ColorPair) ColorPair { + return p.WithAttr(other.attr) +} + +func (p ColorPair) Merge(other ColorPair) ColorPair { + return p.merge(other, colUndefined) +} + +func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair { + return p.merge(other, colDefault) +} + type ColorTheme struct { - Fg Color - Bg Color - PreviewFg Color - PreviewBg Color - DarkBg Color - Gutter Color - Prompt Color - Match Color - Current Color - CurrentMatch Color - Spinner Color - Info Color - Cursor Color - Selected Color - Header Color - Border Color + Colored bool + Input ColorAttr + Fg ColorAttr + Bg ColorAttr + PreviewFg ColorAttr + PreviewBg ColorAttr + DarkBg ColorAttr + Gutter ColorAttr + Prompt ColorAttr + Match ColorAttr + Current ColorAttr + CurrentMatch ColorAttr + Spinner ColorAttr + Info ColorAttr + Cursor ColorAttr + Selected ColorAttr + Header ColorAttr + Border ColorAttr } type Event struct { @@ -307,7 +352,7 @@ type Window interface { Move |