diff options
author | Michael Kelley <michael.a.kelley@gmail.com> | 2019-02-04 22:51:39 -0800 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2020-03-10 00:03:34 +0900 |
commit | 7d5985baf927cdd04070679cbfbb5ff6b0728f6d (patch) | |
tree | 5b67c8ac90f2a040832c1d17b1ed10746f1d6b36 | |
parent | 7c40a424c0bf5a8967816d51ead6a71a334f30bb (diff) |
Make height option work under Windows (#1341)
Separate Unix & Windows code into platform specific files for light renderer
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | src/options.go | 5 | ||||
-rw-r--r-- | src/tui/light.go | 78 | ||||
-rw-r--r-- | src/tui/light_unix.go | 97 | ||||
-rw-r--r-- | src/tui/light_windows.go | 132 |
5 files changed, 244 insertions, 69 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f68751..1a16fd53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG finder instead of printing horizontal lines above and below it. The previous style is available via `--border=horizontal` - Added `--pointer` and `--marker` options +- `--height` option is now available on Windows binary (@kelleyma49) - More keys and actions for `--bind` - Bug fixes and improvements - Vim plugin: Floating windows support diff --git a/src/options.go b/src/options.go index b87e85c2..0fd97050 100644 --- a/src/options.go +++ b/src/options.go @@ -11,7 +11,6 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" - "github.com/junegunn/fzf/src/util" "github.com/mattn/go-runewidth" "github.com/mattn/go-shellwords" @@ -1412,8 +1411,8 @@ func validateSign(sign string, signOptName string) error { } func postProcessOptions(opts *Options) { - if util.IsWindows() && opts.Height.size > 0 { - errorExit("--height option is currently not supported on Windows") + if !tui.IsLightRendererSupported() && opts.Height.size > 0 { + errorExit("--height option is currently not supported on this platform") } // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { diff --git a/src/tui/light.go b/src/tui/light.go index d8de4b66..3ff84870 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -7,7 +7,6 @@ import ( "regexp" "strconv" "strings" - "syscall" "time" "unicode/utf8" @@ -30,21 +29,6 @@ const consoleDevice string = "/dev/tty" var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") -func openTtyIn() *os.File { - in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) - if err != nil { - tty := ttyname() - if len(tty) > 0 { - if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { - return in - } - } - fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) - os.Exit(2) - } - return in -} - func (r *LightRenderer) stderr(str string) { r.stderrInternal(str, true) } @@ -101,6 +85,13 @@ type LightRenderer struct { y int x int maxHeightFunc func(int) int + + // Windows only + ttyinChannel chan byte + inHandle uintptr + outHandle uintptr + origStateInput uint32 + origStateOutput uint32 } type LightWindow struct { @@ -134,10 +125,6 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in return &r } -func (r *LightRenderer) fd() int { - return int(r.ttyin.Fd()) -} - func (r *LightRenderer) defaultTheme() *ColorTheme { if strings.Contains(os.Getenv("TERM"), "256") { return Dark256 @@ -149,22 +136,6 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { return Default16 } -func (r *LightRenderer) findOffset() (row int, col int) { - r.csi("6n") - r.flush() - bytes := []byte{} - for tries := 0; tries < offsetPollTries; tries++ { - bytes = r.getBytesInternal(bytes, tries > 0) - offsets := offsetRegexp.FindSubmatch(bytes) - if len(offsets) > 3 { - // add anything we skipped over to the input buffer - r.buffer = append(r.buffer, offsets[1]...) - return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1 - } - } - return -1, -1 -} - func repeat(r rune, times int) string { if times > 0 { return strings.Repeat(string(r), times) @@ -183,13 +154,9 @@ func atoi(s string, defaultValue int) int { func (r *LightRenderer) Init() { r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) - fd := r.fd() - origState, err := terminal.GetState(fd) - if err != nil { + if err := r.initPlatform(); err != nil { errorExit(err.Error()) } - r.origState = origState - terminal.MakeRaw(fd) r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -262,28 +229,6 @@ func getEnv(name string, defaultValue int) int { return atoi(env, defaultValue) } -func (r *LightRenderer) updateTerminalSize() { - width, height, err := terminal.GetSize(r.fd()) - if err == nil { - r.width = width - r.height = r.maxHeightFunc(height) - } else { - r.width = getEnv("COLUMNS", defaultWidth) - r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) - } -} - -func (r *LightRenderer) getch(nonblock bool) (int, bool) { - b := make([]byte, 1) - fd := r.fd() - util.SetNonblock(r.ttyin, nonblock) - _, err := util.Read(fd, b) - if err != nil { - return 0, false - } - return int(b[0]), true -} - func (r *LightRenderer) getBytes() []byte { return r.getBytesInternal(r.buffer, false) } @@ -604,7 +549,7 @@ func (r *LightRenderer) rmcup() { } func (r *LightRenderer) Pause(clear bool) { - terminal.Restore(r.fd(), r.origState) + r.restoreTerminal() if clear { if r.fullscreen { r.rmcup() @@ -617,7 +562,7 @@ func (r *LightRenderer) Pause(clear bool) { } func (r *LightRenderer) Resume(clear bool) { - terminal.MakeRaw(r.fd()) + r.setupTerminal() if clear { if r.fullscreen { r.smcup() @@ -671,7 +616,8 @@ func (r *LightRenderer) Close() { r.csi("?1000l") } r.flush() - terminal.Restore(r.fd(), r.origState) + r.closePlatform() + r.restoreTerminal() } func (r *LightRenderer) MaxX() int { diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go new file mode 100644 index 00000000..e4ce6313 --- /dev/null +++ b/src/tui/light_unix.go @@ -0,0 +1,97 @@ +// +build !windows + +package tui + +import ( + "fmt" + "os" + "syscall" + + "github.com/junegunn/fzf/src/util" + "golang.org/x/crypto/ssh/terminal" +) + +func IsLightRendererSupported() bool { + return true +} + +func (r *LightRenderer) fd() int { + return int(r.ttyin.Fd()) +} + +func (r *LightRenderer) initPlatform() error { + fd := r.fd() + origState, err := terminal.GetState(fd) + if err != nil { + return err + } + r.origState = origState + terminal.MakeRaw(fd) + return nil +} + +func (r *LightRenderer) closePlatform() { + // NOOP +} + +func openTtyIn() *os.File { + in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) + if err != nil { + tty := ttyname() + if len(tty) > 0 { + if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { + return in + } + } + fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) + os.Exit(2) + } + return in +} + +func (r *LightRenderer) setupTerminal() { + terminal.MakeRaw(r.fd()) +} + +func (r *LightRenderer) restoreTerminal() { + terminal.Restore(r.fd(), r.origState) +} + +func (r *LightRenderer) updateTerminalSize() { + width, height, err := terminal.GetSize(r.fd()) + + if err == nil { + r.width = width + r.height = r.maxHeightFunc(height) + } else { + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) + } +} + +func (r *LightRenderer) findOffset() (row int, col int) { + r.csi("6n") + r.flush() + bytes := []byte{} + for tries := 0; tries < offsetPollTries; tries++ { + bytes = r.getBytesInternal(bytes, tries > 0) + offsets := offsetRegexp.FindSubmatch(bytes) + if len(offsets) > 3 { + // Add anything we skipped over to the input buffer + r.buffer = append(r.buffer, offsets[1]...) + return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1 + } + } + return -1, -1 +} + +func (r *LightRenderer) getch(nonblock bool) (int, bool) { + b := make([]byte, 1) + fd := r.fd() + util.SetNonblock(r.ttyin, nonblock) + _, err := util.Read(fd, b) + if err != nil { + return 0, false + } + return int(b[0]), true +} diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go new file mode 100644 index 00000000..6f876af2 --- /dev/null +++ b/src/tui/light_windows.go @@ -0,0 +1,132 @@ +//+build windows + +package tui + +import ( + "os" + "syscall" + + "github.com/junegunn/fzf/src/util" + "golang.org/x/sys/windows" +) + +var ( + consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) + consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) +) + +// IsLightRendererSupported checks to see if the Light renderer is supported +func IsLightRendererSupported() bool { + var oldState uint32 + // enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) + if windows.GetConsoleMode(windows.Stderr, &oldState) != nil { + return false + } + // attempt to set mode to determine if we support VT 100 codes. This will work on newer Windows 10 + // version: + canSetVt100 := windows.SetConsoleMode(windows.Stderr, oldState|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil + var checkState uint32 + if windows.GetConsoleMode(windows.Stderr, &checkState) != nil || + (checkState&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { + return false + } + windows.SetConsoleMode(windows.Stderr, oldState) + return canSetVt100 +} + +func (r *LightRenderer) initPlatform() error { + //outHandle := windows.Stdout + outHandle, _ := syscall.Open("CONOUT$", syscall.O_RDWR, 0) + // enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) + if err := windows.GetConsoleMode(windows.Handle(outHandle), &r.origStateOutput); err != nil { + return err + } + r.outHandle = uintptr(outHandle) + inHandle, _ := syscall.Open("CONIN$", syscall.O_RDWR, 0) + if err := windows.GetConsoleMode(windows.Handle(inHandle), &r.origStateInput); err != nil { + return err + } + r.inHandle = uintptr(inHandle) + + r.setupTerminal() + + // channel for non-blocking reads. Buffer to make sure + // we get the ESC sets: + r.ttyinChannel = make(chan byte, 12) + + // the following allows for non-blocking IO. + // syscall.SetNonblock() is a NOOP under Windows. + go func() { + fd := int(r.inHandle) + b := make([]byte, 1) + for { + // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. + _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) + + _, err := util.Read(fd, b) + if err == nil { + r.ttyinChannel <- b[0] + } + } + }() + + return nil +} + +func (r *LightRenderer) closePlatform() { + windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) + windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) +} + +func openTtyIn() *os.File { + // not used + return nil +} + +func (r *LightRenderer) setupTerminal() error { + if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { + return err + } + return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) +} + +func (r *LightRenderer) restoreTerminal() error { + if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil { + return err + } + return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) +} + +func (r *LightRenderer) updateTerminalSize() { + var bufferInfo windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) + + } else { + r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left) + r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top)) + } +} + +func (r *LightRenderer) findOffset() (row int, col int) { + var bufferInfo windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { + return -1, -1 + } + return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y) +} + +func (r *LightRenderer) getch(nonblock bool) (int, bool) { + if nonblock { + select { + case bc := <-r.ttyinChannel: + return int(bc), true + default: + return 0, false + } + } else { + bc := <-r.ttyinChannel + return int(bc), true + } +} |