diff options
Diffstat (limited to 'src/tui')
-rw-r--r-- | src/tui/dummy.go | 4 | ||||
-rw-r--r-- | src/tui/eventtype_string.go | 122 | ||||
-rw-r--r-- | src/tui/light.go | 151 | ||||
-rw-r--r-- | src/tui/light_unix.go | 32 | ||||
-rw-r--r-- | src/tui/light_windows.go | 10 | ||||
-rw-r--r-- | src/tui/tcell.go | 62 | ||||
-rw-r--r-- | src/tui/tcell_test.go | 53 | ||||
-rw-r--r-- | src/tui/ttyname_unix.go | 32 | ||||
-rw-r--r-- | src/tui/ttyname_windows.go | 13 | ||||
-rw-r--r-- | src/tui/tui.go | 183 |
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 |