m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-18 08:13:40 -05:00

Prepare for termbox/windows build

`TAGS=termbox make` (or `go build -tags termbox`)
This commit is contained in:
Junegunn Choi
2016-10-24 09:44:56 +09:00
parent 2cff00dce2
commit 0c573b3dff
21 changed files with 924 additions and 659 deletions

513
src/tui/ncurses.go Normal file
View File

@@ -0,0 +1,513 @@
// +build !windows
// +build !termbox
package tui
/*
#include <ncurses.h>
#include <locale.h>
#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}
}

151
src/tui/termbox.go Normal file
View File

@@ -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 Dark256
}
return Default16
}
func PairFor(fg Color, bg Color) ColorPair {
return [2]Color{fg, bg}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
func Init(theme *ColorTheme, black bool, mouse bool) {
ColNormal = ColorPair{theme.Fg, theme.Bg}
ColPrompt = ColorPair{theme.Prompt, theme.Bg}
ColMatch = ColorPair{theme.Match, theme.Bg}
ColCurrent = ColorPair{theme.Current, theme.DarkBg}
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg}
ColSpinner = ColorPair{theme.Spinner, theme.Bg}
ColInfo = ColorPair{theme.Info, theme.Bg}
ColCursor = ColorPair{theme.Cursor, theme.DarkBg}
ColSelected = ColorPair{theme.Selected, theme.DarkBg}
ColHeader = ColorPair{theme.Header, theme.Bg}
ColBorder = ColorPair{theme.Border, theme.Bg}
// TODO
}
func MaxX() int {
// TODO
return 80
}
func MaxY() int {
// TODO
return 24
}
func Clear() {
// TODO
}
func Refresh() {
// TODO
}
func GetChar() Event {
// TODO
return Event{}
}
func Pause() {
// TODO
}
func Close() {
// TODO
}
func RefreshWindows(windows []*Window) {
// TODO
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
// TODO
return &Window{}
}
func (w *Window) Close() {
// TODO
}
func (w *Window) Erase() {
// TODO
}
func (w *Window) Enclose(y int, x int) bool {
// TODO
return false
}
func (w *Window) Move(y int, x int) {
// TODO
}
func (w *Window) MoveAndClear(y int, x int) {
// TODO
}
func (w *Window) Print(text string) {
// TODO
}
func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
// TODO
}
func (w *Window) Fill(str string) bool {
// TODO
return false
}
func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool {
// TODO
return false
}

250
src/tui/tui.go Normal file
View File

@@ -0,0 +1,250 @@
package tui
import (
"time"
)
// 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'
)
const (
doubleClickDuration = 500 * time.Millisecond
)
type Color int16
const (
colUndefined Color = -2
colDefault = -1
)
const (
colBlack Color = iota
colRed
colGreen
colYellow
colBlue
colMagenta
colCyan
colWhite
)
type ColorTheme struct {
Fg Color
Bg Color
DarkBg Color
Prompt Color
Match Color
Current Color
CurrentMatch Color
Spinner Color
Info Color
Cursor Color
Selected Color
Header Color
Border Color
}
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
_color bool
_prevDownTime time.Time
_clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
)
type Window struct {
impl *WindowImpl
Top int
Left int
Width int
Height int
}
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{}
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: colBlack,
Prompt: colBlue,
Match: colGreen,
Current: colYellow,
CurrentMatch: colGreen,
Spinner: colGreen,
Info: colWhite,
Cursor: colRed,
Selected: colMagenta,
Header: colCyan,
Border: colBlack}
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 InitTheme(theme *ColorTheme, black bool) {
_color = theme != nil
if !_color {
return
}
baseTheme := DefaultTheme()
if black {
theme.Bg = colBlack
}
o := func(a Color, b Color) Color {
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)
}

14
src/tui/tui_test.go Normal file
View File

@@ -0,0 +1,14 @@
package tui
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()
}
}