Compare commits

..

16 Commits

Author SHA1 Message Date
Junegunn Choi
a2beb159f1 0.15.7 2016-11-09 12:41:46 +09:00
Junegunn Choi
7ce427ff47 Fix panic when color is disabled and header lines contain ANSI colors
Close #732
2016-11-09 12:05:45 +09:00
Junegunn Choi
a221c672fb 0.15.6 2016-11-09 01:45:27 +09:00
Junegunn Choi
f87d382ec8 Fix --color=bw on tcell build 2016-11-09 01:45:06 +09:00
Junegunn Choi
3dfc020fac Merge pull request #730 from laur89/master
Minor README markup fix
2016-11-09 00:06:42 +09:00
Laur Aliste
2d87896939 Minor README markup fix. 2016-11-08 15:41:46 +01:00
Junegunn Choi
2192d8d816 GOOS=windows make release 2016-11-08 03:32:41 +09:00
Junegunn Choi
d206949f62 Wait for additional keys after ESC for up to 100ms
Close #661
2016-11-08 03:07:26 +09:00
Junegunn Choi
4accc69022 Fix flaky test cases 2016-11-08 02:19:05 +09:00
Junegunn Choi
898d8d94c8 Fix issues in tcell renderer and Windows build
- Fix display of CJK wide characters
- Fix horizontal offset of header lines
- Add support for keys with ALT modifier, shift-tab, page-up and down
- Fix util.ExecCommand to properly parse command-line arguments
- Fix redraw on resize
- Implement Pause/Resume for execute action
- Remove runtime check of GOOS
- Change exit status to 2 when tcell failed to start
- TBD: Travis CI build for tcell renderer
    - Pending. tcell cannot reliably ingest keys from tmux send-keys
2016-11-08 02:06:34 +09:00
Michael Kelley
26895da969 Implement tcell-based renderer 2016-11-07 02:32:14 +09:00
Junegunn Choi
0c573b3dff Prepare for termbox/windows build
`TAGS=termbox make` (or `go build -tags termbox`)
2016-11-07 02:32:14 +09:00
Junegunn Choi
2cff00dce2 man fzf in README
Close #726
2016-11-01 00:39:02 +09:00
Junegunn Choi
06a6ad8bca Update ANSI processor to ignore ^N and ^O
This reverts commit 02c6ad0e59.
2016-10-30 12:29:29 +09:00
Junegunn Choi
02c6ad0e59 Strip ^N and ^O from preview output
https://github.com/junegunn/fzf/issues/391#issuecomment-257090266

e.g. fzf --preview 'printf "$(tput setaf 2)foo$(tput sgr0)bar\nbar\n"'
2016-10-30 11:43:06 +09:00
Junegunn Choi
9f321cbe13 Fix header lines being cleared on toggle-preview
Close #722
2016-10-28 03:13:50 +09:00
30 changed files with 1501 additions and 704 deletions

View File

@@ -1,6 +1,10 @@
language: ruby language: ruby
rvm: matrix:
- 2.2.0 include:
- env: TAGS=
rvm: 2.2.0
# - env: TAGS=tcell
# rvm: 2.2.0
install: install:
- sudo apt-get update - sudo apt-get update

View File

@@ -1,6 +1,18 @@
CHANGELOG CHANGELOG
========= =========
0.15.7
------
- Fixed panic when color is disabled and header lines contain ANSI colors
0.15.6
------
- Windows binaries! (@kelleyma49)
- Fixed the bug where header lines are cleared when preview window is toggled
- Fixed not to display ^N and ^O on screen
- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by
making fzf wait for additional keystrokes after ESC for up to 100ms
0.15.5 0.15.5
------ ------
- Setting foreground color will no longer set background color to black - Setting foreground color will no longer set background color to black

View File

@@ -82,6 +82,15 @@ method used.
- brew: `brew update; brew reinstall fzf` - brew: `brew update; brew reinstall fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. However, other
components of the project may not work on Windows. You might want to consider
installing fzf on [Windows Subsystem for Linux][wsl] where everything runs
flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Usage Usage
----- -----
@@ -102,7 +111,7 @@ vim $(fzf)
#### Using the finder #### Using the finder
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down - `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items - On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
- Emacs style key bindings - Emacs style key bindings
@@ -145,6 +154,10 @@ or `py`.
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
#### Options
See the man page (`man fzf`) for the full list of options.
Examples Examples
-------- --------

View File

@@ -2,8 +2,8 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.15.5 pre=1 || [[ "$@" =~ --pre ]] && version=0.15.7 pre=1 ||
version=0.15.5 pre=0 version=0.15.7 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Oct 2016" "fzf 0.15.5" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Nov 2016" "fzf 0.15.7" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Oct 2016" "fzf 0.15.5" "fzf - a command-line fuzzy finder" .TH fzf 1 "Nov 2016" "fzf 0.15.7" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder

View File

@@ -33,17 +33,24 @@ endif
all: fzf/$(BINARY) all: fzf/$(BINARY)
ifeq ($(GOOS),windows)
release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \
rm -f $(RELEASE32).exe $(RELEASE64).exe
else
release: test fzf/$(BINARY32) fzf/$(BINARY64) release: test fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64) rm -f $(RELEASE32) $(RELEASE64)
endif
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES) deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get cd $(SRCDIR) && go get -tags "$(TAGS)"
android-build: $(SRCDIR) android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
@@ -52,7 +59,7 @@ android-build: $(SRCDIR)
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
test: deps test: deps
SHELL=/bin/sh go test -v ./... SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf

View File

