405 lines
9.1 KiB
Go
405 lines
9.1 KiB
Go
package fuzzyfinder
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
runewidth "github.com/mattn/go-runewidth"
|
|
"github.com/nsf/termbox-go"
|
|
)
|
|
|
|
type cell struct {
|
|
ch rune
|
|
bg, fg termbox.Attribute
|
|
}
|
|
|
|
type simScreen tcell.SimulationScreen
|
|
|
|
// TerminalMock is a mocked terminal for testing.
|
|
// Most users should use it by calling UseMockedTerminal.
|
|
type TerminalMock struct {
|
|
simScreen
|
|
sizeMu sync.RWMutex
|
|
width, height int
|
|
|
|
eventsMu sync.Mutex
|
|
events []termbox.Event
|
|
|
|
cellsMu sync.RWMutex
|
|
cells []*cell
|
|
|
|
resultMu sync.RWMutex
|
|
result string
|
|
|
|
sleepDuration time.Duration
|
|
v2 bool
|
|
}
|
|
|
|
// SetSize changes the pseudo-size of the window.
|
|
// Note that SetSize resets added cells.
|
|
func (m *TerminalMock) SetSize(w, h int) {
|
|
if m.v2 {
|
|
m.simScreen.SetSize(w, h)
|
|
return
|
|
}
|
|
m.sizeMu.Lock()
|
|
defer m.sizeMu.Unlock()
|
|
m.cellsMu.Lock()
|
|
defer m.cellsMu.Unlock()
|
|
m.width = w
|
|
m.height = h
|
|
m.cells = make([]*cell, w*h)
|
|
}
|
|
|
|
// Deprecated: Use SetEventsV2
|
|
// SetEvents sets all events, which are fetched by pollEvent.
|
|
// A user of this must set the EscKey event at the end.
|
|
func (m *TerminalMock) SetEvents(events ...termbox.Event) {
|
|
m.eventsMu.Lock()
|
|
defer m.eventsMu.Unlock()
|
|
m.events = events
|
|
}
|
|
|
|
// SetEventsV2 sets all events, which are fetched by pollEvent.
|
|
// A user of this must set the EscKey event at the end.
|
|
func (m *TerminalMock) SetEventsV2(events ...tcell.Event) {
|
|
for _, event := range events {
|
|
switch event := event.(type) {
|
|
case *tcell.EventKey:
|
|
ek := event
|
|
m.simScreen.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers())
|
|
case *tcell.EventResize:
|
|
er := event
|
|
w, h := er.Size()
|
|
m.simScreen.SetSize(w, h)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetResult returns a flushed string that is displayed to the actual terminal.
|
|
// It contains all escape sequences such that ANSI escape code.
|
|
func (m *TerminalMock) GetResult() string {
|
|
if !m.v2 {
|
|
m.resultMu.RLock()
|
|
defer m.resultMu.RUnlock()
|
|
return m.result
|
|
}
|
|
|
|
var s string
|
|
|
|
// set cursor for snapshot test
|
|
setCursor := func() {
|
|
cursorX, cursorY, _ := m.simScreen.GetCursor()
|
|
mainc, _, _, _ := m.simScreen.GetContent(cursorX, cursorY)
|
|
if mainc == ' ' {
|
|
m.simScreen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault))
|
|
} else {
|
|
m.simScreen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite))
|
|
}
|
|
m.simScreen.Show()
|
|
}
|
|
|
|
setCursor()
|
|
|
|
m.resultMu.Lock()
|
|
|
|
cells, width, height := m.simScreen.GetContents()
|
|
|
|
for h := 0; h < height; h++ {
|
|
prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault
|
|
|
|
for w := 0; w < width; w++ {
|
|
cell := cells[h*width+w]
|
|
fg, bg, attr := cell.Style.Decompose()
|
|
if fg != prevFg || bg != prevBg {
|
|
prevFg, prevBg = fg, bg
|
|
|
|
s += "\x1b\x5b\x6d" // Reset previous color.
|
|
v := parseAttrV2(fg, bg, attr)
|
|
s += v
|
|
}
|
|
|
|
s += string(cell.Runes)
|
|
rw := runewidth.RuneWidth(cell.Runes[0])
|
|
if rw != 0 {
|
|
w += rw - 1
|
|
}
|
|
}
|
|
s += "\n"
|
|
}
|
|
s += "\x1b\x5b\x6d" // Reset previous color.
|
|
|
|
m.resultMu.Unlock()
|
|
|
|
return s
|
|
}
|
|
|
|
func (m *TerminalMock) init() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *TerminalMock) size() (width int, height int) {
|
|
m.sizeMu.RLock()
|
|
defer m.sizeMu.RUnlock()
|
|
return m.width, m.height
|
|
}
|
|
|
|
func (m *TerminalMock) clear(fg termbox.Attribute, bg termbox.Attribute) error {
|
|
// TODO
|
|
return nil
|
|
}
|
|
|
|
func (m *TerminalMock) setCell(x int, y int, ch rune, fg termbox.Attribute, bg termbox.Attribute) {
|
|
m.sizeMu.RLock()
|
|
defer m.sizeMu.RUnlock()
|
|
m.cellsMu.Lock()
|
|
defer m.cellsMu.Unlock()
|
|
|
|
if x < 0 || x >= m.width {
|
|
return
|
|
}
|
|
if y < 0 || y >= m.height {
|
|
return
|
|
}
|
|
m.cells[y*m.width+x] = &cell{ch: ch, fg: fg, bg: bg}
|
|
}
|
|
|
|
func (m *TerminalMock) setCursor(x int, y int) {
|
|
m.sizeMu.RLock()
|
|
defer m.sizeMu.RUnlock()
|
|
m.cellsMu.Lock()
|
|
defer m.cellsMu.Unlock()
|
|
if x < 0 || x >= m.width {
|
|
return
|
|
}
|
|
if y < 0 || y >= m.height {
|
|
return
|
|
}
|
|
i := y*m.width + x
|
|
if m.cells[i] == nil {
|
|
m.cells[y*m.width+x] = &cell{ch: '\u2588', fg: termbox.ColorWhite, bg: termbox.ColorDefault}
|
|
} else {
|
|
// Cursor on a rune.
|
|
m.cells[y*m.width+x].bg = termbox.ColorWhite
|
|
}
|
|
}
|
|
|
|
func (m *TerminalMock) pollEvent() termbox.Event {
|
|
m.eventsMu.Lock()
|
|
defer m.eventsMu.Unlock()
|
|
if len(m.events) == 0 {
|
|
panic("pollEvent called with empty events. have you set expected events by SetEvents?")
|
|
}
|
|
e := m.events[0]
|
|
m.events = m.events[1:]
|
|
// Wait a moment for goroutine scheduling.
|
|
time.Sleep(m.sleepDuration)
|
|
return e
|
|
}
|
|
|
|
// flush displays all items with formatted layout.
|
|
func (m *TerminalMock) flush() {
|
|
m.cellsMu.RLock()
|
|
|
|
var s string
|
|
for j := 0; j < m.height; j++ {
|
|
prevFg, prevBg := termbox.ColorDefault, termbox.ColorDefault
|
|
for i := 0; i < m.width; i++ {
|
|
c := m.cells[j*m.width+i]
|
|
if c == nil {
|
|
s += " "
|
|
prevFg, prevBg = termbox.ColorDefault, termbox.ColorDefault
|
|
continue
|
|
} else {
|
|
var fgReset bool
|
|
if c.fg != prevFg {
|
|
s += "\x1b\x5b\x6d" // Reset previous color.
|
|
s += parseAttr(c.fg, true)
|
|
prevFg = c.fg
|
|
prevBg = termbox.ColorDefault
|
|
fgReset = true
|
|
}
|
|
if c.bg != prevBg {
|
|
if !fgReset {
|
|
s += "\x1b\x5b\x6d" // Reset previous color.
|
|
prevFg = termbox.ColorDefault
|
|
}
|
|
s += parseAttr(c.bg, false)
|
|
prevBg = c.bg
|
|
}
|
|
s += string(c.ch)
|
|
rw := runewidth.RuneWidth(c.ch)
|
|
if rw != 0 {
|
|
i += rw - 1
|
|
}
|
|
}
|
|
}
|
|
s += "\n"
|
|
}
|
|
s += "\x1b\x5b\x6d" // Reset previous color.
|
|
|
|
m.cellsMu.RUnlock()
|
|
m.cellsMu.Lock()
|
|
m.cells = make([]*cell, m.width*m.height)
|
|
m.cellsMu.Unlock()
|
|
|
|
m.resultMu.Lock()
|
|
defer m.resultMu.Unlock()
|
|
|
|
m.result = s
|
|
}
|
|
|
|
func (m *TerminalMock) close() {}
|
|
|
|
// UseMockedTerminal switches the terminal, which is used from
|
|
// this package to a mocked one.
|
|
func UseMockedTerminal() *TerminalMock {
|
|
f := newFinder()
|
|
return f.UseMockedTerminal()
|
|
}
|
|
|
|
// UseMockedTerminalV2 switches the terminal, which is used from
|
|
// this package to a mocked one.
|
|
func UseMockedTerminalV2() *TerminalMock {
|
|
f := newFinder()
|
|
return f.UseMockedTerminalV2()
|
|
}
|
|
|
|
func (f *finder) UseMockedTerminal() *TerminalMock {
|
|
m := &TerminalMock{}
|
|
f.term = m
|
|
return m
|
|
}
|
|
|
|
func (f *finder) UseMockedTerminalV2() *TerminalMock {
|
|
screen := tcell.NewSimulationScreen("UTF-8")
|
|
if err := screen.Init(); err != nil {
|
|
panic(err)
|
|
}
|
|
m := &TerminalMock{
|
|
simScreen: screen,
|
|
v2: true,
|
|
}
|
|
f.term = m
|
|
return m
|
|
}
|
|
|
|
// parseAttr parses an attribute of termbox
|
|
// as an escape sequence.
|
|
// parseAttr doesn't support output modes othar than color256 in termbox-go.
|
|
func parseAttr(attr termbox.Attribute, isFg bool) string {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("\x1b[")
|
|
if attr >= termbox.AttrReverse {
|
|
buf.WriteString("7;")
|
|
attr -= termbox.AttrReverse
|
|
}
|
|
if attr >= termbox.AttrUnderline {
|
|
buf.WriteString("4;")
|
|
attr -= termbox.AttrUnderline
|
|
}
|
|
if attr >= termbox.AttrBold {
|
|
buf.WriteString("1;")
|
|
attr -= termbox.AttrBold
|
|
}
|
|
|
|
if attr > termbox.ColorWhite {
|
|
panic(fmt.Sprintf("invalid color code: %d", attr))
|
|
}
|
|
|
|
if attr == termbox.ColorDefault {
|
|
if isFg {
|
|
buf.WriteString("39")
|
|
} else {
|
|
buf.WriteString("49")
|
|
}
|
|
} else {
|
|
color := int(attr) - 1
|
|
if isFg {
|
|
fmt.Fprintf(&buf, "38;5;%d", color)
|
|
} else {
|
|
fmt.Fprintf(&buf, "48;5;%d", color)
|
|
}
|
|
}
|
|
buf.WriteString("m")
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// parseAttrV2 parses color and attribute for testing.
|
|
func parseAttrV2(fg, bg tcell.Color, attr tcell.AttrMask) string {
|
|
if attr == tcell.AttrInvalid {
|
|
panic("invalid attribute")
|
|
}
|
|
|
|
var params []string
|
|
if attr&tcell.AttrBold == tcell.AttrBold {
|
|
params = append(params, "1")
|
|
attr ^= tcell.AttrBold
|
|
}
|
|
if attr&tcell.AttrBlink == tcell.AttrBlink {
|
|
params = append(params, "5")
|
|
attr ^= tcell.AttrBlink
|
|
}
|
|
if attr&tcell.AttrReverse == tcell.AttrReverse {
|
|
params = append(params, "7")
|
|
attr ^= tcell.AttrReverse
|
|
}
|
|
if attr&tcell.AttrUnderline == tcell.AttrUnderline {
|
|
params = append(params, "4")
|
|
attr ^= tcell.AttrUnderline
|
|
}
|
|
if attr&tcell.AttrDim == tcell.AttrDim {
|
|
params = append(params, "2")
|
|
attr ^= tcell.AttrDim
|
|
}
|
|
if attr&tcell.AttrItalic == tcell.AttrItalic {
|
|
params = append(params, "3")
|
|
attr ^= tcell.AttrItalic
|
|
}
|
|
if attr&tcell.AttrStrikeThrough == tcell.AttrStrikeThrough {
|
|
params = append(params, "9")
|
|
attr ^= tcell.AttrStrikeThrough
|
|
}
|
|
|
|
switch {
|
|
case fg == 0: // Ignore.
|
|
case fg == tcell.ColorDefault:
|
|
params = append(params, "39")
|
|
case fg > tcell.Color255:
|
|
r, g, b := fg.RGB()
|
|
params = append(params, "38", "2", fmt.Sprint(r), fmt.Sprint(g), fmt.Sprint(b))
|
|
default:
|
|
params = append(params, "38", "5", fmt.Sprint(fg-tcell.ColorValid))
|
|
}
|
|
|
|
switch {
|
|
case bg == 0: // Ignore.
|
|
case bg == tcell.ColorDefault:
|
|
params = append(params, "49")
|
|
case bg > tcell.Color255:
|
|
r, g, b := bg.RGB()
|
|
params = append(params, "48", "2", fmt.Sprint(r), fmt.Sprint(g), fmt.Sprint(b))
|
|
default:
|
|
params = append(params, "48", "5", fmt.Sprint(bg-tcell.ColorValid))
|
|
}
|
|
|
|
return fmt.Sprintf("\x1b[%sm", strings.Join(params, ";"))
|
|
}
|
|
|
|
func toAnsi3bit(color tcell.Color) int {
|
|
colors := []tcell.Color{
|
|
tcell.ColorBlack, tcell.ColorRed, tcell.ColorGreen, tcell.ColorYellow, tcell.ColorBlue, tcell.ColorDarkMagenta, tcell.ColorDarkCyan, tcell.ColorWhite,
|
|
}
|
|
for i, c := range colors {
|
|
if c == color {
|
|
return i
|
|
}
|
|
}
|
|
return 0
|
|
}
|