mirror of
https://github.com/junegunn/fzf
synced 2026-07-31 18:21:42 +00:00
86bc9d506f
Interrupt handling during execute action was not serialized and often caused crash, failed to restore the terminal state.
549 lines
9.3 KiB
Go
549 lines
9.3 KiB
Go
package curses
|
|
|
|
/*
|
|
#include <ncurses.h>
|
|
#include <locale.h>
|
|
#cgo !static LDFLAGS: -lncurses
|
|
#cgo static LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// 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
|
|
|
|
BTab
|
|
BSpace
|
|
|
|
Del
|
|
PgUp
|
|
PgDn
|
|
|
|
Up
|
|
Down
|
|
Left
|
|
Right
|
|
Home
|
|
End
|
|
|
|
SLeft
|
|
SRight
|
|
|
|
F1
|
|
F2
|
|
F3
|
|
F4
|
|
|
|
AltBS
|
|
AltA
|
|
AltB
|
|
AltC
|
|
AltD
|
|
AltE
|
|
AltF
|
|
|
|
AltZ = AltA + 'z' - 'a'
|
|
)
|
|
|
|
// Pallete
|
|
const (
|
|
ColNormal = iota
|
|
ColPrompt
|
|
ColMatch
|
|
ColCurrent
|
|
ColCurrentMatch
|
|
ColSpinner
|
|
ColInfo
|
|
ColCursor
|
|
ColSelected
|
|
ColHeader
|
|
ColUser
|
|
)
|
|
|
|
const (
|
|
doubleClickDuration = 500 * time.Millisecond
|
|
)
|
|
|
|
type ColorTheme struct {
|
|
UseDefault bool
|
|
Fg int16
|
|
Bg int16
|
|
DarkBg int16
|
|
Prompt int16
|
|
Match int16
|
|
Current int16
|
|
CurrentMatch int16
|
|
Spinner int16
|
|
Info int16
|
|
Cursor int16
|
|
Selected int16
|
|
Header 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 func(int, bool) C.int
|
|
_colorMap map[int]int
|
|
_prevDownTime time.Time
|
|
_clickY []int
|
|
_screen *C.SCREEN
|
|
Default16 *ColorTheme
|
|
Dark256 *ColorTheme
|
|
Light256 *ColorTheme
|
|
FG int
|
|
CurrentFG int
|
|
BG int
|
|
DarkBG int
|
|
)
|
|
|
|
func init() {
|
|
_prevDownTime = time.Unix(0, 0)
|
|
_clickY = []int{}
|
|
_colorMap = make(map[int]int)
|
|
Default16 = &ColorTheme{
|
|
UseDefault: true,
|
|
Fg: 15,
|
|
Bg: 0,
|
|
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}
|
|
Dark256 = &ColorTheme{
|
|
UseDefault: true,
|
|
Fg: 15,
|
|
Bg: 0,
|
|
DarkBg: 236,
|
|
Prompt: 110,
|
|
Match: 108,
|
|
Current: 254,
|
|
CurrentMatch: 151,
|
|
Spinner: 148,
|
|
Info: 144,
|
|
Cursor: 161,
|
|
Selected: 168,
|
|
Header: 109}
|
|
Light256 = &ColorTheme{
|
|
UseDefault: true,
|
|
Fg: 15,
|
|
Bg: 0,
|
|
DarkBg: 251,
|
|
Prompt: 25,
|
|
Match: 66,
|
|
Current: 237,
|
|
CurrentMatch: 23,
|
|
Spinner: 65,
|
|
Info: 101,
|
|
Cursor: 161,
|
|
Selected: 168,
|
|
Header: 31}
|
|
}
|
|
|
|
func attrColored(pair int, bold bool) C.int {
|
|
var attr C.int
|
|
if pair > ColNormal {
|
|
attr = C.COLOR_PAIR(C.int(pair))
|
|
}
|
|
if bold {
|
|
attr = attr | C.A_BOLD
|
|
}
|
|
return attr
|
|
}
|
|
|
|
func attrMono(pair int, bold bool) C.int {
|
|
var attr C.int
|
|
switch pair {
|
|
case ColCurrent:
|
|
if bold {
|
|
attr = C.A_REVERSE
|
|
}
|
|
case ColMatch:
|
|
attr = C.A_UNDERLINE
|
|
case ColCurrentMatch:
|
|
attr = C.A_UNDERLINE | C.A_REVERSE
|
|
}
|
|
if 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.newterm(nil, C.stderr, C.stdin)
|
|
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
|
|
|
|
if theme != nil {
|
|
C.start_color()
|
|
initPairs(theme, black)
|
|
_color = attrColored
|
|
} else {
|
|
_color = attrMono
|
|
}
|
|
}
|
|
|
|
func initPairs(theme *ColorTheme, black bool) {
|
|
fg := C.short(theme.Fg)
|
|
bg := C.short(theme.Bg)
|
|
if black {
|
|
bg = C.COLOR_BLACK
|
|
} else if theme.UseDefault {
|
|
fg = -1
|
|
bg = -1
|
|
C.use_default_colors()
|
|
}
|
|
if theme.UseDefault {
|
|
FG = -1
|
|
BG = -1
|
|
} else {
|
|
FG = int(fg)
|
|
BG = int(bg)
|
|
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
|
|
}
|
|
|
|
CurrentFG = int(theme.Current)
|
|
DarkBG = int(theme.DarkBg)
|
|
darkBG := C.short(DarkBG)
|
|
C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
|
|
C.init_pair(ColMatch, C.short(theme.Match), bg)
|
|
C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
|
|
C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
|
|
C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
|
|
C.init_pair(ColInfo, C.short(theme.Info), bg)
|
|
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
|
|
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
|
|
C.init_pair(ColHeader, C.short(theme.Header), 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
|
|
return Event{Mouse, 0, &MouseEvent{0, 0, 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 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:
|
|
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 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 Move(y int, x int) {
|
|
C.move(C.int(y), C.int(x))
|
|
}
|
|
|
|
func MoveAndClear(y int, x int) {
|
|
Move(y, x)
|
|
C.clrtoeol()
|
|
}
|
|
|
|
func Print(text string) {
|
|
C.addstr(C.CString(text))
|
|
}
|
|
|
|
func CPrint(pair int, bold bool, text string) {
|
|
attr := _color(pair, bold)
|
|
C.attron(attr)
|
|
Print(text)
|
|
C.attroff(attr)
|
|
}
|
|
|
|
func Clear() {
|
|
C.clear()
|
|
}
|
|
|
|
func Endwin() {
|
|
C.endwin()
|
|
}
|
|
|
|
func Refresh() {
|
|
C.refresh()
|
|
}
|
|
|
|
func PairFor(fg int, bg int) int {
|
|
key := (fg << 8) + bg
|
|
if found, prs := _colorMap[key]; prs {
|
|
return found
|
|
}
|
|
|
|
id := len(_colorMap) + ColUser
|
|
C.init_pair(C.short(id), C.short(fg), C.short(bg))
|
|
_colorMap[key] = id
|
|
return id
|
|
}
|