@@ -83,9 +83,11 @@ Third-party libraries used
- [ncurses][ncurses] - [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org/2013) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org/2014) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
- Licensed under [MIT](http://mattn.mit-license.org)
License License
------- -------

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
) )
type ansiOffset struct { type ansiOffset struct {
@@ -16,9 +16,9 @@ type ansiOffset struct {
} }
type ansiState struct { type ansiState struct {
fg int fg tui.Color
bg int bg tui.Color
attr curses.Attr attr tui.Attr
} }
func (s *ansiState) colored() bool { func (s *ansiState) colored() bool {
@@ -35,7 +35,7 @@ func (s *ansiState) equals(t *ansiState) bool {
var ansiRegex *regexp.Regexp var ansiRegex *regexp.Regexp
func init() { func init() {
ansiRegex = regexp.MustCompile("\x1b.[0-9;]*.") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*.|[\x0e\x0f]")
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -100,7 +100,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.attr} state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
} }
if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { if ansiCode[0] != '\x1b' || ansiCode[len(ansiCode)-1] != 'm' {
return state return state
} }
@@ -134,26 +134,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49: case 49:
state.bg = -1 state.bg = -1
case 1: case 1:
state.attr = curses.Bold state.attr = tui.Bold
case 2: case 2:
state.attr = curses.Dim state.attr = tui.Dim
case 4: case 4:
state.attr = curses.Underline state.attr = tui.Underline
case 5: case 5:
state.attr = curses.Blink state.attr = tui.Blink
case 7: case 7:
state.attr = curses.Reverse state.attr = tui.Reverse
case 0: case 0:
init() init()
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
state.fg = num - 30 state.fg = tui.Color(num - 30)
} else if num >= 40 && num <= 47 { } else if num >= 40 && num <= 47 {
state.bg = num - 40 state.bg = tui.Color(num - 40)
} else if num >= 90 && num <= 97 { } else if num >= 90 && num <= 97 {
state.fg = num - 90 + 8 state.fg = tui.Color(num - 90 + 8)
} else if num >= 100 && num <= 107 { } else if num >= 100 && num <= 107 {
state.bg = num - 100 + 8 state.bg = tui.Color(num - 100 + 8)
} }
} }
case 1: case 1:
@@ -164,7 +164,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
state256 = 0 state256 = 0
} }
case 2: case 2:
*ptr = num *ptr = tui.Color(num)
state256 = 0 state256 = 0
} }
} }

View File

