summaryrefslogtreecommitdiffstats
path: root/src/tui
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui')
-rw-r--r--src/tui/dummy.go4
-rw-r--r--src/tui/eventtype_string.go122
-rw-r--r--src/tui/light.go151
-rw-r--r--src/tui/light_unix.go32
-rw-r--r--src/tui/light_windows.go10
-rw-r--r--src/tui/tcell.go62
-rw-r--r--src/tui/tcell_test.go53
-rw-r--r--src/tui/ttyname_unix.go32
-rw-r--r--src/tui/ttyname_windows.go13
-rw-r--r--src/tui/tui.go183
10 files changed, 454 insertions, 208 deletions
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index 7760a724..1a761460 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false
}
-var DefaultBorderShape BorderShape = BorderRounded
+var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
@@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7)
)
-func (r *FullscreenRenderer) Init() {}
+func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}
diff --git a/src/tui/eventtype_string.go b/src/tui/eventtype_string.go
new file mode 100644
index 00000000..a62ba073
--- /dev/null
+++ b/src/tui/eventtype_string.go
@@ -0,0 +1,122 @@
+// Code generated by "stringer -type=EventType"; DO NOT EDIT.
+
+package tui
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[Rune-0]
+ _ = x[CtrlA-1]
+ _ = x[CtrlB-2]
+ _ = x[CtrlC-3]
+ _ = x[CtrlD-4]
+ _ = x[CtrlE-5]
+ _ = x[CtrlF-6]
+ _ = x[CtrlG-7]
+ _ = x[CtrlH-8]
+ _ = x[Tab-9]
+ _ = x[CtrlJ-10]
+ _ = x[CtrlK-11]
+ _ = x[CtrlL-12]
+ _ = x[CtrlM-13]
+ _ = x[CtrlN-14]
+ _ = x[CtrlO-15]
+ _ = x[CtrlP-16]
+ _ = x[CtrlQ-17]
+ _ = x[CtrlR-18]
+ _ = x[CtrlS-19]
+ _ = x[CtrlT-20]
+ _ = x[CtrlU-21]
+ _ = x[CtrlV-22]
+ _ = x[CtrlW-23]
+ _ = x[CtrlX-24]
+ _ = x[CtrlY-25]
+ _ = x[CtrlZ-26]
+ _ = x[Esc-27]
+ _ = x[CtrlSpace-28]
+ _ = x[CtrlDelete-29]
+ _ = x[CtrlBackSlash-30]
+ _ = x[CtrlRightBracket-31]
+ _ = x[CtrlCaret-32]
+ _ = x[CtrlSlash-33]
+ _ = x[ShiftTab-34]
+ _ = x[Backspace-35]
+ _ = x[Delete-36]
+ _ = x[PageUp-37]
+ _ = x[PageDown-38]
+ _ = x[Up-39]
+ _ = x[Down-40]
+ _ = x[Left-41]
+ _ = x[Right-42]
+ _ = x[Home-43]
+ _ = x[End-44]
+ _ = x[Insert-45]
+ _ = x[ShiftUp-46]
+ _ = x[ShiftDown-47]
+ _ = x[ShiftLeft-48]
+ _ = x[ShiftRight-49]
+ _ = x[ShiftDelete-50]
+ _ = x[F1-51]
+ _ = x[F2-52]
+ _ = x[F3-53]
+ _ = x[F4-54]
+ _ = x[F5-55]
+ _ = x[F6-56]
+ _ = x[F7-57]
+ _ = x[F8-58]
+ _ = x[F9-59]
+ _ = x[F10-60]
+ _ = x[F11-61]
+ _ = x[F12-62]
+ _ = x[AltBackspace-63]
+ _ = x[AltUp-64]
+ _ = x[AltDown-65]
+ _ = x[AltLeft-66]
+ _ = x[AltRight-67]
+ _ = x[AltShiftUp-68]
+ _ = x[AltShiftDown-69]
+ _ = x[AltShiftLeft-70]
+ _ = x[AltShiftRight-71]
+ _ = x[Alt-72]
+ _ = x[CtrlAlt-73]
+ _ = x[Invalid-74]
+ _ = x[Fatal-75]
+ _ = x[Mouse-76]
+ _ = x[DoubleClick-77]
+ _ = x[LeftClick-78]
+ _ = x[RightClick-79]
+ _ = x[SLeftClick-80]
+ _ = x[SRightClick-81]
+ _ = x[ScrollUp-82]
+ _ = x[ScrollDown-83]
+ _ = x[SScrollUp-84]
+ _ = x[SScrollDown-85]
+ _ = x[PreviewScrollUp-86]
+ _ = x[PreviewScrollDown-87]
+ _ = x[Resize-88]
+ _ = x[Change-89]
+ _ = x[BackwardEOF-90]
+ _ = x[Start-91]
+ _ = x[Load-92]
+ _ = x[Focus-93]
+ _ = x[One-94]
+ _ = x[Zero-95]
+ _ = x[Result-96]
+ _ = x[Jump-97]
+ _ = x[JumpCancel-98]
+ _ = x[ClickHeader-99]
+}
+
+const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
+
+var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
+
+func (i EventType) String() string {
+ if i < 0 || i >= EventType(len(_EventType_index)-1) {
+ return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
+}
diff --git a/src/tui/light.go b/src/tui/light.go
index 216c4c36..f202899a 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -2,6 +2,7 @@ package tui
import (
"bytes"
+ "errors"
"fmt"
"os"
"regexp"
@@ -10,6 +11,7 @@ import (
"time"
"unicode/utf8"
+ "github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
@@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty"
-var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
-var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
+var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
+var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -71,13 +73,14 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() {
if r.queued.Len() > 0 {
- fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
+ fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset()
}
}
// Light renderer
type LightRenderer struct {
+ closed *util.AtomicBool
theme *ColorTheme
mouse bool
forceBlack bool
@@ -85,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time
clicks [][2]int
ttyin *os.File
+ ttyout *os.File
buffer []byte
origState *term.State
width int
@@ -123,19 +127,25 @@ type LightWindow struct {
bg Color
}
-func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
+func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
+ out, err := openTtyOut()
+ if err != nil {
+ out = os.Stderr
+ }
r := LightRenderer{
+ closed: util.NewAtomicBool(false),
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
clearOnExit: clearOnExit,
- ttyin: openTtyIn(),
+ ttyin: ttyin,
+ ttyout: out,
yoffset: 0,
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
- return &r
+ return &r, nil
}
func repeat(r rune, times int) string {
@@ -153,11 +163,11 @@ func atoi(s string, defaultValue int) int {
return value
}
-func (r *LightRenderer) Init() {
+func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil {
- errorExit(err.Error())
+ return err
}
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +205,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
+ return nil
}
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,19 +244,20 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
-func (r *LightRenderer) getBytes() []byte {
- return r.getBytesInternal(r.buffer, false)
+func (r *LightRenderer) getBytes() ([]byte, error) {
+ bytes, err := r.getBytesInternal(r.buffer, false)
+ return bytes, err
}
-func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
+func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
- errorExit("Failed to read " + consoleDevice)
+ return nil, errors.New("failed to read " + consoleDevice)
}
retries := 0
- if c == ESC.Int() || nonblock {
+ if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
@@ -260,7 +272,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue
}
break
- } else if c == ESC.Int() && pc != c {
+ } else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval
} else {
retries = 0
@@ -272,19 +284,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
- panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
+ return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
- return buffer
+ return buffer, nil
}
func (r *LightRenderer) GetChar() Event {
+ var err error
if len(r.buffer) == 0 {
- r.buffer = r.getBytes()
+ r.buffer, err = r.getBytes()
+ if err != nil {
+ return Event{Fatal, 0, nil}
+ }
}
if len(r.buffer) == 0 {
- panic("Empty buffer")
+ return Event{Fatal, 0, nil}
}
sz := 1
@@ -300,7 +316,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte():
return Event{CtrlQ, 0, nil}
case 127:
- return Event{BSpace, 0, nil}
+ return Event{Backspace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case 28:
@@ -311,11 +327,13 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
- case ESC.Byte():
+ case Esc.Byte():
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
- r.buffer = r.getBytes()
+ if r.buffer, err = r.getBytes(); err != nil {
+ return Event{Fatal, 0, nil}
+ }
ev = r.escSequence(&sz)
}
return ev
@@ -327,7 +345,7 @@ func (r *LightRenderer) GetChar() Event {
}
char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError {
- return Event{ESC, 0, nil}
+ return Event{Esc, 0, nil}
}
sz = rsz
return Event{Rune, char, nil}
@@ -335,7 +353,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
- return Event{ESC, 0, nil}
+ return Event{Esc, 0, nil}
}
loc := offsetRegexpBegin.FindIndex(r.buffer)
@@ -349,15 +367,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
}
alt := false
- if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() {
+ if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
r.buffer = r.buffer[1:]
alt = true
}
switch r.buffer[1] {
- case ESC.Byte():
- return Event{ESC, 0, nil}
+ case Esc.Byte():
+ return Event{Esc, 0, nil}
case 127:
- return Event{AltBS, 0, nil}
+ return Event{AltBackspace, 0, nil}
case '[', 'O':
if len(r.buffer) < 3 {
return Event{Invalid, 0, nil}
@@ -386,7 +404,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
return Event{Up, 0, nil}
case 'Z':
- return Event{BTab, 0, nil}
+ return Event{ShiftTab, 0, nil}
case 'H':
return Event{Home, 0, nil}
case 'F':
@@ -434,7 +452,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} // INS
case '3':
if r.buffer[3] == '~' {
- return Event{Del, 0, nil}
+ return Event{Delete, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
@@ -442,16 +460,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
- return Event{SDelete, 0, nil}
+ return Event{ShiftDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4':
return Event{End, 0, nil}
case '5':
- return Event{PgUp, 0, nil}
+ return Event{PageUp, 0, nil}
case '6':
- return Event{PgDn, 0, nil}
+ return Event{PageDown, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
@@ -489,16 +507,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
}
*sz = 6
switch r.buffer[4] {
- case '1', '2', '3', '5':
+ case '1', '2', '3', '4', '5':
+ // Kitty iTerm2 WezTerm
+ // SHIFT-ARROW "\e[1;2D"
+ // ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
+ // CTRL-SHIFT-ARROW "\e[1;6D" N/A
+ // CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3'
- altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5]
- if altShift {
+ altShift := false
+ if r.buffer[4] == '1' && r.buffer[5] == '0' {
+ altShift = true
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
}
*sz = 7
char = r.buffer[6]
+ } else if r.buffer[4] == '4' {
+ altShift = true
+ if len(r.buffer) < 6 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 6
+ char = r.buffer[5]
}
switch char {
case 'A':
@@ -506,33 +537,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil}
}
if altShift {
- return Event{AltSUp, 0, nil}
+ return Event{AltShiftUp, 0, nil}
}
- return Event{SUp, 0, nil}
+ return Event{ShiftUp, 0, nil}
case 'B':
if alt {
return Event{AltDown, 0, nil}
}
if altShift {
- return Event{AltSDown, 0, nil}
+ return Event{AltShiftDown, 0, nil}
}
- return Event{SDown, 0, nil}
+ return Event{ShiftDown, 0, nil}
case 'C':
if alt {
return Event{AltRight, 0, nil}
}
if altShift {
- return Event{AltSRight, 0, nil}
+ return Event{AltShiftRight, 0, nil}
}
- return Event{SRight, 0, nil}
+ return Event{ShiftRight, 0, nil}
case 'D':
if alt {
return Event{AltLeft, 0, nil}
}
if altShift {
- return Event{AltSLeft, 0, nil}
+ return Event{AltShiftLeft, 0, nil}
}
- return Event{SLeft, 0, nil}
+ return Event{ShiftLeft, 0, nil}
}
} // r.buffer[4]
} // r.buffer[3]
@@ -725,6 +756,7 @@ func (r *LightRenderer) Close() {
r.flush()
r.closePlatform()
r.restoreTerminal()
+ r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
@@ -808,44 +840,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
- pad := repeat(' ', w.width/hw)
-
- w.Move(0, 0)
if top {
+ w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
- } else {
- w.CPrint(color, pad)
- }
-
- for y := 1; y < w.height-1; y++ {
- w.Move(y, 0)
- w.CPrint(color, pad)
}
- w.Move(w.height-1, 0)
if bottom {
+ w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
- } else {
- w.CPrint(color, pad)
}
}
func (w *LightWindow) drawBorderVertical(left, right bool) {
- width := w.width - 2
- if !left || !right {
- width++
- }
+ vw := runeWidth(w.border.left)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
- w.Move(y, 0)
if left {
+ w.Move(y, 0)
w.CPrint(color, string(w.border.left))
+ w.CPrint(color, " ") // Margin
}
- w.CPrint(color, repeat(' ', width))
if right {
+ w.Move(y, w.width-vw-1)
+ w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@@ -867,7 +887,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
- w.CPrint(color, repeat(' ', w.width-vw*2))
+ w.CPrint(color, " ") // Margin
+
+ w.Move(y, w.width-vw-1)
+ w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@@ -998,7 +1021,7 @@ func (w *LightWindow) Print(text string) {
}
func cleanse(str string) string {
- return strings.Replace(str, "\x1b", "", -1)
+ return strings.ReplaceAll(str, "\x1b", "")
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
index 46188869..06099d2f 100644
--- a/src/tui/light_unix.go
+++ b/src/tui/light_unix.go
@@ -3,7 +3,7 @@
package tui
import (
- "fmt"
+ "errors"
"os"
"os/exec"
"strings"
@@ -45,22 +45,29 @@ func (r *LightRenderer) initPlatform() error {
}
func (r *LightRenderer) closePlatform() {
- // NOOP
+ r.ttyout.Close()
}
-func openTtyIn() *os.File {
- in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
+func openTty(mode int) (*os.File, error) {
+ in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
- if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
- return in
+ if in, err := os.OpenFile(tty, mode, 0); err == nil {
+ return in, nil
}
}
- fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
- os.Exit(2)
+ return nil, errors.New("failed to open " + consoleDevice)
}
- return in
+ return in, nil
+}
+
+func openTtyIn() (*os.File, error) {
+ return openTty(syscall.O_RDONLY)
+}
+
+func openTtyOut() (*os.File, error) {
+ return openTty(syscall.O_WRONLY)
}
func (r *LightRenderer) setupTerminal() {
@@ -86,9 +93,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
+ var err error
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
- bytes = r.getBytesInternal(bytes, tries > 0)
+ bytes, err = r.getBytesInternal(bytes, tries > 0)
+ if err != nil {
+ return -1, -1
+ }
+
offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 {
// Add anything we skipped over to the input buffer
diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go
index 62b10c12..b7fc3402 100644
--- a/src/tui/light_windows.go
+++ b/src/tui/light_windows.go
@@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
- for {
+ for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@@ -91,9 +91,13 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
}
-func openTtyIn() *os.File {
+func openTtyIn() (*os.File, error) {
// not used
- return nil
+ return nil, nil
+}
+
+func openTtyOut() (*os.File, error) {
+ return os.Stderr, nil
}
func (r *LightRenderer) setupTerminal() error {
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 0ca8aee7..16ce452d 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -7,7 +7,6 @@ import (
"time"
"github.com/gdamore/tcell/v2"
- "github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
@@ -146,13 +145,13 @@ var (
_initialResize bool = true
)
-func (r *FullscreenRenderer) initScreen() {
+func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
if e != nil {
- errorExit(e.Error())
+ return e
}
if e = s.Init(); e != nil {
- errorExit(e.Error())
+ return e
}
if r.mouse {
s.EnableMouse()
@@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse()
}
_screen = s
+
+ return nil
}
-func (r *FullscreenRenderer) Init() {
+func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "")
}
- encoding.Register()
- r.initScreen()
+ if err := r.initScreen(); err != nil {
+ return err
+ }
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
+
+ return nil
}
func (r *FullscreenRenderer) Top() int {
@@ -320,16 +324,16 @@ func (r *FullscreenRenderer) GetChar() Event {
switch ev.Rune() {
case 0:
if ctrl {
- return Event{BSpace, 0, nil}
+ return Event{Backspace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
case ctrl:
return keyfn('h')
case alt:
- return Event{AltBS, 0, nil}
+ return Event{AltBackspace, 0, nil}
case none, shift:
- return Event{BSpace, 0, nil}
+ return Event{Backspace, 0, nil}
}
}
case tcell.KeyCtrlI:
@@ -382,17 +386,17 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2:
if alt {
- return Event{AltBS, 0, nil}
+ return Event{AltBackspace, 0, nil}
}
- return Event{BSpace, 0, nil}
+ return Event{Backspace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp:
if altShift {
- return Event{AltSUp, 0, nil}
+ return Event{AltShiftUp, 0, nil}
}
if shift {
- return Event{SUp, 0, nil}
+ return Event{ShiftUp, 0, nil}
}
if alt {
return Event{AltUp, 0, nil}
@@ -400,10 +404,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil}
case tcell.KeyDown:
if altShift {
- return Event{AltSDown, 0, nil}
+ return Event{AltShiftDown, 0, nil}
}
if shift {
- return Event{SDown, 0, nil}
+ return Event{ShiftDown, 0, nil}
}
if alt {
return Event{AltDown, 0, nil}
@@ -411,10 +415,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil}
case tcell.KeyLeft:
if altShift {
- return Event{AltSLeft, 0, nil}
+ return Event{AltShiftLeft, 0, nil}
}
if shift {
- return Event{SLeft, 0, nil}
+ return Event{ShiftLeft, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
@@ -422,10 +426,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil}
case tcell.KeyRight:
if altShift {
- return Event{AltSRight, 0, nil}
+ return Event{AltShiftRight, 0, nil}
}
if shift {
- return Event{SRight, 0, nil}
+ return Event{ShiftRight, 0, nil}
}
if alt {
return Event{AltRight, 0, nil}
@@ -442,17 +446,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlDelete, 0, nil}
}
if shift {
- return Event{SDelete, 0, nil}
+ return Event{ShiftDelete, 0, nil}
}
- return Event{Del, 0, nil}
+ return Event{Delete, 0, nil}
case tcell.KeyEnd:
return Event{End, 0, nil}
case tcell.KeyPgUp:
- return Event{PgUp, 0, nil}
+ return Event{PageUp, 0, nil}
case tcell.KeyPgDn:
- return Event{PgDn, 0, nil}
+ return Event{PageDown, 0, nil}
case tcell.KeyBacktab:
- return Event{BTab, 0, nil}
+ return Event{ShiftTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
@@ -498,7 +502,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc
case tcell.KeyEsc:
- return Event{ESC, 0, nil}
+ return Event{Esc, 0, nil}
}
}
@@ -561,7 +565,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
- fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
+ if w.borderStyle.shape.HasLeft() {
+ fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
+ } else {
+ fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
+ }
w.drawBorder(false)
}
diff --git a/src/tui/tcell_test.go b/src/tui/tcell_test.go
index 0772a9e2..217ad048 100644
--- a/src/tui/tcell_test.go
+++ b/src/tui/tcell_test.go
@@ -3,6 +3,7 @@
package tui
import (
+ "os"
"testing"
"github.com/gdamore/tcell/v2"
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
// Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) {
- if util.ToTty() {
+ if util.IsTty(os.Stdout) {
// This test is skipped when output goes to terminal, because it causes
// some glitches:
// - output lines may not start at the beginning of a row which makes
@@ -102,22 +103,22 @@ func TestGetCharEventKey(t *testing.T) {
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
- {giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
- {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
- {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
- {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
- {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
+ {giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
+ {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
+ {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
+ {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
+ {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
- {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
- {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
- {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
+ {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
+ {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,8 +127,8 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
- {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
- {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
+ {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
+ {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
@@ -161,11 +162,11 @@ func TestGetCharEventKey(t *testing.T) {
// section 7: Esc
// KeyEsc and KeyEscape are aliases for KeyESC
- {giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
- {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
- {giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
- {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
- {giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
+ {giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
+ {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
+ {giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
+ {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
+ {giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
// section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
@@ -259,7 +260,7 @@ Quick reference
37 LeftClick
38 RightClick
39 BTab
-40 BSpace
+40 Backspace
41 Del
42 PgUp
43 PgDn
@@ -272,7 +273,7 @@ Quick reference
50 Insert
51 SUp
52 SDown
-53 SLeft
+53 ShiftLeft
54 SRight
55 F1
56 F2
@@ -288,15 +289,15 @@ Quick reference
66 F12
67 Change
68 BackwardEOF
-69 AltBS
+69 AltBackspace
70 AltUp
71 AltDown
72 AltLeft
73 AltRight
74 AltSUp
75 AltSDown
-76 AltSLeft
-77 AltSRight
+76 AltShiftLeft
+77 AltShiftRight
78 Alt
79 CtrlAlt
..
diff --git a/src/tui/ttyname_unix.go b/src/tui/ttyname_unix.go
index bc6fe968..d0350a0b 100644
--- a/src/tui/ttyname_unix.go
+++ b/src/tui/ttyname_unix.go
@@ -4,12 +4,19 @@ package tui
import (
"os"
+ "sync/atomic"
"syscall"
)
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
+var tty atomic.Value
+
func ttyname() string {
+ if cached := tty.Load(); cached != nil {
+ return cached.(string)
+ }
+
var stderr syscall.Stat_t
if syscall.Fstat(2, &stderr) != nil {
return ""
@@ -27,24 +34,21 @@ func ttyname() string {
continue
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
- return prefix + file.Name()
+ value := prefix + file.Name()
+ tty.Store(value)
+ return value
}
}
}
return ""
}
-// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
-func TtyIn() *os.File {
- in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
- if err != nil {
- tty := ttyname()
- if len(tty) > 0 {
- if in, err := o