From 0c573b3dffe806253e1df2447754a5f3939a11f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 24 Oct 2016 09:44:56 +0900 Subject: Prepare for termbox/windows build `TAGS=termbox make` (or `go build -tags termbox`) --- src/Makefile | 2 +- src/README.md | 6 +- src/ansi.go | 28 +- src/ansi_test.go | 8 +- src/constants.go | 1 - src/constants_unix.go | 8 + src/constants_windows.go | 8 + src/curses/curses.go | 714 ---------------------------------------------- src/curses/curses_test.go | 14 - src/options.go | 88 +++--- src/options_test.go | 172 +++++------ src/result.go | 20 +- src/result_test.go | 24 +- src/terminal.go | 189 ++++++------ src/terminal_unix.go | 13 + src/terminal_windows.go | 11 + src/tui/ncurses.go | 513 +++++++++++++++++++++++++++++++++ src/tui/termbox.go | 151 ++++++++++ src/tui/tui.go | 250 ++++++++++++++++ src/tui/tui_test.go | 14 + src/util/util.go | 17 +- src/util/util_unix.go | 17 ++ src/util/util_windows.go | 17 ++ 23 files changed, 1275 insertions(+), 1010 deletions(-) create mode 100644 src/constants_unix.go create mode 100644 src/constants_windows.go delete mode 100644 src/curses/curses.go delete mode 100644 src/curses/curses_test.go create mode 100644 src/terminal_unix.go create mode 100644 src/terminal_windows.go create mode 100644 src/tui/ncurses.go create mode 100644 src/tui/termbox.go create mode 100644 src/tui/tui.go create mode 100644 src/tui/tui_test.go create mode 100644 src/util/util_unix.go create mode 100644 src/util/util_windows.go diff --git a/src/Makefile b/src/Makefile index f7aa0b2d..edc77faf 100644 --- a/src/Makefile +++ b/src/Makefile @@ -43,7 +43,7 @@ $(SRCDIR): ln -s $(ROOTDIR) $(SRCDIR) deps: $(SRCDIR) $(SOURCES) - cd $(SRCDIR) && go get + cd $(SRCDIR) && go get -tags "$(TAGS)" android-build: $(SRCDIR) cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get diff --git a/src/README.md b/src/README.md index 272c7554..e8b8debc 100644 --- a/src/README.md +++ b/src/README.md @@ -83,9 +83,11 @@ Third-party libraries used - [ncurses][ncurses] - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - - Licensed under [MIT](http://mattn.mit-license.org/2013) + - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - - Licensed under [MIT](http://mattn.mit-license.org/2014) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-isatty](https://github.com/mattn/go-isatty) + - Licensed under [MIT](http://mattn.mit-license.org) License ------- diff --git a/src/ansi.go b/src/ansi.go index 02be18c6..b7d3a9e3 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -7,7 +7,7 @@ import ( "strings" "unicode/utf8" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" ) type ansiOffset struct { @@ -16,9 +16,9 @@ type ansiOffset struct { } type ansiState struct { - fg int - bg int - attr curses.Attr + fg tui.Color + bg tui.Color + attr tui.Attr } func (s *ansiState) colored() bool { @@ -134,26 +134,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { case 49: state.bg = -1 case 1: - state.attr = curses.Bold + state.attr = tui.Bold case 2: - state.attr = curses.Dim + state.attr = tui.Dim case 4: - state.attr = curses.Underline + state.attr = tui.Underline case 5: - state.attr = curses.Blink + state.attr = tui.Blink case 7: - state.attr = curses.Reverse + state.attr = tui.Reverse case 0: init() default: if num >= 30 && num <= 37 { - state.fg = num - 30 + state.fg = tui.Color(num - 30) } else if num >= 40 && num <= 47 { - state.bg = num - 40 + state.bg = tui.Color(num - 40) } else if num >= 90 && num <= 97 { - state.fg = num - 90 + 8 + state.fg = tui.Color(num - 90 + 8) } else if num >= 100 && num <= 107 { - state.bg = num - 100 + 8 + state.bg = tui.Color(num - 100 + 8) } } case 1: @@ -164,7 +164,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { state256 = 0 } case 2: - *ptr = num + *ptr = tui.Color(num) state256 = 0 } } diff --git a/src/ansi_test.go b/src/ansi_test.go index 0ba9e400..3afcc6c5 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" ) func TestExtractColor(t *testing.T) { - assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { - var attr curses.Attr + assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { + var attr tui.Attr if bold { - attr = curses.Bold + attr = tui.Bold } if offset.offset[0] != b || offset.offset[1] != e || offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { diff --git a/src/constants.go b/src/constants.go index bec3bb6c..778d219a 100644 --- a/src/constants.go +++ b/src/constants.go @@ -15,7 +15,6 @@ const ( coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader - defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` readerBufferSize = 64 * 1024 // Terminal diff --git a/src/constants_unix.go b/src/constants_unix.go new file mode 100644 index 00000000..52677e6c --- /dev/null +++ b/src/constants_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package fzf + +const ( + // Reader + defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` +) diff --git a/src/constants_windows.go b/src/constants_windows.go new file mode 100644 index 00000000..efd3f11c --- /dev/null +++ b/src/constants_windows.go @@ -0,0 +1,8 @@ +// +build windows + +package fzf + +const ( + // Reader + defaultCommand = `dir /s/b` +) diff --git a/src/curses/curses.go b/src/curses/curses.go deleted file mode 100644 index 638a8629..00000000 --- a/src/curses/curses.go +++ /dev/null @@ -1,714 +0,0 @@ -package curses - -/* -#include -#include -#cgo !static LDFLAGS: -lncurses -#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl -#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch - -SCREEN *c_newterm () { - return newterm(NULL, stderr, stdin); -} - -*/ -import "C" - -import ( - "fmt" - "os" - "strings" - "syscall" - "time" - "unicode/utf8" -) - -const ( - Bold = C.A_BOLD - Dim = C.A_DIM - Blink = C.A_BLINK - Reverse = C.A_REVERSE - Underline = C.A_UNDERLINE -) - -type Attr C.int - -// Types of user action -const ( - Rune = iota - - CtrlA - CtrlB - CtrlC - CtrlD - CtrlE - CtrlF - CtrlG - CtrlH - Tab - CtrlJ - CtrlK - CtrlL - CtrlM - CtrlN - CtrlO - CtrlP - CtrlQ - CtrlR - CtrlS - CtrlT - CtrlU - CtrlV - CtrlW - CtrlX - CtrlY - CtrlZ - ESC - - Invalid - Mouse - DoubleClick - - BTab - BSpace - - Del - PgUp - PgDn - - Up - Down - Left - Right - Home - End - - SLeft - SRight - - F1 - F2 - F3 - F4 - F5 - F6 - F7 - F8 - F9 - F10 - - AltEnter - AltSpace - AltSlash - AltBS - AltA - AltB - AltC - AltD - AltE - AltF - - AltZ = AltA + 'z' - 'a' -) - -// Pallete -const ( - _ = iota - ColNormal - ColPrompt - ColMatch - ColCurrent - ColCurrentMatch - ColSpinner - ColInfo - ColCursor - ColSelected - ColHeader - ColBorder - ColUser // Should be the last entry -) - -const ( - doubleClickDuration = 500 * time.Millisecond - colDefault = -1 - colUndefined = -2 -) - -type ColorTheme struct { - Fg int16 - Bg int16 - DarkBg int16 - Prompt int16 - Match int16 - Current int16 - CurrentMatch int16 - Spinner int16 - Info int16 - Cursor int16 - Selected int16 - Header int16 - Border int16 -} - -type Event struct { - Type int - Char rune - MouseEvent *MouseEvent -} - -type MouseEvent struct { - Y int - X int - S int - Down bool - Double bool - Mod bool -} - -var ( - _buf []byte - _in *os.File - _color bool - _colorFn func(int, Attr) C.int - _colorMap map[int]int - _prevDownTime time.Time - _clickY []int - _screen *C.SCREEN - Default16 *ColorTheme - Dark256 *ColorTheme - Light256 *ColorTheme -) - -type Window struct { - win *C.WINDOW - Top int - Left int - Width int - Height int -} - -func NewWindow(top int, left int, width int, height int, border bool) *Window { - win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) - if _color { - C.wbkgd(win, C.chtype(C.COLOR_PAIR(ColNormal))) - } - if border { - attr := _colorFn(ColBorder, 0) - C.wattron(win, attr) - C.box(win, 0, 0) - C.wattroff(win, attr) - } - - return &Window{ - win: win, - Top: top, - Left: left, - Width: width, - Height: height, - } -} - -func EmptyTheme() *ColorTheme { - return &ColorTheme{ - Fg: colUndefined, - Bg: colUndefined, - DarkBg: colUndefined, - Prompt: colUndefined, - Match: colUndefined, - Current: colUndefined, - CurrentMatch: colUndefined, - Spinner: colUndefined, - Info: colUndefined, - Cursor: colUndefined, - Selected: colUndefined, - Header: colUndefined, - Border: colUndefined} -} - -func init() { - _prevDownTime = time.Unix(0, 0) - _clickY = []int{} - _colorMap = make(map[int]int) - Default16 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - 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, - Header: C.COLOR_CYAN, - Border: C.COLOR_BLACK} - Dark256 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - DarkBg: 236, - Prompt: 110, - Match: 108, - Current: 254, - CurrentMatch: 151, - Spinner: 148, - Info: 144, - Cursor: 161, - Selected: 168, - Header: 109, - Border: 59} - Light256 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - DarkBg: 251, - Prompt: 25, - Match: 66, - Current: 237, - CurrentMatch: 23, - Spinner: 65, - Info: 101, - Cursor: 161, - Selected: 168, - Header: 31, - Border: 145} -} - -func attrColored(pair int, a Attr) C.int { - var attr C.int - if pair > 0 { - attr = C.COLOR_PAIR(C.int(pair)) - } - return attr | C.int(a) -} - -func attrMono(pair int, a Attr) C.int { - var attr C.int - switch pair { - case ColCurrent: - if a&C.A_BOLD == C.A_BOLD { - attr = C.A_REVERSE - } - case ColMatch: - attr = C.A_UNDERLINE - case ColCurrentMatch: - attr = C.A_UNDERLINE | C.A_REVERSE - } - if a&C.A_BOLD == C.A_BOLD { - attr = attr | C.A_BOLD - } - return attr -} - -func MaxX() int { - return int(C.COLS) -} - -func MaxY() int { - return int(C.LINES) -} - -func getch(nonblock bool) int { - b := make([]byte, 1) - syscall.SetNonblock(int(_in.Fd()), nonblock) - _, err := _in.Read(b) - if err != nil { - return -1 - } - return int(b[0]) -} - -func Init(theme *ColorTheme, black bool, mouse bool) { - { - in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) - if err != nil { - panic("Failed to open /dev/tty") - } - _in = in - // Break STDIN - // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) - } - - C.setlocale(C.LC_ALL, C.CString("")) - _screen = C.c_newterm() - if _screen == nil { - fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - os.Exit(2) - } - C.set_term(_screen) - if mouse { - C.mousemask(C.ALL_MOUSE_EVENTS, nil) - } - C.noecho() - C.raw() // stty dsusp undef - - _color = theme != nil - if _color { - C.start_color() - var baseTheme *ColorTheme - if C.tigetnum(C.CString("colors")) >= 256 { - baseTheme = Dark256 - } else { - baseTheme = Default16 - } - initPairs(baseTheme, theme, black) - C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal))) - _colorFn = attrColored - } else { - _colorFn = attrMono - } -} - -func override(baseTheme *ColorTheme, theme *ColorTheme) { - o := func(a int16, b int16) int16 { - if b == colUndefined { - return a - } - return b - } - theme.Fg = o(baseTheme.Fg, theme.Fg) - theme.Bg = o(baseTheme.Bg, theme.Bg) - theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) - theme.Prompt = o(baseTheme.Prompt, theme.Prompt) - theme.Match = o(baseTheme.Match, theme.Match) - theme.Current = o(baseTheme.Current, theme.Current) - theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) - theme.Spinner = o(baseTheme.Spinner, theme.Spinner) - theme.Info = o(baseTheme.Info, theme.Info) - theme.Cursor = o(baseTheme.Cursor, theme.Cursor) - theme.Selected = o(baseTheme.Selected, theme.Selected) - theme.Header = o(baseTheme.Header, theme.Header) - theme.Border = o(baseTheme.Border, theme.Border) -} - -func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { - if black { - theme.Bg = C.COLOR_BLACK - } - // Updates theme - override(baseTheme, theme) - - C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - initPair := func(group C.short, fg int16, bg int16) { - C.init_pair(group, C.short(fg), C.short(bg)) - } - initPair(ColNormal, theme.Fg, theme.Bg) - initPair(ColPrompt, theme.Prompt, theme.Bg) - initPair(ColMatch, theme.Match, theme.Bg) - initPair(ColCurrent, theme.Current, theme.DarkBg) - initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) - initPair(ColSpinner, theme.Spinner, theme.Bg) - initPair(ColInfo, theme.Info, theme.Bg) - initPair(ColCursor, theme.Cursor, theme.DarkBg) - initPair(ColSelected, theme.Selected, theme.DarkBg) - initPair(ColHeader, theme.Header, theme.Bg) - initPair(ColBorder, theme.Border, theme.Bg) -} - -func Close() { - C.endwin() - C.delscreen(_screen) -} - -func GetBytes() []byte { - c := getch(false) - _buf = append(_buf, byte(c)) - - for { - c = getch(true) - if c == -1 { - break - } - _buf = append(_buf, byte(c)) - } - - return _buf -} - -// 27 (91 79) 77 type x y -func mouseSequence(sz *int) Event { - if len(_buf) < 6 { - return Event{Invalid, 0, nil} - } - *sz = 6 - switch _buf[3] { - case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl - 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl - mod := _buf[3] >= 36 - down := _buf[3]%2 == 0 - x := int(_buf[4] - 33) - y := int(_buf[5] - 33) - double := false - if down { - now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, y) - } else { - _clickY = []int{y} - } - _prevDownTime = now - } else { - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { - double = true - } - } - return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} - case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl - 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl - mod := _buf[3] >= 100 - s := 1 - int(_buf[3]%2)*2 - x := int(_buf[4] - 33) - y := int(_buf[5] - 33) - return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} - } - return Event{Invalid, 0, nil} -} - -func escSequence(sz *int) Event { - if len(_buf) < 2 { - return Event{ESC, 0, nil} - } - *sz = 2 - switch _buf[1] { - case 13: - return Event{AltEnter, 0, nil} - case 32: - return Event{AltSpace, 0, nil} - case 47: - return Event{AltSlash, 0, nil} - case 98: - return Event{AltB, 0, nil} - case 100: - return Event{AltD, 0, nil} - case 102: - return Event{AltF, 0, nil} - case 127: - return Event{AltBS, 0, nil} - case 91, 79: - if len(_buf) < 3 { - return Event{Invalid, 0, nil} - } - *sz = 3 - switch _buf[2] { - case 68: - return Event{Left, 0, nil} - case 67: - return Event{Right, 0, nil} - case 66: - return Event{Down, 0, nil} - case 65: - return Event{Up, 0, nil} - case 90: - return Event{BTab, 0, nil} - case 72: - return Event{Home, 0, nil} - case 70: - return Event{End, 0, nil} - case 77: - return mouseSequence(sz) - case 80: - return Event{F1, 0, nil} - case 81: - return Event{F2, 0, nil} - case 82: - return Event{F3, 0, nil} - case 83: - return Event{F4, 0, nil} - case 49, 50, 51, 52, 53, 54: - if len(_buf) < 4 { - return Event{Invalid, 0, nil} - } - *sz = 4 - switch _buf[2] { - case 50: - if len(_buf) == 5 && _buf[4] == 126 { - *sz = 5 - switch _buf[3] { - case 48: - return Event{F9, 0, nil} - case 49: - return Event{F10, 0, nil} - } - } - // Bracketed paste mode \e[200~ / \e[201 - if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { - *sz = 6 - return Event{Invalid, 0, nil} - } - return Event{Invalid, 0, nil} // INS - case 51: - return Event{Del, 0, nil} - case 52: - return Event{End, 0, nil} - case 53: - return Event{PgUp, 0, nil} - case 54: - return Event{PgDn, 0, nil} - case 49: - switch _buf[3] { - case 126: - return Event{Home, 0, nil} - case 53, 55, 56, 57: - if len(_buf) == 5 && _buf[4] == 126 { - *sz = 5 - switch _buf[3] { - case 53: - return Event{F5, 0, nil} - case 55: - return Event{F6, 0, nil} - case 56: - return Event{F7, 0, nil} - case 57: - return Event{F8, 0, nil} - } - } - return Event{Invalid, 0, nil} - case 59: - if len(_buf) != 6 { - return Event{Invalid, 0, nil} - } - *sz = 6 - switch _buf[4] { - case 50: - switch _buf[5] { - case 68: - return Event{Home, 0, nil} - case 67: - return Event{End, 0, nil} - } - case 53: - switch _buf[5] { - case 68: - return Event{SLeft, 0, nil} - case 67: - return Event{SRight, 0, nil} - } - } // _buf[4] - } // _buf[3] - } // _buf[2] - } // _buf[2] - } // _buf[1] - if _buf[1] >= 'a' && _buf[1] <= 'z' { - return Event{AltA + int(_buf[1]) - 'a', 0, nil} - } - return Event{Invalid, 0, nil} -} - -func GetChar() Event { - if len(_buf) == 0 { - _buf = GetBytes() - } - if len(_buf) == 0 { - panic("Empty _buffer") - } - - sz := 1 - defer func() { - _buf = _buf[sz:] - }() - - switch _buf[0] { - case CtrlC: - return Event{CtrlC, 0, nil} - case CtrlG: - return Event{CtrlG, 0, nil} - case CtrlQ: - return Event{CtrlQ, 0, nil} - case 127: - return Event{BSpace, 0, nil} - case ESC: - return escSequence(&sz) - } - - // CTRL-A ~ CTRL-Z - if _buf[0] <= CtrlZ { - return Event{int(_buf[0]), 0, nil} - } - r, rsz := utf8.DecodeRune(_buf) - if r == utf8.RuneError { - return Event{ESC, 0, nil} - } - sz = rsz - return Event{Rune, r, nil} -} - -func (w *Window) Close() { - C.delwin(w.win) -} - -func (w *Window) Enclose(y int, x int) bool { - return bool(C.wenclose(w.win, C.int(y), C.int(x))) -} - -func (w *Window) Move(y int, x int) { - C.wmove(w.win, C.int(y), C.int(x)) -} - -func (w *Window) MoveAndClear(y int, x int) { - w.Move(y, x) - C.wclrtoeol(w.win) -} - -func (w *Window) Print(text string) { - C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { - if r < 32 { - return -1 - } - return r - }, text))) -} - -func (w *Window) CPrint(pair int, a Attr, text string) { - attr := _colorFn(pair, a) - C.wattron(w.win, attr) - w.Print(text) - C.wattroff(w.win, attr) -} - -func Clear() { - C.clear() -} - -func Endwin() { - C.endwin() -} - -func Refresh() { - C.refresh() -} - -func (w *Window) Erase() { - C.werase(w.win) -} - -func (w *Window) Fill(str string) bool { - return C.waddstr(w.win, C.CString(str)) == C.OK -} - -func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { - attr := _colorFn(PairFor(fg, bg), a) - C.wattron(w.win, attr) - ret := w.Fill(str) - C.wattroff(w.win, attr) - return ret -} - -func (w *Window) Refresh() { - C.wnoutrefresh(w.win) -} - -func DoUpdate() { - C.doupdate() -} - -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 -} diff --git a/src/curses/curses_test.go b/src/curses/curses_test.go deleted file mode 100644 index db75c408..00000000 --- a/src/curses/curses_test.go +++ /dev/null @@ -1,14 +0,0 @@ -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() - } -} diff --git a/src/options.go b/src/options.go index c5e6d7b7..a3ed22d9 100644 --- a/src/options.go +++ b/src/options.go @@ -9,7 +9,7 @@ import ( "unicode/utf8" "github.com/junegunn/fzf/src/algo" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/go-shellwords" ) @@ -142,7 +142,7 @@ type Options struct { Multi bool Ansi bool Mouse bool - Theme *curses.ColorTheme + Theme *tui.ColorTheme Black bool Reverse bool Cycle bool @@ -187,7 +187,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Theme: curses.EmptyTheme(), + Theme: tui.EmptyTheme(), Black: false, Reverse: false, Cycle: false, @@ -358,60 +358,60 @@ func parseKeyChords(str string, message string) map[int]string { chord := 0 switch lkey { case "up": - chord = curses.Up + chord = tui.Up case "down": - chord = curses.Down + chord = tui.Down case "left": - chord = curses.Left + chord = tui.Left case "right": - chord = curses.Right + chord = tui.Right case "enter", "return": - chord = curses.CtrlM + chord = tui.CtrlM case "space": - chord = curses.AltZ + int(' ') + chord = tui.AltZ + int(' ') case "bspace", "bs": - chord = curses.BSpace + chord = tui.BSpace case "alt-enter", "alt-return": - chord = curses.AltEnter + chord = tui.AltEnter case "alt-space": - chord = curses.AltSpace + chord = tui.AltSpace case "alt-/": - chord = curses.AltSlash + chord = tui.AltSlash case "alt-bs", "alt-bspace": - chord = curses.AltBS + chord = tui.AltBS case "tab": - chord = curses.Tab + chord = tui.Tab case "btab", "shift-tab": - chord = curses.BTab + chord = tui.BTab case "esc": - chord = curses.ESC + chord = tui.ESC case "del": - chord = curses.Del + chord = tui.Del case "home": - chord = curses.Home + chord = tui.Home case "end": - chord = curses.End + chord = tui.End case "pgup", "page-up": - chord = curses.PgUp + chord = tui.PgUp case "pgdn", "page-down": - chord = curses.PgDn + chord = tui.PgDn case "shift-left": - chord = curses.SLeft + chord = tui.SLeft case "shift-right": - chord = curses.SRight + chord = tui.SRight case "double-click": - chord = curses.DoubleClick + chord = tui.DoubleClick case "f10": - chord = curses.F10 + chord = tui.F10 default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { - chord = curses.CtrlA + int(lkey[5]) - 'a' + chord = tui.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { - chord = curses.AltA + int(lkey[4]) - 'a' + chord = tui.AltA + int(lkey[4]) - 'a' } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { - chord = curses.F1 + int(key[1]) - '1' + chord = tui.F1 + int(key[1]) - '1' } else if utf8.RuneCountInString(key) == 1 { - chord = curses.AltZ + int([]rune(key)[0]) + chord = tui.AltZ + int([]rune(key)[0]) } else { errorExit("unsupported key: " + key) } @@ -458,7 +458,7 @@ func parseTiebreak(str string) []criterion { return criteria } -func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { +func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { if theme != nil { dupe := *theme return &dupe @@ -466,16 +466,16 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { return nil } -func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { +func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme := dupeTheme(defaultTheme) for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "dark": - theme = dupeTheme(curses.Dark256) + theme = dupeTheme(tui.Dark256) case "light": - theme = dupeTheme(curses.Light256) + theme = dupeTheme(tui.Light256) case "16": - theme = dupeTheme(curses.Default16) + theme = dupeTheme(tui.Default16) case "bw", "no": theme = nil default: @@ -495,7 +495,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme if err != nil || ansi32 < -1 || ansi32 > 255 { fail() } - ansi := int16(ansi32) + ansi := tui.Color(ansi32) switch pair[0] { case "fg": theme.Fg = ansi @@ -572,9 +572,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) } var key int if len(pair[0]) == 1 && pair[0][0] == escapedColon { - key = ':' + curses.AltZ + key = ':' + tui.AltZ } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { - key = ',' + curses.AltZ + key = ',' + tui.AltZ } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) @@ -868,7 +868,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { - opts.Theme = curses.EmptyTheme() + opts.Theme = tui.EmptyTheme() } else { opts.Theme = parseTheme(opts.Theme, spec) } @@ -905,7 +905,7 @@ func parseOptions(opts *Options, allArgs []string) { case "+c", "--no-color": opts.Theme = nil case "+2", "--no-256": - opts.Theme = curses.Default16 + opts.Theme = tui.Default16 case "--black": opts.Black = true case "--no-black": @@ -1071,11 +1071,11 @@ func parseOptions(opts *Options, allArgs []string) { func postProcessOptions(opts *Options) { // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { - if _, prs := opts.Keymap[curses.CtrlP]; !prs { - opts.Keymap[curses.CtrlP] = actPreviousHistory + if _, prs := opts.Keymap[tui.CtrlP]; !prs { + opts.Keymap[tui.CtrlP] = actPreviousHistory } - if _, prs := opts.Keymap[curses.CtrlN]; !prs { - opts.Keymap[curses.CtrlN] = actNextHistory + if _, prs := opts.Keymap[tui.CtrlN]; !prs { + opts.Keymap[tui.CtrlN] = actNextHistory } } diff --git a/src/options_test.go b/src/options_test.go index 60136173..f16569e3 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -133,48 +133,48 @@ func TestParseKeys(t *testing.T) { if len(pairs) != 11 { t.Error(11) } - check(curses.CtrlZ, "ctrl-z") - check(curses.AltZ, "alt-z") - check(curses.F2, "f2") - check(curses.AltZ+'@', "@") - check(curses.AltA, "Alt-a") - check(curses.AltZ+'!', "!") - check(curses.CtrlA+'g'-'a', "ctrl-G") - check(curses.AltZ+'J', "J") - check(curses.AltZ+'g', "g") - check(curses.AltEnter, "ALT-enter") - check(curses.AltSpace, "alt-SPACE") + check(tui.CtrlZ, "ctrl-z") + check(tui.AltZ, "alt-z") + check(tui.F2, "f2") + check(tui.AltZ+'@', "@") + check(tui.AltA, "Alt-a") + check(tui.AltZ+'!', "!") + check(tui.CtrlA+'g'-'a', "ctrl-G") + check(tui.AltZ+'J', "J") + check(tui.AltZ+'g', "g") + check(tui.AltEnter, "ALT-enter") + check(tui.AltSpace, "alt-SPACE") // Synonyms pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") if len(pairs) != 9 { t.Error(9) } - check(curses.CtrlM, "Return") - check(curses.AltZ+' ', "space") - check(curses.Tab, "tab") - check(curses.BTab, "btab") - check(curses.ESC, "esc") - check(curses.Up, "up") - check(curses.Down, "down") - check(curses.Left, "left") - check(curses.Right, "right") + check(tui.CtrlM, "Return") + check(tui.AltZ+' ', "space") + check(tui.Tab, "tab") + check(tui.BTab, "btab") + check(tui.ESC, "esc") + check(tui.Up, "up") + check(tui.Down, "down") + check(tui.Left, "left") + check(tui.Right, "right") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") if len(pairs) != 11 { t.Error(11) } - check(curses.Tab, "Ctrl-I") - check(curses.PgUp, "page-up") - check(curses.PgDn, "Page-Down") - check(curses.Home, "Home") - check(curses.End, "End") - check(curses.AltBS, "Alt-BSpace") - check(curses.SLeft, "shift-left") - check(curses.SRight, "shift-right") - check(curses.BTab, "shift-tab") - check(curses.CtrlM, "Enter") - check(curses.BSpace, "bspace") + check(tui.Tab, "Ctrl-I") + check(tui.PgUp, "page-up") + check(tui.PgDn, "Page-Down") + check(tui.Home, "Home") + check(tui.End, "End") + check(tui.AltBS, "Alt-BSpace") + check(tui.SLeft, "shift-left") + check(tui.SRight, "shift-right") + check(tui.BTab, "shift-tab") + check(tui.CtrlM, "Enter") + check(tui.BSpace, "bspace") } func TestParseKeysWithComma(t *testing.T) { @@ -191,36 +191,36 @@ func TestParseKeysWithComma(t *testing.T) { pairs := parseKeyChords(",", "") checkN(len(pairs), 1) - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+'c', "c") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+'c', "c") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,,", "") checkN(len(pairs), 1) - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+',', ",") } func TestBind(t *testing.T) { @@ -236,41 +236,41 @@ func TestBind(t *testing.T) { } keymap := defaultKeymap() execmap := make(map[int]string) - check(actBeginningOfLine, keymap[curses.CtrlA]) + check(actBeginningOfLine, keymap[tui.CtrlA]) parseKeymap(keymap, execmap, "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") - check(actKillLine, keymap[curses.CtrlA]) - check(actToggleSort, keymap[curses.CtrlB]) - check(actPageUp, keymap[curses.AltZ+'c']) - check(actAbort, keymap[curses.AltZ+',']) - check(actAccept, keymap[curses.AltZ+':']) - check(actPageDown, keymap[curses.AltZ]) - check(actExecute, keymap[curses.F1]) - check(actExecute, keymap[curses.F2]) - check(actExecute, keymap[curses.F3]) - check(actExecute, keymap[curses.F4]) - checkString("ls {}", execmap[curses.F1]) - checkString("echo {}, {}, {}", execmap[curses.F2]) - checkString("echo '({})'", execmap[curses.F3]) - checkString("less {}", execmap[curses.F4]) - checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) - checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) - checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) + check(actKillLine, keymap[tui.CtrlA]) + check(actToggleSort, keymap[tui.CtrlB]) + check(actPageUp, keymap[tui.AltZ+'c']) + check(actAbort, keymap[tui.AltZ+',']) + check(actAccept, keymap[tui.AltZ+':']) + check(actPageDown, keymap[tui.AltZ]) + check(actExecute, keymap[tui.F1]) + check(actExecute, keymap[tui.F2]) + check(actExecute, keymap[tui.F3]) + check(actExecute, keymap[tui.F4]) + checkString("ls {}", execmap[tui.F1]) + checkString("echo {}, {}, {}", execmap[tui.F2]) + checkString("echo '({})'", execmap[tui.F3]) + checkString("less {}", execmap[tui.F4]) + checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) + checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) + checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) - checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) + checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) } parseKeymap(keymap, execmap, "f1:abort") - check(actAbort, keymap[curses.F1]) + check(actAbort, keymap[tui.F1]) } func TestColorSpec(t *testing.T) { - theme := curses.Dark256 + theme := tui.Dark256 dark := parseTheme(theme, "dark") if *dark != *theme { t.Errorf("colors should be equivalent") @@ -283,7 +283,7 @@ func TestColorSpec(t *testing.T) { if *light == *theme { t.Errorf("should not be equivalent") } - if *light != *curses.Light256 { + if *light != *tui.Light256 { t.Errorf("colors should be equivalent") } if light == theme { @@ -294,23 +294,23 @@ func TestColorSpec(t *testing.T) { if customized.Fg != 231 || customized.Bg != 232 { t.Errorf("color not customized") } - if *curses.Dark256 == *customized { + if *tui.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: %v, %v", curses.Dark256, customized) + customized.Fg = tui.Dark256.Fg + customized.Bg = tui.Dark256.Bg + if *tui.Dark256 != *customized { + t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) } customized = parseTheme(theme, "fg:231,dark,bg:232") - if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { + if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { t.Errorf("color not customized") } } func TestParseNilTheme(t *testing.T) { - var theme *curses.ColorTheme + var theme *tui.ColorTheme newTheme := parseTheme(theme, "prompt:12") if newTheme != nil { t.Errorf("color is disabled. keep it that way.") @@ -330,21 +330,21 @@ func TestDefaultCtrlNP(t *testing.T) { t.Error() } } - check([]string{}, curses.CtrlN, actDown) - check([]string{}, curses.CtrlP, actUp) + check([]string{}, tui.CtrlN, actDown) + check([]string{}, tui.CtrlP, actUp) - check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) - check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) + check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) hist := "--history=/tmp/fzf-history" - check([]string{hist}, curses.CtrlN, actNextHistory) - check([]string{hist}, curses.CtrlP, actPreviousHistory) + check([]string{hist}, tui.CtrlN, actNextHistory) + check([]string{hist}, tui.CtrlP, actPreviousHistory) - check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) - check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) - check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) - check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) } func optsFor(words ...string) *Options { diff --git a/src/result.go b/src/result.go index 69e83f85..c0cf5d61 100644 --- a/src/result.go +++ b/src/result.go @@ -5,7 +5,7 @@ import ( "sort" "unicode" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -14,8 +14,8 @@ type Offset [2]int32 type colorOffset struct { offset [2]int32 - color int - attr curses.Attr + color tui.ColorPair + attr tui.Attr index int32 } @@ -92,7 +92,7 @@ func minRank() rank { return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { itemColors := result.item.Colors() // No ANSI code, or --color=no @@ -149,23 +149,23 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorThe fg := ansi.color.fg if fg == -1 { if current { - fg = int(theme.Current) + fg = theme.Current } else { - fg = int(theme.Fg) + fg = theme.Fg } } bg := ansi.color.bg if bg == -1 { if current { - bg = int(theme.DarkBg) + bg = theme.DarkBg } else { - bg = int(theme.Bg) + bg = theme.Bg } } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: curses.PairFor(fg, bg), - attr: ansi.color.attr | attr}) + color: tui.PairFor(fg, bg), + attr: ansi.color.attr.Merge(attr)}) } } } diff --git a/src/result_test.go b/src/result_test.go index 645684dd..06402c1b 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -1,3 +1,5 @@ +// +build !termbox + package fzf import ( @@ -5,7 +7,7 @@ import ( "sort" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -98,26 +100,26 @@ func TestColorOffset(t *testing.T) { item: &Item{ colors: &[]ansiOffset{ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, - ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}}, + 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, curses.Bold}}}}} + 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}] - colors := item.colorOffsets(offsets, curses.Dark256, 99, 0, true) - assert := func(idx int, b int32, e int32, c int, bold bool) { - var attr curses.Attr + colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true) + assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { + var attr tui.Attr if bold { - attr = curses.Bold + attr = tui.Bold } o := colors[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { t.Error(o) } } - assert(0, 0, 5, curses.ColUser, false) + assert(0, 0, 5, tui.ColUser, false) assert(1, 5, 15, 99, false) - assert(2, 15, 20, curses.ColUser, false) - assert(3, 22, 25, curses.ColUser+1, true) + assert(2, 15, 20, tui.ColUser, false) + assert(3, 22, 25, tui.ColUser+1, true) assert(4, 25, 35, 99, false) - assert(5, 35, 40, curses.ColUser+2, true) + assert(5, 35, 40, tui.ColUser+2, true) } diff --git a/src/terminal.go b/src/terminal.go index 376c6408..39c78239 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -12,7 +12,7 @@ import ( "syscall" "time" - C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" "github.com/junegunn/go-runewidth" @@ -69,9 +69,9 @@ type Terminal struct { header0 []string ansi bool margin [4]sizeSpec - window *C.Window - bwindow *C.Window - pwindow *C.Window + window *tui.Window + bwindow *tui.Window + pwindow *tui.Window count int progress int reading bool @@ -90,7 +90,7 @@ type Terminal struct { suppress bool startChan chan bool slab *util.Slab - theme *C.ColorTheme + theme *tui.ColorTheme } type selectedItem struct { @@ -187,51 +187,51 @@ const ( func defaultKeymap() map[int]actionType { keymap := make(map[int]actionType) - keymap[C.Invalid] = actInvalid - keymap[C.CtrlA] = actBeginningOfLine - keymap[C.CtrlB] = actBackwardChar - keymap[C.CtrlC] = actAbort - keymap[C.CtrlG] = actAbort - keymap[C.CtrlQ] = actAbort - keymap[C.ESC] = actAbort - keymap[C.CtrlD] = actDeleteCharEOF - keymap[C.CtrlE] = actEndOfLine - keymap[C.CtrlF] = actForwardChar - keymap[C.CtrlH] = actBackwardDeleteChar - keymap[C.BSpace] = actBackwardDeleteChar - keymap[C.Tab] = actToggleDown - keymap[C.BTab] = actToggleUp - keymap[C.CtrlJ] = actDown - keymap[C.CtrlK] = actUp - keymap[C.CtrlL] = actClearScreen - keymap[C.CtrlM] = actAccept - keymap[C.CtrlN] = actDown - keymap[C.CtrlP] = actUp - keymap[C.CtrlU] = actUnixLineDiscard - keymap[C.CtrlW] = actUnixWordRubout - keymap[C.CtrlY] = actYank - - keymap[C.AltB] = actBackwardWord - keymap[C.SLeft] = actBackwardWord - keymap[C.AltF] = actForwardWord - keymap[C.SRight] = actForwardWord - keymap[C.AltD] = actKillWord - keymap[C.AltBS] = actBackwardKillWord - - keymap[C.Up] = actUp - keymap[C.Down] = actDown - keymap[C.Left] = actBackwardChar - keymap[C.Right] = actForwardChar - - keymap[C.Home] = actBeginningOfLine - keymap[C.End] = actEndOfLine - keymap[C.Del] = actDeleteChar - keymap[C.PgUp] = actPageUp - keymap[C.PgDn] = actPageDown - - keymap[C.Rune] = actRune - keymap[C.Mouse] = actMouse - keymap[C.DoubleClick] = actAccept + keymap[tui.Invalid] = actInvalid + keymap[tui.CtrlA] = actBeginningOfLine + keymap[tui.CtrlB] = actBackwardChar + keymap[tui.CtrlC] = actAbort + keymap[tui.CtrlG] = actAbort + keymap[tui.CtrlQ] = actAbort + keymap[tui.ESC] = actAbort + keymap[tui.CtrlD] = actDeleteCharEOF + keymap[tui.CtrlE] = actEndOfLine + keymap[tui.CtrlF] = actForwardChar + keymap[tui.CtrlH] = actBackwardDeleteChar + keymap[tui.BSpace] = actBackwardDeleteChar + keymap[tui.Tab] = actToggleDown + keymap[tui.BTab] = actToggleUp + keymap[tui.CtrlJ] = actDown + keymap[tui.CtrlK] = actUp + keymap[tui.CtrlL] = actClearScreen + keymap[tui.CtrlM] = actAccept + keymap[tui.CtrlN] = actDown + keymap[tui.CtrlP] = actUp + keymap[tui.CtrlU] = actUnixLineDiscard + keymap[tui.CtrlW] = actUnixWordRubout + keymap[tui.CtrlY] = actYank + + keymap[tui.AltB] = actBackwardWord + keymap[tui.SLeft] = actBackwardWord + keymap[tui.AltF] = actForwardWord + keymap[tui.SRight] = actForwardWord + keymap[tui.AltD] = actKillWord + keymap[tui.AltBS] = actBackwardKillWord + + keymap[tui.Up] = actUp + keymap[tui.Down] = actDown + keymap[tui.Left] = actBackwardChar + keymap[tui.Right] = actForwardChar + + keymap[tui.Home] = actBeginningOfLine + keymap[tui.End] = actEndOfLine + keymap[tui.Del] = actDeleteChar + keymap[tui.PgUp] = actPageUp + keymap[tui.PgDn] = actPageDown + + keymap[tui.Rune] = actRune + keymap[tui.Mouse] = actMouse + keymap[tui.DoubleClick] = actAccept return keymap } @@ -299,7 +299,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { theme: opts.Theme, startChan: make(chan bool, 1), initFunc: func() { - C.Init(opts.Theme, opts.Black, opts.Mouse) + tui.Init(opts.Theme, opts.Black, opts.Mouse) }} } @@ -429,8 +429,8 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int { } func (t *Terminal) resizeWindows() { - screenWidth := C.MaxX() - screenHeight := C.MaxY() + screenWidth := tui.MaxX() + screenHeight := tui.MaxY() marginInt := [4]int{} for idx, sizeSpec := range t.margin { if sizeSpec.percent { @@ -479,33 +479,33 @@ func (t *Terminal) resizeWindows() { height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = C.NewWindow(y, x, w, h, true) - t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false) + t.bwindow = tui.NewWindow(y, x, w, h, true) + t.pwindow = tui.NewWindow(y+1, x+2, w-4, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width, @@ -531,24 +531,24 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - t.window.CPrint(C.ColPrompt, C.Bold, t.prompt) - t.window.CPrint(C.ColNormal, C.Bold, string(t.input)) + t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt) + t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input)) } func (t *Terminal) printInfo() { if t.inlineInfo { t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { - t.window.CPrint(C.ColSpinner, C.Bold, " < ") + t.window.CPrint(tui.ColSpinner, tui.Bold, " < ") } else { - t.window.CPrint(C.ColPrompt, C.Bold, " < ") + t.window.CPrint(tui.ColPrompt, tui.Bold, " < ") } } else { t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - t.window.CPrint(C.ColSpinner, C.Bold, _spinner[idx]) + t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx]) } t.move(1, 2, false) } @@ -567,7 +567,7 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - t.window.CPrint(C.ColInfo, 0, output) + t.window.CPrint(tui.ColInfo, 0, output) } func (t *Terminal) printHeader() { @@ -591,7 +591,8 @@ func (t *Terminal) printHeader() { colors: colors} t.move(line, 2, true) - t.printHighlighted(&Result{item: item}, 0, C.ColHeader, 0, false) + t.printHighlighted(&Result{item: item}, + tui.AttrRegular, tui.ColHeader, tui.ColDefault, false) } } @@ -625,21 +626,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } - t.window.CPrint(C.ColCursor, C.Bold, label) + t.window.CPrint(tui.ColCursor, tui.Bold, label) if current { if selected { - t.window.CPrint(C.ColSelected, C.Bold, ">") + t.window.CPrint(tui.ColSelected, tui.Bold, ">") } else { - t.window.CPrint(C.ColCurrent, C.Bold, " ") + t.window.CPrint(tui.ColCurrent, tui.Bold, " ") } - t.printHighlighted(result, C.Bold, C.ColCurrent, C.ColCurrentMatch, true) + t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true) } else { if selected { - t.window.CPrint(C.ColSelected, C.Bold, ">") + t.window.CPrint(tui.ColSelected, tui.Bold, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, C.ColNormal, C.ColMatch, false) + t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false) } } @@ -695,7 +696,7 @@ func overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 int, current bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) { item := result.item // Overflow @@ -827,7 +828,7 @@ func (t *Terminal) printPreview() { if t.previewer.offset > 0 { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) t.pwindow.Move(0, t.pwindow.Width-len(offset)) - t.pwindow.CPrint(C.ColInfo, C.Reverse, offset) + t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } @@ -858,11 +859,10 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { if t.isPreviewEnabled() { - t.bwindow.Refresh() - t.pwindow.Refresh() + tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window}) + } else { + tui.RefreshWindows([]*tui.Window{t.window}) } - t.window.Refresh() - C.DoUpdate() } } @@ -912,10 +912,10 @@ func (t *Terminal) rubout(pattern string) { t.input = append(t.input[:t.cx], after...) } -func keyMatch(key int, event C.Event) bool { +func keyMatch(key int, event tui.Event) bool { return event.Type == key || - event.Type == C.Rune && int(event.Char) == key-C.AltZ || - event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double + event.Type == tui.Rune && int(event.Char) == key-tui.AltZ || + event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double } func quoteEntry(entry string) string { @@ -980,7 +980,7 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - C.Endwin() + tui.Pause() cmd.Run() t.refresh() } @@ -1014,7 +1014,7 @@ func (t *Terminal) Loop() { }() resizeChan := make(chan os.Signal, 1) - signal.Notify(resizeChan, syscall.SIGWINCH) + notifyOnResize(resizeChan) // Non-portable go func() { for { <-resizeChan @@ -1126,12 +1126,11 @@ func (t *Terminal) Loop() { case reqRefresh: t.suppress = false case reqRedraw: - C.Clear() - C.Endwin() - C.Refresh() + tui.Clear() + tui.Refresh() t.printAll() case reqClose: - C.Close() + tui.Close() if t.output() { exit(exitOk) } @@ -1144,11 +1143,11 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - C.Close() + tui.Close() t.printer(string(t.input)) exit(exitOk) case reqQuit: - C.Close() + tui.Close() exit(exitInterrupt) } } @@ -1161,7 +1160,7 @@ func (t *Terminal) Loop() { looping := true for looping { - event := C.GetChar() + event := tui.GetChar() t.mutex.Lock() previousInput := t.input @@ -1445,7 +1444,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - return doAction(t.keymap[C.DoubleClick], C.DoubleClick) + return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) } } } else if me.Down { @@ -1468,8 +1467,8 @@ func (t *Terminal) Loop() { mapkey := event.Type if t.jumping == jumpDisabled { action := t.keymap[mapkey] - if mapkey == C.Rune { - mapkey = int(event.Char) + int(C.AltZ) + if mapkey == tui.Rune { + mapkey = int(event.Char) + int(tui.AltZ) if act, prs := t.keymap[mapkey]; prs { action = act } @@ -1484,7 +1483,7 @@ func (t *Terminal) Loop() { } changed = string(previousInput) != string(t.input) } else { - if mapkey == C.Rune { + if mapkey == tui.Rune { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { t.cy = idx + t.offset if t.jumping == jumpAcceptEnabled { diff --git a/src/terminal_unix.go b/src/terminal_unix.go new file mode 100644 index 00000000..6284c22d --- /dev/null +++ b/src/terminal_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package fzf + +import ( + "os" + "os/signal" + "syscall" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + signal.Notify(resizeChan, syscall.SIGWINCH) +} diff --git a/src/terminal_windows.go b/src/terminal_windows.go new file mode 100644 index 00000000..5512bbaf --- /dev/null +++ b/src/terminal_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package fzf + +import ( + "os" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + // TODO +} diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go new file mode 100644 index 00000000..7a443407 --- /dev/null +++ b/src/tui/ncurses.go @@ -0,0 +1,513 @@ +// +build !windows +// +build !termbox + +package tui + +/* +#include +#include +#cgo !static LDFLAGS: -lncurses +#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl +#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch + +SCREEN *c_newterm () { + return newterm(NULL, stderr, stdin); +} +*/ +import "C" + +import ( + "fmt" + "os" + "strings" + "syscall" + "time" + "unicode/utf8" +) + +type ColorPair int16 +type Attr C.int +type WindowImpl C.WINDOW + +const ( + Bold = C.A_BOLD + Dim = C.A_DIM + Blink = C.A_BLINK + Reverse = C.A_REVERSE + Underline = C.A_UNDERLINE +) + +const ( + AttrRegular Attr = 0 +) + +// Pallete +const ( + ColDefault ColorPair = iota + ColNormal + ColPrompt + ColMatch + ColCurrent + ColCurrentMatch + ColSpinner + ColInfo + ColCursor + ColSelected + ColHeader + ColBorder + ColUser // Should be the last entry +) + +var ( + _in *os.File + _screen *C.SCREEN + _colorMap map[int]ColorPair + _colorFn func(ColorPair, Attr) C.int +) + +func init() { + _colorMap = make(map[int]ColorPair) +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +func DefaultTheme() *ColorTheme { + if C.tigetnum(C.CString("colors")) >= 256 { + return Dark256 + } + return Default16 +} + +func Init(theme *ColorTheme, black bool, mouse bool) { + { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + _in = in + // Break STDIN + // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) + } + + C.setlocale(C.LC_ALL, C.CString("")) + _screen = C.c_newterm() + if _screen == nil { + fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) + os.Exit(2) + } + C.set_term(_screen) + if mouse { + C.mousemask(C.ALL_MOUSE_EVENTS, nil) + } + C.noecho() + C.raw() // stty dsusp undef + + _color = theme != nil + if _color { + C.start_color() + InitTheme(theme, black) + initPairs(theme) + C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + _colorFn = attrColored + } else { + _colorFn = attrMono + } +} + +func initPairs(theme *ColorTheme) { + C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) + initPair := func(group ColorPair, fg Color, bg Color) { + C.init_pair(C.short(group), C.short(fg), C.short(bg)) + } + initPair(ColNormal, theme.Fg, theme.Bg) + initPair(ColPrompt, theme.Prompt, theme.Bg) + initPair(ColMatch, theme.Match, theme.Bg) + initPair(ColCurrent, theme.Current, theme.DarkBg) + initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) + initPair(ColSpinner, theme.Spinner, theme.Bg) + initPair(ColInfo, theme.Info, theme.Bg) + initPair(ColCursor, theme.Cursor, theme.DarkBg) + initPair(ColSelected, theme.Selected, theme.DarkBg) + initPair(ColHeader, theme.Header, theme.Bg) + initPair(ColBorder, theme.Border, theme.Bg) +} + +func Pause() { + C.endwin() +} + +func Close() { + C.endwin() + C.delscreen(_screen) +} + +func NewWindow(top int, left int, width int, height int, border bool) *Window { + win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) + if _color { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + } + if border { + attr := _colorFn(ColBorder, 0) + C.wattron(win, attr) + C.box(win, 0, 0) + C.wattroff(win, attr) + } + + return &Window{ + impl: (*WindowImpl)(win), + Top: top, + Left: left, + Width: width, + Height: height, + } +} + +func attrColored(pair ColorPair, a Attr) C.int { + var attr C.int + if pair > 0 { + attr = C.COLOR_PAIR(C.int(pair)) + } + return attr | C.int(a) +} + +func attrMono(pair ColorPair, a Attr) C.int { + var attr C.int + switch pair { + case ColCurrent: + if C.int(a)&C.A_BOLD == C.A_BOLD { + attr = C.A_REVERSE + } + case ColMatch: + attr = C.A_UNDERLINE + case ColCurrentMatch: + attr = C.A_UNDERLINE | C.A_REVERSE + } + if C.int(a)&C.A_BOLD == C.A_BOLD { + attr = attr | C.A_BOLD + } + return attr +} + +func MaxX() int { + return int(C.COLS) +} + +func MaxY() int { + return int(C.LINES) +} + +func (w *Window) win() *C.WINDOW { + return (*C.WINDOW)(w.impl) +} + +func (w *Window) Close() { + C.delwin(w.win()) +} + +func (w *Window) Enclose(y int, x int) bool { + return bool(C.wenclose(w.win(), C.int(y), C.int(x))) +} + +func (w *Window) Move(y int, x int) { + C.wmove(w.win(), C.int(y), C.int(x)) +} + +func (w *Window) MoveAndClear(y int, x int) { + w.Move(y, x) + C.wclrtoeol(w.win()) +} + +func (w *Window) Print(text string) { + C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { + if r < 32 { + return -1 + } + return r + }, text))) +} + +func (w *Window) CPrint(pair ColorPair, a Attr, text string) { + attr := _colorFn(pair, a) + C.wattron(w.win(), attr) + w.Print(text) + C.wattroff(w.win(), attr) +} + +func Clear() { + C.clear() + C.endwin() +} + +func Refresh() { + C.refresh() +} + +func (w *Window) Erase() { + C.werase(w.win()) +} + +func (w *Window) Fill(str string) bool { + return C.waddstr(w.win(), C.CString(str)) == C.OK +} + +func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { + attr := _colorFn(PairFor(fg, bg), a) + C.wattron(w.win(), attr) + ret := w.Fill(str) + C.wattroff(w.win(), attr) + return ret +} + +func RefreshWindows(windows []*Window) { + for _, w := range windows { + C.wnoutrefresh(w.win()) + } + C.doupdate() +} + +func PairFor(fg Color, bg Color) ColorPair { + key := (int(fg) << 8) + int(bg) + if found, prs := _colorMap[key]; prs { + return found + } + + id := ColorPair(len(_colorMap) + int(ColUser)) + C.init_pair(C.short(id), C.short(fg), C.short(bg)) + _colorMap[key] = id + return id +} + +func getch(nonblock bool) int { + b := make([]byte, 1) + syscall.SetNonblock(int(_in.Fd()), nonblock) + _, err := _in.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func GetBytes() []byte { + c := getch(false) + _buf = append(_buf, byte(c)) + + for { + c = getch(true) + if c == -1 { + break + } + _buf = append(_buf, byte(c)) + } + + return _buf +} + +// 27 (91 79) 77 type x y +func mouseSequence(sz *int) Event { + if len(_buf) < 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch _buf[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := _buf[3] >= 36 + down := _buf[3]%2 == 0 + x := int(_buf[4] - 33) + y := int(_buf[5] - 33) + double := false + if down { + now := time.Now() + if now.Sub(_prevDownTime) < doubleClickDuration { + _clickY = append(_clickY, y) + } else { + _clickY = []int{y} + } + _prevDownTime = now + } else { + if len(_clickY) > 1 && _clickY[0] == _clickY[1] && + time.Now().Sub(_prevDownTime) < doubleClickDuration { + double = true + } + } + return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} + case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl + 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl + mod := _buf[3] >= 100 + s := 1 - int(_buf[3]%2)*2 + x := int(_buf[4] - 33) + y := int(_buf[5] - 33) + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} + } + return Event{Invalid, 0, nil} +} + +func escSequence(sz *int) Event { + if len(_buf) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch _buf[1] { + case 13: + return Event{AltEnter, 0, nil} + case 32: + return Event{AltSpace, 0, nil} + case 47: + return Event{AltSlash, 0, nil} + case 98: + return Event{AltB, 0, nil} + case 100: + return Event{AltD, 0, nil} + case 102: + return Event{AltF, 0, nil} + case 127: + return Event{AltBS, 0, nil} + case 91, 79: + if len(_buf) < 3 { + return Event{Invalid, 0, nil} + } + *sz = 3 + switch _buf[2] { + case 68: + return Event{Left, 0, nil} + case 67: + return Event{Right, 0, nil} + case 66: + return Event{Down, 0, nil} + case 65: + return Event{Up, 0, nil} + case 90: + return Event{BTab, 0, nil} + case 72: + return Event{Home, 0, nil} + case 70: + return Event{End, 0, nil} + case 77: + return mouseSequence(sz) + case 80: + return Event{F1, 0, nil} + case 81: + return Event{F2, 0, nil} + case 82: + return Event{F3, 0, nil} + case 83: + return Event{F4, 0, nil} + case 49, 50, 51, 52, 53, 54: + if len(_buf) < 4 { + return Event{Invalid, 0, nil} + } + *sz = 4 + switch _buf[2] { + case 50: + if len(_buf) == 5 && _buf[4] == 126 { + *sz = 5 + switch _buf[3] { + case 48: + return Event{F9, 0, nil} + case 49: + return Event{F10, 0, nil} + } + } + // Bracketed paste mode \e[200~ / \e[201 + if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { + *sz = 6 + return Event{Invalid, 0, nil} + } + return Event{Invalid, 0, nil} // INS + case 51: + return Event{Del, 0, nil} + case 52: + return Event{End, 0, nil} + case 53: + return Event{PgUp, 0, nil} + case 54: + return Event{PgDn, 0, nil} + case 49: + switch _buf[3] { + case 126: + return Event{Home, 0, nil} + case 53, 55, 56, 57: + if len(_buf) == 5 && _buf[4] == 126 { + *sz = 5 + switch _buf[3] { + case 53: + return Event{F5, 0, nil} + case 55: + return Event{F6, 0, nil} + case 56: + return Event{F7, 0, nil} + case 57: + return Event{F8, 0, nil} + } + } + return Event{Invalid, 0, nil} + case 59: + if len(_buf) != 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch _buf[4] { + case 50: + switch _buf[5] { + case 68: + return Event{Home, 0, nil} + case 67: + return Event{End, 0, nil} + } + case 53: + switch _buf[5] { + case 68: + return Event{SLeft, 0, nil} + case 67: + return Event{SRight, 0, nil} + } + } // _buf[4] + } // _buf[3] + } // _buf[2] + } // _buf[2] + } // _buf[1] + if _buf[1] >= 'a' && _buf[1] <= 'z' { + return Event{AltA + int(_buf[1]) - 'a', 0, nil} + } + return Event{Invalid, 0, nil} +} + +func GetChar() Event { + if len(_buf) == 0 { + _buf = GetBytes() + } + if len(_buf) == 0 { + panic("Empty _buffer") + } + + sz := 1 + defer func() { + _buf = _buf[sz:] + }() + + switch _buf[0] { + case CtrlC: + return Event{CtrlC, 0, nil} + case CtrlG: + return Event{CtrlG, 0, nil} + case CtrlQ: + return Event{CtrlQ, 0, nil} + case 127: + return Event{BSpace, 0, nil} + case ESC: + return escSequence(&sz) + } + + // CTRL-A ~ CTRL-Z + if _buf[0] <= CtrlZ { + return Event{int(_buf[0]), 0, nil} + } + r, rsz := utf8.DecodeRune(_buf) + if r == utf8.RuneError { + return Event{ESC, 0, nil} + } + sz = rsz + return Event{Rune, r, nil} +} diff --git a/src/tui/termbox.go b/src/tui/termbox.go new file mode 100644 index 00000000..c49512ce --- /dev/null +++ b/src/tui/termbox.go @@ -0,0 +1,151 @@ +// +build termbox windows + +package tui + +import ( + "github.com/nsf/termbox-go" +) + +type ColorPair [2]Color +type Attr uint16 +type WindowImpl int // FIXME + +const ( + // TODO + _ = iota + Bold + Dim + Blink + Reverse + Underline +) + +const ( + AttrRegular Attr = 0 +) + +var ( + ColDefault = ColorPair{colDefault, colDefault} + ColNormal ColorPair + ColPrompt ColorPair + ColMatch ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColCursor ColorPair + ColSelected ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColUser ColorPair +) + +func DefaultTheme() *ColorTheme { + if termbox.SetOutputMode(termbox.OutputCurrent) == termbox.Output256 { + return Dar