@@ -4,14 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
) )
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr curses.Attr var attr tui.Attr
if bold { if bold {
attr = curses.Bold attr = tui.Bold
} }
if offset.offset[0] != b || offset.offset[1] != e || if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {

View File

@@ -8,14 +8,13 @@ import (
const ( const (
// Current version // Current version
version = "0.15.5" version = "0.15.7"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader // Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
readerBufferSize = 64 * 1024 readerBufferSize = 64 * 1024
// Terminal // Terminal

8
src/constants_unix.go Normal file
View File

@@ -0,0 +1,8 @@
// +build !windows
package fzf
const (
// Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
)

8
src/constants_windows.go Normal file
View File

@@ -0,0 +1,8 @@
// +build windows
package fzf
const (
// Reader
defaultCommand = `dir /s/b`
)

View File

@@ -1,7 +1,10 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os"
"os/user" "os/user"
"runtime"
"testing" "testing"
) )
@@ -10,23 +13,34 @@ func TestHistory(t *testing.T) {
// Invalid arguments // Invalid arguments
user, _ := user.Current() user, _ := user.Current()
paths := []string{"/etc", "/proc"} var paths []string
if user.Name != "root" { if runtime.GOOS == "windows" {
paths = append(paths, "/etc/sudoers") // GOPATH should exist, so we shouldn't be able to override it
paths = []string{os.Getenv("GOPATH")}
} else {
paths = []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
} }
for _, path := range paths { for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil { if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path) t.Error("Error expected for: " + path)
} }
} }
f, _ := ioutil.TempFile("", "fzf-history")
f.Close()
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
for i := 0; i < maxHistory+10; i++ { for i := 0; i < maxHistory+10; i++ {
h.append("foobar") h.append("foobar")
} }
} }
{ // Read lines { // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }
@@ -37,13 +51,13 @@ func TestHistory(t *testing.T) {
} }
} }
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
h.append("barfoo") h.append("barfoo")
h.append("") h.append("")
h.append("foobarbaz") h.append("foobarbaz")
} }
{ // Read lines again { // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }

View File

@@ -9,7 +9,7 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@@ -142,7 +142,7 @@ type Options struct {
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *tui.ColorTheme
Black bool Black bool
Reverse bool Reverse bool
Cycle bool Cycle bool
@@ -187,7 +187,7 @@ func defaultOptions() *Options {
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: curses.EmptyTheme(), Theme: tui.EmptyTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
@@ -358,60 +358,60 @@ func parseKeyChords(str string, message string) map[int]string {
chord := 0 chord := 0
switch lkey { switch lkey {
case "up": case "up":
chord = curses.Up chord = tui.Up
case "down": case "down":
chord = curses.Down chord = tui.Down
case "left": case "left":
chord = curses.Left chord = tui.Left
case "right": case "right":
chord = curses.Right chord = tui.Right
case "enter", "return": case "enter", "return":
chord = curses.CtrlM chord = tui.CtrlM
case "space": case "space":
chord = curses.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = curses.BSpace chord = tui.BSpace
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = curses.AltEnter chord = tui.AltEnter
case "alt-space": case "alt-space":
chord = curses.AltSpace chord = tui.AltSpace
case "alt-/": case "alt-/":
chord = curses.AltSlash chord = tui.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = curses.AltBS chord = tui.AltBS
case "tab": case "tab":
chord = curses.Tab chord = tui.Tab
case "btab", "shift-tab": case "btab", "shift-tab":
chord = curses.BTab chord = tui.BTab
case "esc": case "esc":
chord = curses.ESC chord = tui.ESC
case "del": case "del":
chord = curses.Del chord = tui.Del
case "home": case "home":
chord = curses.Home chord = tui.Home
case "end": case "end":
chord = curses.End chord = tui.End
case "pgup", "page-up": case "pgup", "page-up":
chord = curses.PgUp chord = tui.PgUp
case "pgdn", "page-down": case "pgdn", "page-down":
chord = curses.PgDn chord = tui.PgDn
case "shift-left": case "shift-left":
chord = curses.SLeft chord = tui.SLeft
case "shift-right": case "shift-right":
chord = curses.SRight chord = tui.SRight
case "double-click": case "double-click":
chord = curses.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
chord = curses.F10 chord = tui.F10
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a' chord = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1' chord = tui.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 { } else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0]) chord = tui.AltZ + int([]rune(key)[0])
} else { } else {
errorExit("unsupported key: " + key) errorExit("unsupported key: " + key)
} }
@@ -458,7 +458,7 @@ func parseTiebreak(str string) []criterion {
return criteria return criteria
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
if theme != nil { if theme != nil {
dupe := *theme dupe := *theme
return &dupe return &dupe
@@ -466,16 +466,16 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
return nil return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") { for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str { switch str {
case "dark": case "dark":
theme = dupeTheme(curses.Dark256) theme = dupeTheme(tui.Dark256)
case "light": case "light":
theme = dupeTheme(curses.Light256) theme = dupeTheme(tui.Light256)
case "16": case "16":
theme = dupeTheme(curses.Default16) theme = dupeTheme(tui.Default16)
case "bw", "no": case "bw", "no":
theme = nil theme = nil
default: default:
@@ -495,7 +495,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
if err != nil || ansi32 < -1 || ansi32 > 255 { if err != nil || ansi32 < -1 || ansi32 > 255 {
fail() fail()
} }
ansi := int16(ansi32) ansi := tui.Color(ansi32)
switch pair[0] { switch pair[0] {
case "fg": case "fg":
theme.Fg = ansi theme.Fg = ansi
@@ -572,9 +572,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
} }
var key int var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon { if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ key = ',' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
@@ -868,7 +868,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
opts.Theme = curses.EmptyTheme() opts.Theme = tui.EmptyTheme()
} else { } else {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
@@ -905,7 +905,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "+c", "--no-color": case "+c", "--no-color":
opts.Theme = nil opts.Theme = nil
case "+2", "--no-256": case "+2", "--no-256":
opts.Theme = curses.Default16 opts.Theme = tui.Default16
case "--black": case "--black":
opts.Black = true opts.Black = true
case "--no-black": case "--no-black":
@@ -1071,11 +1071,11 @@ func parseOptions(opts *Options, allArgs []string) {
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[curses.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[curses.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = actPreviousHistory
} }
if _, prs := opts.Keymap[curses.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[curses.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = actNextHistory
} }
} }

View File

@@ -2,9 +2,10 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -133,48 +134,48 @@ func TestParseKeys(t *testing.T) {
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(curses.CtrlZ, "ctrl-z") check(tui.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z") check(tui.AltZ, "alt-z")
check(curses.F2, "f2") check(tui.F2, "f2")
check(curses.AltZ+'@', "@") check(tui.AltZ+'@', "@")
check(curses.AltA, "Alt-a") check(tui.AltA, "Alt-a")
check(curses.AltZ+'!', "!") check(tui.AltZ+'!', "!")
check(curses.CtrlA+'g'-'a', "ctrl-G") check(tui.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J") check(tui.AltZ+'J', "J")
check(curses.AltZ+'g', "g") check(tui.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter") check(tui.AltEnter, "ALT-enter")
check(curses.AltSpace, "alt-SPACE") check(tui.AltSpace, "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
check(curses.CtrlM, "Return") check(tui.CtrlM, "Return")
check(curses.AltZ+' ', "space") check(tui.AltZ+' ', "space")
check(curses.Tab, "tab") check(tui.Tab, "tab")
check(curses.BTab, "btab") check(tui.BTab, "btab")
check(curses.ESC, "esc") check(tui.ESC, "esc")
check(curses.Up, "up") check(tui.Up, "up")
check(curses.Down, "down") check(tui.Down, "down")
check(curses.Left, "left") check(tui.Left, "left")
check(curses.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(curses.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(curses.PgUp, "page-up") check(tui.PgUp, "page-up")
check(curses.PgDn, "Page-Down") check(tui.PgDn, "Page-Down")
check(curses.Home, "Home") check(tui.Home, "Home")
check(curses.End, "End") check(tui.End, "End")
check(curses.AltBS, "Alt-BSpace") check(tui.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left") check(tui.SLeft, "shift-left")
check(curses.SRight, "shift-right") check(tui.SRight, "shift-right")
check(curses.BTab, "shift-tab") check(tui.BTab, "shift-tab")
check(curses.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(curses.BSpace, "bspace") check(tui.BSpace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
@@ -191,36 +192,36 @@ func TestParseKeysWithComma(t *testing.T) {
pairs := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+'c', "c") check(pairs, tui.AltZ+'c', "c")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
@@ -236,41 +237,41 @@ func TestBind(t *testing.T) {
} }
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string) execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA]) check(actBeginningOfLine, keymap[tui.CtrlA])
parseKeymap(keymap, execmap, parseKeymap(keymap, execmap,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
check(actKillLine, keymap[curses.CtrlA]) check(actKillLine, keymap[tui.CtrlA])
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[tui.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[tui.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+',']) check(actAbort, keymap[tui.AltZ+','])
check(actAccept, keymap[curses.AltZ+':']) check(actAccept, keymap[tui.AltZ+':'])
check(actPageDown, keymap[curses.AltZ]) check(actPageDown, keymap[tui.AltZ])
check(actExecute, keymap[curses.F1]) check(actExecute, keymap[tui.F1])
check(actExecute, keymap[curses.F2]) check(actExecute, keymap[tui.F2])
check(actExecute, keymap[curses.F3]) check(actExecute, keymap[tui.F3])
check(actExecute, keymap[curses.F4]) check(actExecute, keymap[tui.F4])
checkString("ls {}", execmap[curses.F1]) checkString("ls {}", execmap[tui.F1])
checkString("echo {}, {}, {}", execmap[curses.F2]) checkString("echo {}, {}, {}", execmap[tui.F2])
checkString("echo '({})'", execmap[curses.F3]) checkString("echo '({})'", execmap[tui.F3])
checkString("less {}", execmap[curses.F4]) checkString("less {}", execmap[tui.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X'])
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, execmap, "f1:abort")
check(actAbort, keymap[curses.F1]) check(actAbort, keymap[tui.F1])
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := curses.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
@@ -283,7 +284,7 @@ func TestColorSpec(t *testing.T) {
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
if *light != *curses.Light256 { if *light != *tui.Light256 {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
if light == theme { if light == theme {
@@ -294,23 +295,23 @@ func TestColorSpec(t *testing.T) {
if customized.Fg != 231 || customized.Bg != 232 { if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
if *curses.Dark256 == *customized { if *tui.Dark256 == *customized {
t.Errorf("colors should not be equivalent") t.Errorf("colors should not be equivalent")
} }
customized.Fg = curses.Dark256.Fg customized.Fg = tui.Dark256.Fg
customized.Bg = curses.Dark256.Bg customized.Bg = tui.Dark256.Bg
if *curses.Dark256 != *customized { if *tui.Dark256 != *customized {
t.Errorf("colors should now be equivalent: %v, %v", curses.Dark256, customized) t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
} }
func TestParseNilTheme(t *testing.T) { func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme var theme *tui.ColorTheme
newTheme := parseTheme(theme, "prompt:12") newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil { if newTheme != nil {
t.Errorf("color is disabled. keep it that way.") t.Errorf("color is disabled. keep it that way.")
@@ -330,21 +331,23 @@ func TestDefaultCtrlNP(t *testing.T) {
t.Error() t.Error()
} }
} }
check([]string{}, curses.CtrlN, actDown) check([]string{}, tui.CtrlN, actDown)
check([]string{}, curses.CtrlP, actUp) check([]string{}, tui.CtrlP, actUp)
check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
hist := "--history=/tmp/fzf-history" f, _ := ioutil.TempFile("", "fzf-history")
check([]string{hist}, curses.CtrlN, actNextHistory) f.Close()
check([]string{hist}, curses.CtrlP, actPreviousHistory) hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
} }
func optsFor(words ...string) *Options { func optsFor(words ...string) *Options {

View File

@@ -39,9 +39,15 @@ func (r *Reader) feed(src io.Reader) {
// ReadBytes returns err != nil if and only if the returned data does not // ReadBytes returns err != nil if and only if the returned data does not
// end in delim. // end in delim.
bytea, err := reader.ReadBytes(delim) bytea, err := reader.ReadBytes(delim)
byteaLen := len(bytea)
if len(bytea) > 0 { if len(bytea) > 0 {
if err == nil { if err == nil {
bytea = bytea[:len(bytea)-1] // get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
bytea = bytea[:byteaLen-2]
} else {
bytea = bytea[:byteaLen-1]
}
} }
if r.pusher(bytea) { if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)

View File

@@ -5,7 +5,7 @@ import (
"sort" "sort"
"unicode" "unicode"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -14,8 +14,8 @@ type Offset [2]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
color int color tui.ColorPair
attr curses.Attr attr tui.Attr
index int32 index int32
} }
@@ -92,7 +92,7 @@ func minRank() rank {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
} }
func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, current bool) []colorOffset { func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
itemColors := result.item.Colors() itemColors := result.item.Colors()
// No ANSI code, or --color=no // No ANSI code, or --color=no
@@ -147,25 +147,27 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorThe
} else { } else {
ansi := itemColors[curr-1] ansi := itemColors[curr-1]
fg := ansi.color.fg fg := ansi.color.fg
if fg == -1 {
if current {
fg = int(theme.Current)
} else {
fg = int(theme.Fg)
}
}
bg := ansi.color.bg bg := ansi.color.bg
if bg == -1 { if theme != nil {
if current { if fg == -1 {
bg = int(theme.DarkBg) if current {
} else { fg = theme.Current
bg = int(theme.Bg) } else {
fg = theme.Fg
}
}
if bg == -1 {
if current {
bg = theme.DarkBg
} else {
bg = theme.Bg
}
} }
} }
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg), color: tui.PairFor(fg, bg),
attr: ansi.color.attr | attr}) attr: ansi.color.attr.Merge(attr)})
} }
} }
} }

View File

@@ -1,3 +1,5 @@
// +build !tcell
package fzf package fzf
import ( import (
@@ -5,7 +7,7 @@ import (
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -98,26 +100,26 @@ func TestColorOffset(t *testing.T) {
item: &Item{ item: &Item{
colors: &[]ansiOffset{ colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}}, ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}}, ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, curses.Bold}}}}} ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
colors := item.colorOffsets(offsets, curses.Dark256, 99, 0, true) colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true)
assert := func(idx int, b int32, e int32, c int, bold bool) { assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
var attr curses.Attr var attr tui.Attr
if bold { if bold {
attr = curses.Bold attr = tui.Bold
} }
o := colors[idx] o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
t.Error(o) t.Error(o)
} }
} }
assert(0, 0, 5, curses.ColUser, false) assert(0, 0, 5, tui.ColUser, false)
assert(1, 5, 15, 99, false) assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false) assert(2, 15, 20, tui.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true) assert(3, 22, 25, tui.ColUser+1, true)
assert(4, 25, 35, 99, false) assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true) assert(5, 35, 40, tui.ColUser+2, true)
} }

View File

@@ -7,12 +7,13 @@ import (
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
C "github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth" "github.com/junegunn/go-runewidth"
@@ -69,9 +70,9 @@ type Terminal struct {
header0 []string header0 []string
ansi bool ansi bool
margin [4]sizeSpec margin [4]sizeSpec
window *C.Window window *tui.Window
bwindow *C.Window bwindow *tui.Window
pwindow *C.Window pwindow *tui.Window
count int count int
progress int progress int
reading bool reading bool
@@ -90,7 +91,7 @@ type Terminal struct {
suppress bool suppress bool
startChan chan bool startChan chan bool
slab *util.Slab slab *util.Slab
theme *C.ColorTheme theme *tui.ColorTheme
} }
type selectedItem struct { type selectedItem struct {
@@ -187,51 +188,51 @@ const (
func defaultKeymap() map[int]actionType { func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType) keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid keymap[tui.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine keymap[tui.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar keymap[tui.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort keymap[tui.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort keymap[tui.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort keymap[tui.CtrlQ] = actAbort
keymap[C.ESC] = actAbort keymap[tui.ESC] = actAbort
keymap[C.CtrlD] = actDeleteCharEOF keymap[tui.CtrlD] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine keymap[tui.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar keymap[tui.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar keymap[tui.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar keymap[tui.BSpace] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown keymap[tui.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp keymap[tui.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown keymap[tui.CtrlJ] = actDown
keymap[C.CtrlK] = actUp keymap[tui.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen keymap[tui.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept keymap[tui.CtrlM] = actAccept
keymap[C.CtrlN] = actDown keymap[tui.CtrlN] = actDown
keymap[C.CtrlP] = actUp keymap[tui.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard keymap[tui.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout keymap[tui.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank keymap[tui.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord keymap[tui.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord keymap[tui.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord keymap[tui.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord keymap[tui.SRight] = actForwardWord
keymap[C.AltD] = actKillWord keymap[tui.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord keymap[tui.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp keymap[tui.Up] = actUp
keymap[C.Down] = actDown keymap[tui.Down] = actDown
keymap[C.Left] = actBackwardChar keymap[tui.Left] = actBackwardChar
keymap[C.Right] = actForwardChar keymap[tui.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine keymap[tui.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine keymap[tui.End] = actEndOfLine
keymap[C.Del] = actDeleteChar keymap[tui.Del] = actDeleteChar
keymap[C.PgUp] = actPageUp keymap[tui.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown keymap[tui.PgDn] = actPageDown
keymap[C.Rune] = actRune keymap[tui.Rune] = actRune
keymap[C.Mouse] = actMouse keymap[tui.Mouse] = actMouse
keymap[C.DoubleClick] = actAccept keymap[tui.DoubleClick] = actAccept
return keymap return keymap
} }
@@ -299,7 +300,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
theme: opts.Theme, theme: opts.Theme,
startChan: make(chan bool, 1), startChan: make(chan bool, 1),
initFunc: func() { initFunc: func() {
C.Init(opts.Theme, opts.Black, opts.Mouse) tui.Init(opts.Theme, opts.Black, opts.Mouse)
}} }}
} }
@@ -429,8 +430,8 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
} }
func (t *Terminal) resizeWindows() { func (t *Terminal) resizeWindows() {
screenWidth := C.MaxX() screenWidth := tui.MaxX()
screenHeight := C.MaxY() screenHeight := tui.MaxY()
marginInt := [4]int{} marginInt := [4]int{}
for idx, sizeSpec := range t.margin { for idx, sizeSpec := range t.margin {
if sizeSpec.percent { if sizeSpec.percent {
@@ -479,33 +480,33 @@ func (t *Terminal) resizeWindows() {
height := screenHeight - marginInt[0] - marginInt[2] height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = C.NewWindow(y, x, w, h, true) t.bwindow = tui.NewWindow(y, x, w, h, true)
t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false) t.pwindow = tui.NewWindow(y+1, x+2, w-4, h-2, false)
} }
switch t.preview.position { switch t.preview.position {
case posUp: case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3) pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow( t.window = tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false) marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown: case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3) pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow( t.window = tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false) marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft: case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow( t.window = tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight: case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow( t.window = tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false) marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
} }
} else { } else {
t.window = C.NewWindow( t.window = tui.NewWindow(
marginInt[0], marginInt[0],
marginInt[3], marginInt[3],
width, width,
@@ -531,24 +532,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
t.move(0, 0, true) t.move(0, 0, true)
t.window.CPrint(C.ColPrompt, C.Bold, t.prompt) t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt)
t.window.CPrint(C.ColNormal, C.Bold, string(t.input)) t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input))
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading { if t.reading {
t.window.CPrint(C.ColSpinner, C.Bold, " < ") t.window.CPrint(tui.ColSpinner, tui.Bold, " < ")
} else { } else {
t.window.CPrint(C.ColPrompt, C.Bold, " < ") t.window.CPrint(tui.ColPrompt, tui.Bold, " < ")
} }
} else { } else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(C.ColSpinner, C.Bold, _spinner[idx]) t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx])
} }
t.move(1, 2, false) t.move(1, 2, false)
} }
@@ -567,7 +568,7 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
t.window.CPrint(C.ColInfo, 0, output) t.window.CPrint(tui.ColInfo, 0, output)
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
@@ -591,7 +592,8 @@ func (t *Terminal) printHeader() {
colors: colors} colors: colors}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(&Result{item: item}, 0, C.ColHeader, 0, false) t.printHighlighted(&Result{item: item},
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false)
} }
} }
@@ -625,21 +627,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current { } else if current {
label = ">" label = ">"
} }
t.window.CPrint(C.ColCursor, C.Bold, label) t.window.CPrint(tui.ColCursor, tui.Bold, label)
if current { if current {
if selected { if selected {
t.window.CPrint(C.ColSelected, C.Bold, ">") t.window.CPrint(tui.ColSelected, tui.Bold, ">")
} else { } else {
t.window.CPrint(C.ColCurrent, C.Bold, " ") t.window.CPrint(tui.ColCurrent, tui.Bold, " ")
} }
t.printHighlighted(result, C.Bold, C.ColCurrent, C.ColCurrentMatch, true) t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true)
} else { } else {
if selected { if selected {
t.window.CPrint(C.ColSelected, C.Bold, ">") t.window.CPrint(tui.ColSelected, tui.Bold, ">")
} else { } else {
t.window.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(result, 0, C.ColNormal, C.ColMatch, false) t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false)
} }
} }
@@ -695,7 +697,7 @@ func overflow(runes []rune, max int) bool {
return false return false
} }
func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 int, current bool) { func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) {
item := result.item item := result.item
// Overflow // Overflow
@@ -827,7 +829,7 @@ func (t *Terminal) printPreview() {
if t.previewer.offset > 0 { if t.previewer.offset > 0 {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
t.pwindow.Move(0, t.pwindow.Width-len(offset)) t.pwindow.Move(0, t.pwindow.Width-len(offset))
t.pwindow.CPrint(C.ColInfo, C.Reverse, offset) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
} }
} }
@@ -858,11 +860,10 @@ func (t *Terminal) printAll() {
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
t.bwindow.Refresh() tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window})
t.pwindow.Refresh() } else {
tui.RefreshWindows([]*tui.Window{t.window})
} }
t.window.Refresh()
C.DoUpdate()
} }
} }
@@ -912,13 +913,16 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...) t.input = append(t.input[:t.cx], after...)
} }
func keyMatch(key int, event C.Event) bool { func keyMatch(key int, event tui.Event) bool {
return event.Type == key || return event.Type == key ||
event.Type == C.Rune && int(event.Char) == key-C.AltZ || event.Type == tui.Rune && int(event.Char) == key-tui.AltZ ||
event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
if util.IsWindows() {
return strconv.Quote(strings.Replace(entry, "\"", "\\\"", -1))
}
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
@@ -980,8 +984,11 @@ func (t *Terminal) executeCommand(template string, items []*Item) {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
C.Endwin() tui.Pause()
cmd.Run() cmd.Run()
if tui.Resume() {
t.printAll()
}
t.refresh() t.refresh()
} }
@@ -1014,7 +1021,7 @@ func (t *Terminal) Loop() {
}() }()
resizeChan := make(chan os.Signal, 1) resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH) notifyOnResize(resizeChan) // Non-portable
go func() { go func() {
for { for {
<-resizeChan <-resizeChan
@@ -1126,12 +1133,11 @@ func (t *Terminal) Loop() {
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
C.Clear() tui.Clear()
C.Endwin() tui.Refresh()
C.Refresh()
t.printAll() t.printAll()
case reqClose: case reqClose:
C.Close() tui.Close()
if t.output() { if t.output() {
exit(exitOk) exit(exitOk)
} }
@@ -1144,11 +1150,11 @@ func (t *Terminal) Loop() {
case reqPreviewRefresh: case reqPreviewRefresh:
t.printPreview() t.printPreview()
case reqPrintQuery: case reqPrintQuery:
C.Close() tui.Close()
t.printer(string(t.input)) t.printer(string(t.input))
exit(exitOk) exit(exitOk)
case reqQuit: case reqQuit:
C.Close() tui.Close()
exit(exitInterrupt) exit(exitInterrupt)
} }
} }
@@ -1161,7 +1167,7 @@ func (t *Terminal) Loop() {
looping := true looping := true
for looping { for looping {
event := C.GetChar() event := tui.GetChar()
t.mutex.Lock() t.mutex.Lock()
previousInput := t.input previousInput := t.input
@@ -1236,7 +1242,7 @@ func (t *Terminal) Loop() {
if t.previewer.enabled && cnt > 0 && cnt > t.cy { if t.previewer.enabled && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
} }
req(reqList, reqInfo) req(reqList, reqInfo, reqHeader)
} }
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
@@ -1445,7 +1451,7 @@ func (t *Terminal) Loop() {
// Double-click // Double-click
if my >= min { if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[C.DoubleClick], C.DoubleClick) return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick)
} }
} }
} else if me.Down { } else if me.Down {
@@ -1468,8 +1474,8 @@ func (t *Terminal) Loop() {
mapkey := event.Type mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
action := t.keymap[mapkey] action := t.keymap[mapkey]
if mapkey == C.Rune { if mapkey == tui.Rune {
mapkey = int(event.Char) + int(C.AltZ) mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act action = act
} }
@@ -1484,7 +1490,7 @@ func (t *Terminal) Loop() {
} }
changed = string(previousInput) != string(t.input) changed = string(previousInput) != string(t.input)
} else { } else {
if mapkey == C.Rune { if mapkey == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled { if t.jumping == jumpAcceptEnabled {

13
src/terminal_unix.go Normal file
View File

@@ -0,0 +1,13 @@
// +build !windows
package fzf
import (
"os"
"os/signal"
"syscall"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}

11
src/terminal_windows.go Normal file
View File

@@ -0,0 +1,11 @@
// +build windows
package fzf
import (
"os"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
// TODO
}

View File

@@ -1,4 +1,7 @@
package curses // +build !windows
// +build !tcell
package tui
/* /*
#include <ncurses.h> #include <ncurses.h>
@@ -10,7 +13,6 @@ package curses
SCREEN *c_newterm () { SCREEN *c_newterm () {
return newterm(NULL, stderr, stdin); return newterm(NULL, stderr, stdin);
} }
*/ */
import "C" import "C"
@@ -23,6 +25,10 @@ import (
"unicode/utf8" "unicode/utf8"
) )
type ColorPair int16
type Attr C.int
type WindowImpl C.WINDOW
const ( const (
Bold = C.A_BOLD Bold = C.A_BOLD
Dim = C.A_DIM Dim = C.A_DIM
@@ -31,89 +37,13 @@ const (
Underline = C.A_UNDERLINE Underline = C.A_UNDERLINE
) )
type Attr C.int
// Types of user action
const ( const (
Rune = iota AttrRegular Attr = 0
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'
) )
// Pallete // Pallete
const ( const (
_ = iota ColDefault ColorPair = iota
ColNormal ColNormal
ColPrompt ColPrompt
ColMatch ColMatch
@@ -128,193 +58,26 @@ const (
ColUser // Should be the last entry ColUser // Should be the last entry
) )
const (
doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
)
type ColorTheme struct {
Fg int16
Bg int16
DarkBg int16
Prompt int16
Match int16
Current int16
CurrentMatch int16
Spinner int16
Info int16
Cursor int16
Selected int16
Header int16
Border 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 ( var (
_buf []byte _in *os.File
_in *os.File _screen *C.SCREEN
_color bool _colorMap map[int]ColorPair
_colorFn func(int, Attr) C.int _colorFn func(ColorPair, Attr) C.int
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
_screen *C.SCREEN
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
) )
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
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(ColNormal)))
}
if border {
attr := _colorFn(ColBorder, 0)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
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() { func init() {
_prevDownTime = time.Unix(0, 0) _colorMap = make(map[int]ColorPair)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
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,
Border: C.COLOR_BLACK}
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 attrColored(pair int, a Attr) C.int { func (a Attr) Merge(b Attr) Attr {
var attr C.int return a | b
if pair > 0 { }
attr = C.COLOR_PAIR(C.int(pair))
func DefaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
} }
return attr | C.int(a) return Default16
}
func attrMono(pair int, a Attr) C.int {
var attr C.int
switch pair {
case ColCurrent:
if 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 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 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) { func Init(theme *ColorTheme, black bool, mouse bool) {
@@ -344,52 +107,19 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
_color = theme != nil _color = theme != nil
if _color { if _color {
C.start_color() C.start_color()
var baseTheme *ColorTheme InitTheme(theme, black)
if C.tigetnum(C.CString("colors")) >= 256 { initPairs(theme)
baseTheme = Dark256 C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal)))
_colorFn = attrColored _colorFn = attrColored
} else { } else {
_colorFn = attrMono _colorFn = attrMono
} }
} }
func override(baseTheme *ColorTheme, theme *ColorTheme) { func initPairs(theme *ColorTheme) {
o := func(a int16, b int16) int16 {
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)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
if black {
theme.Bg = C.COLOR_BLACK
}
// Updates theme
override(baseTheme, theme)
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
initPair := func(group C.short, fg int16, bg int16) { initPair := func(group ColorPair, fg Color, bg Color) {
C.init_pair(group, C.short(fg), C.short(bg)) C.init_pair(C.short(group), C.short(fg), C.short(bg))
} }
initPair(ColNormal, theme.Fg, theme.Bg) initPair(ColNormal, theme.Fg, theme.Bg)
initPair(ColPrompt, theme.Prompt, theme.Bg) initPair(ColPrompt, theme.Prompt, theme.Bg)
@@ -404,20 +134,185 @@ func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
initPair(ColBorder, theme.Border, theme.Bg) initPair(ColBorder, theme.Border, theme.Bg)
} }
func Pause() {
C.endwin()
}
func Resume() bool {
return false
}
func Close() { func Close() {
C.endwin() C.endwin()
C.delscreen(_screen) 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 { func GetBytes() []byte {
c := getch(false) c := getch(false)
retries := 0
if c == 27 {
// Wait for additional keys after ESC for 100ms (10 * 10ms)
retries = 10
}
_buf = append(_buf, byte(c)) _buf = append(_buf, byte(c))
for { for {
c = getch(true) c = getch(true)
if c == -1 { if c == -1 {
if retries > 0 {
retries--
time.Sleep(10 * time.Millisecond)
continue
}
break break
} }
retries = 0
_buf = append(_buf, byte(c)) _buf = append(_buf, byte(c))
} }
@@ -631,84 +526,3 @@ func GetChar() Event {
sz = rsz sz = rsz
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
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 int, 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()
}
func Endwin() {
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 int, bg int, 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 (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
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
}

575
src/tui/tcell.go Normal file
View File

@@ -0,0 +1,575 @@
// +build tcell windows
package tui
import (
"time"
"unicode/utf8"
"fmt"
"os"
"runtime"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/encoding"
"github.com/junegunn/go-runewidth"
)
type ColorPair [2]Color
func (p ColorPair) fg() Color {
return p[0]
}
func (p ColorPair) bg() Color {
return p[1]
}
func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg()))
}
type Attr tcell.Style
type WindowTcell struct {
LastX int
LastY int
MoveCursor bool
Border bool
}
type WindowImpl WindowTcell
const (
Bold = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
)
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 _screen.Colors() >= 256 {
return Dark256
}
return Default16
}
func PairFor(fg Color, bg Color) ColorPair {
return [2]Color{fg, bg}
}
var (
_colorToAttribute = []tcell.Color{
tcell.ColorBlack,
tcell.ColorRed,
tcell.ColorGreen,
tcell.ColorYellow,
tcell.ColorBlue,
tcell.ColorDarkMagenta,
tcell.ColorLightCyan,
tcell.ColorWhite,
}
)
func (c Color) Style() tcell.Color {
if c <= colDefault {
return tcell.ColorDefault
} else if c >= colBlack && c <= colWhite {
return _colorToAttribute[int(c)]
} else {
return tcell.Color(c)
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
var (
_screen tcell.Screen
_mouse bool
)
func initScreen() {
s, e := tcell.NewScreen()
if e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(2)
}
if e = s.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(2)
}
if _mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
}
func Init(theme *ColorTheme, black bool, mouse bool) {
encoding.Register()
_mouse = mouse
initScreen()
_color = theme != nil
if _color {
InitTheme(theme, black)
} else {
theme = DefaultTheme()
}
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}
}
func MaxX() int {
ncols, _ := _screen.Size()
return int(ncols)
}
func MaxY() int {
_, nlines := _screen.Size()
return int(nlines)
}
func (w *Window) win() *WindowTcell {
return (*WindowTcell)(w.impl)
}
func Clear() {
_screen.Sync()
_screen.Clear()
}
func Refresh() {
// noop
}
func GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
return Event{Invalid, 0, nil}
// process mouse events:
case *tcell.EventMouse:
x, y := ev.Position()
button := ev.Buttons()
mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}}
} else if runtime.GOOS != "windows" {
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
// to consume them.
down := button&tcell.Button1 != 0 // left
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, x)
} else {
_clickY = []int{x}
}
_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}}
}
// process keyboard:
case *tcell.EventKey:
alt := (ev.Modifiers() & tcell.ModAlt) > 0
switch ev.Key() {
case tcell.KeyCtrlA:
return Event{CtrlA, 0, nil}
case tcell.KeyCtrlB:
return Event{CtrlB, 0, nil}
case tcell.KeyCtrlC:
return Event{CtrlC, 0, nil}
case tcell.KeyCtrlD:
return Event{CtrlD, 0, nil}
case tcell.KeyCtrlE:
return Event{CtrlE, 0, nil}
case tcell.KeyCtrlF:
return Event{CtrlF, 0, nil}
case tcell.KeyCtrlG:
return Event{CtrlG, 0, nil}
case tcell.KeyCtrlJ:
return Event{CtrlJ, 0, nil}
case tcell.KeyCtrlK:
return Event{CtrlK, 0, nil}
case tcell.KeyCtrlL:
return Event{CtrlL, 0, nil}
case tcell.KeyCtrlM:
if alt {
return Event{AltEnter, 0, nil}
}
return Event{CtrlM, 0, nil}
case tcell.KeyCtrlN:
return Event{CtrlN, 0, nil}
case tcell.KeyCtrlO:
return Event{CtrlO, 0, nil}
case tcell.KeyCtrlP:
return Event{CtrlP, 0, nil}
case tcell.KeyCtrlQ:
return Event{CtrlQ, 0, nil}
case tcell.KeyCtrlR:
return Event{CtrlR, 0, nil}
case tcell.KeyCtrlS:
return Event{CtrlS, 0, nil}
case tcell.KeyCtrlT:
return Event{CtrlT, 0, nil}
case tcell.KeyCtrlU:
return Event{CtrlU, 0, nil}
case tcell.KeyCtrlV:
return Event{CtrlV, 0, nil}
case tcell.KeyCtrlW:
return Event{CtrlW, 0, nil}
case tcell.KeyCtrlX:
return Event{CtrlX, 0, nil}
case tcell.KeyCtrlY:
return Event{CtrlY, 0, nil}
case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
}
return Event{BSpace, 0, nil}
case tcell.KeyUp:
return Event{Up, 0, nil}
case tcell.KeyDown:
return Event{Down, 0, nil}
case tcell.KeyLeft:
return Event{Left, 0, nil}
case tcell.KeyRight:
return Event{Right, 0, nil}
case tcell.KeyHome:
return Event{Home, 0, nil}
case tcell.KeyDelete:
return Event{Del, 0, nil}
case tcell.KeyEnd:
return Event{End, 0, nil}
case tcell.KeyPgUp:
return Event{PgUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
case tcell.KeyTab:
return Event{Tab, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
return Event{F2, 0, nil}
case tcell.KeyF3:
return Event{F3, 0, nil}
case tcell.KeyF4:
return Event{F4, 0, nil}
case tcell.KeyF5:
return Event{F5, 0, nil}
case tcell.KeyF6:
return Event{F6, 0, nil}
case tcell.KeyF7:
return Event{F7, 0, nil}
case tcell.KeyF8:
return Event{F8, 0, nil}
case tcell.KeyF9:
return Event{F9, 0, nil}
case tcell.KeyF10:
return Event{F10, 0, nil}
case tcell.KeyF11:
return Event{Invalid, 0, nil}
case tcell.KeyF12:
return Event{Invalid, 0, nil}
// ev.Ch doesn't work for some reason for space:
case tcell.KeyRune:
r := ev.Rune()
if alt {
switch r {
case ' ':
return Event{AltSpace, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
}
if r >= 'a' && r <= 'z' {
return Event{AltA + int(r) - 'a', 0, nil}
}
}
return Event{Rune, r, nil}
case tcell.KeyEsc:
return Event{ESC, 0, nil}
}
}
return Event{Invalid, 0, nil}
}
func Pause() {
_screen.Fini()
}
func Resume() bool {
initScreen()
return true
}
func Close() {
_screen.Fini()
}
func RefreshWindows(windows []*Window) {
// TODO
for _, w := range windows {
if w.win().MoveCursor {
_screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY)
w.win().MoveCursor = false
}
w.win().LastX = 0
w.win().LastY = 0
if w.win().Border {
w.DrawBorder()
}
}
_screen.Show()
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
// TODO
win := new(WindowTcell)
win.Border = border
return &Window{
impl: (*WindowImpl)(win),
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func (w *Window) Close() {
// TODO
}
func fill(x, y, w, h int, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style())
}
}
}
func (w *Window) Erase() {
// TODO
fill(w.Left, w.Top, w.Width, w.Height, ' ')
}
func (w *Window) Enclose(y int, x int) bool {
return x >= w.Left && x <= (w.Left+w.Width) &&
y >= w.Top && y <= (w.Top+w.Height)
}
func (w *Window) Move(y int, x int) {
w.win().LastX = x
w.win().LastY = y
w.win().MoveCursor = true
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
for i := w.win().LastX; i < w.Width; i++ {
_screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style())
}
w.win().LastX = x
}
func (w *Window) Print(text string) {
w.PrintString(text, ColDefault, 0)
}
func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
t := text
lx := 0
var style tcell.Style
if _color {
style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
} else {
style = ColDefault.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
for {
if len(t) == 0 {
break
}
r, size := utf8.DecodeRuneInString(t)
t = t[size:]
if r < rune(' ') { // ignore control characters
continue
}
if r == '\n' {
w.win().LastY++
lx = 0
} else {
if r == '\u000D' { // skip carriage return
continue
}
var xPos = w.Left + w.win().LastX + lx
var yPos = w.Top + w.win().LastY
if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
}
}
w.win().LastX += lx
}
func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
w.PrintString(text, pair, a)
}
func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
lx := 0
var style tcell.Style
if _color {
style = pair.style()
} else {
style = ColDefault.style()
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
for _, r := range text {
if r == '\n' {
w.win().LastY++
w.win().LastX = 0
lx = 0
} else {
var xPos = w.Left + w.win().LastX + lx
// word wrap:
if xPos > (w.Left + w.Width) {
w.win().LastY++
w.win().LastX = 0
lx = 0
xPos = w.Left
}
var yPos = w.Top + w.win().LastY
if yPos >= (w.Top + w.Height) {
return false
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
}
}
w.win().LastX += lx
return true
}
func (w *Window) Fill(str string) bool {
return w.FillString(str, ColDefault, 0)
}
func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool {
return w.FillString(str, ColorPair{fg, bg}, a)
}
func (w *Window) DrawBorder() {
left := w.Left
right := left + w.Width
top := w.Top
bot := top + w.Height
var style tcell.Style
if _color {
style = ColBorder.style()
} else {
style = ColDefault.style()
}
for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
}
for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
}

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)
}

View File

@@ -1,4 +1,4 @@
package curses package tui
import ( import (
"testing" "testing"

View File

@@ -1,13 +1,11 @@
package util package util
// #include <unistd.h>
import "C"
import ( import (
"math" "math"
"os" "os"
"os/exec"
"time" "time"
"github.com/junegunn/go-isatty"
) )
// Max returns the largest integer // Max returns the largest integer
@@ -95,14 +93,5 @@ func DurWithin(
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return isatty.IsTerminal(os.Stdin.Fd())
}
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
} }

22
src/util/util_unix.go Normal file
View File

@@ -0,0 +1,22 @@
// +build !windows
package util
import (
"os"
"os/exec"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return false
}

28
src/util/util_windows.go Normal file
View File

@@ -0,0 +1,28 @@
// +build windows
package util
import (
"os"
"os/exec"
"github.com/junegunn/go-shellwords"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
args, _ := shellwords.Parse(command)
allArgs := make([]string, len(args)+1)
allArgs[0] = "/c"
copy(allArgs[1:], args)
return exec.Command(shell, allArgs...)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return true
}

View File

@@ -1058,7 +1058,7 @@ class TestGoFZF < TestBase
def test_invalid_term def test_invalid_term
lines = `TERM=xxx #{FZF}` lines = `TERM=xxx #{FZF}`
assert_equal 2, $?.exitstatus assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx') assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end end
def test_invalid_option def test_invalid_option
@@ -1253,24 +1253,29 @@ module TestShell
tmux.send_keys 'cat ', 'C-t', pane: 0 tmux.send_keys 'cat ', 'C-t', pane: 0
tmux.until(1) { |lines| lines.item_count >= 1 } tmux.until(1) { |lines| lines.item_count >= 1 }
tmux.send_keys 'fzf-unicode', pane: 1 tmux.send_keys 'fzf-unicode', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '1', pane: 1 tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' } tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1 tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '2', pane: 1 tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.until do |lines|
tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') } tmux.send_keys 'C-l'
[-1, -2].map { |offset| lines[offset] }.any? do |line|
line.start_with?('cat') && line.include?('fzf-unicode')
end
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' } tmux.until { |lines| lines[-1].include? 'test1test2' }
end end
@@ -1481,23 +1486,27 @@ module CompletionTest
tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '1', pane: 1 tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' } tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1 tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '2', pane: 1 tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1 tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.until do |lines|
tmux.send_keys 'C-l'
lines[-1].include?('cat') || lines[-2].include?('cat')
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end