diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2015-01-02 04:49:30 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2015-01-04 00:37:29 +0900 |
commit | f3177305d5572b26f135fc045481358b4eb1bf69 (patch) | |
tree | d59fd9587e44e998581a131875bf45e243df6c6e /src | |
parent | 7ba93d9f8351be64b37c65ae04d594ee261d5d26 (diff) |
Rewrite fzf in Go
Diffstat (limited to 'src')
-rw-r--r-- | src/Dockerfile | 33 | ||||
-rw-r--r-- | src/LICENSE | 21 | ||||
-rw-r--r-- | src/Makefile | 49 | ||||
-rw-r--r-- | src/README.md | 59 | ||||
-rw-r--r-- | src/algo.go | 152 | ||||
-rw-r--r-- | src/algo_test.go | 44 | ||||
-rw-r--r-- | src/atomicbool.go | 27 | ||||
-rw-r--r-- | src/atomicbool_test.go | 17 | ||||
-rw-r--r-- | src/cache.go | 47 | ||||
-rw-r--r-- | src/chunklist.go | 73 | ||||
-rw-r--r-- | src/chunklist_test.go | 66 | ||||
-rw-r--r-- | src/constants.go | 12 | ||||
-rw-r--r-- | src/core.go | 153 | ||||
-rw-r--r-- | src/curses/curses.go | 424 | ||||
-rw-r--r-- | src/eventbox.go | 48 | ||||
-rw-r--r-- | src/fzf/main.go | 7 | ||||
-rw-r--r-- | src/item.go | 135 | ||||
-rw-r--r-- | src/item_test.go | 78 | ||||
-rw-r--r-- | src/matcher.go | 215 | ||||
-rw-r--r-- | src/options.go | 276 | ||||
-rw-r--r-- | src/options_test.go | 37 | ||||
-rw-r--r-- | src/pattern.go | 305 | ||||
-rw-r--r-- | src/pattern_test.go | 87 | ||||
-rw-r--r-- | src/reader.go | 60 | ||||
-rw-r--r-- | src/reader_test.go | 52 | ||||
-rw-r--r-- | src/terminal.go | 580 | ||||
-rw-r--r-- | src/tokenizer.go | 194 | ||||
-rw-r--r-- | src/tokenizer_test.go | 97 | ||||
-rw-r--r-- | src/util.go | 21 | ||||
-rw-r--r-- | src/util_test.go | 18 |
30 files changed, 3387 insertions, 0 deletions
diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 00000000..3c062eea --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi <junegunn.c@gmail.com> + +# apt-get +RUN apt-get update && apt-get -y upgrade +RUN apt-get install -y --force-yes git vim-nox curl procps sudo \ + build-essential libncurses-dev + +# Setup jg user with sudo privilege +RUN useradd -s /bin/bash -m jg && echo 'jg:jg' | chpasswd && \ + echo 'jg ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jg + +# Setup dotfiles +USER jg +RUN cd ~ && git clone https://github.com/junegunn/dotfiles.git && \ + dotfiles/install > /dev/null + +# Install Go 1.4 +RUN cd ~ && curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz && \ + mv go go1.4 && \ + echo 'export GOROOT=~/go1.4' >> ~/dotfiles/bashrc-extra && \ + echo 'export PATH=~/go1.4/bin:$PATH' >> ~/dotfiles/bashrc-extra + +# Symlink fzf directory +RUN mkdir -p ~jg/go/src/github.com/junegunn && \ + ln -s /fzf ~jg/go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd ~jg/go/src/github.com/junegunn/fzf/src && /bin/bash -l + diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 00000000..fe4c31ae --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +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 +THE SOFTWARE. diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 00000000..bae4c906 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,49 @@ +BINARY := fzf/fzf + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + BINARY := $(BINARY)_darwin +else ifeq ($(UNAME_S),Linux) + BINARY := $(BINARY)_linux +endif + +UNAME_M := $(shell uname -m) +ifneq ($(filter i386 i686,$(UNAME_M)),) +$(error "filtered is not supported, yet.") +endif + +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY)_amd64 +else ifneq ($(filter i386 i686,$(UNAME_M)),) + BINARY := $(BINARY)_386 +else # TODO +$(error "$(UNAME_M) is not supported, yet.") +endif + +BINDIR = ../bin +SOURCES = $(wildcard *.go fzf/*.go) + +all: build + +build: $(BINARY) + +$(BINARY): $(SOURCES) + go get + go test -v + cd fzf && go build -o $(notdir $(BINARY)) + +install: $(BINARY) + mkdir -p $(BINDIR) + cp -f $(BINARY) $(BINDIR)/fzf + +clean: + rm -f $(BINARY) + +docker: + docker build -t junegunn/ubuntu-sandbox . + +linux64: + docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ + /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make build' + +.PHONY: build install linux64 clean docker run diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..2f3ca3bb --- /dev/null +++ b/src/README.md @@ -0,0 +1,59 @@ +fzf in Go +========= + +This directory contains the source code for the new fzf implementation in Go. +This new version has the following benefits over the previous Ruby version. + +- Immensely faster + - No GIL. Performance is linearly proportional to the number of cores. + - It's so fast that I even decided to remove the sort limit (`--sort=N`) +- Does not require Ruby and distributed as an executable binary + - Ruby dependency is especially painful on Ruby 2.1 or above which + ships without curses gem + +Build +----- + +```sh +# Build fzf executable +make + +# Install the executable to ../bin directory +make install + +# Build executable for Linux x86_64 using Docker +make linux64 +``` + + +Prebuilt binaries +----------------- + +- Darwin x86_64 +- Linux x86_64 + +Third-party libraries used +-------------------------- + +- [ncurses](https://www.gnu.org/software/ncurses/) +- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) + - Licensed under [MIT](http://mattn.mit-license.org/2013) +- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) + - Licensed under [MIT](http://mattn.mit-license.org/2014) + +Contribution +------------ + +For the moment, I will not add or accept any new features until we can be sure +that the implementation is stable and we have a sufficient number of test +cases. However, fixes for obvious bugs and new test cases are welcome. + +I also care much about the performance of the implementation (that's the +reason I rewrote the whole thing in Go, right?), so please make sure that your +change does not result in performance regression. Please be minded that we +still don't have a quantitative measure of the performance. + +License +------- + +- [MIT](LICENSE) diff --git a/src/algo.go b/src/algo.go new file mode 100644 index 00000000..16790ba9 --- /dev/null +++ b/src/algo.go @@ -0,0 +1,152 @@ +package fzf + +import "strings" + +/* + * String matching algorithms here do not use strings.ToLower to avoid + * performance penalty. And they assume pattern runes are given in lowercase + * letters when caseSensitive is false. + * + * In short: They try to do as little work as possible. + */ + +func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + + // 0. (FIXME) How to find the shortest match? + // a_____b__c__abc + // ^^^^^^^^^^ ^^^ + // 1. forward scan (abc) + // *-----*-----*> + // a_____b___abc__ + // 2. reverse scan (cba) + // a_____b___abc__ + // <*** + pidx := 0 + sidx := -1 + eidx := -1 + + for index, char := range runes { + // This is considerably faster than blindly applying strings.ToLower to the + // whole string + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char == pattern[pidx] { + if sidx < 0 { + sidx = index + } + if pidx += 1; pidx == len(pattern) { + eidx = index + 1 + break + } + } + } + + if sidx >= 0 && eidx >= 0 { + pidx -= 1 + for index := eidx - 1; index >= sidx; index-- { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char == pattern[pidx] { + if pidx -= 1; pidx < 0 { + sidx = index + break + } + } + } + return sidx, eidx + } + return -1, -1 +} + +func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { + var str string + if caseSensitive { + str = *input + } else { + str = strings.ToLower(*input) + } + + if idx := strings.Index(str, string(pattern)); idx >= 0 { + prefixRuneLen := len([]rune((*input)[:idx])) + return prefixRuneLen, prefixRuneLen + len(pattern) + } + return -1, -1 +} + +/* + * This is a basic string searching algorithm that handles case sensitivity. + * Although naive, it still performs better than the combination of + * strings.ToLower + strings.Index for typical fzf use cases where input + * strings and patterns are not very long. + * + * We might try to implement better algorithms in the future: + * http://en.wikipedia.org/wiki/String_searching_algorithm + */ +func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + numRunes := len(runes) + plen := len(pattern) + if len(runes) < plen { + return -1, -1 + } + + pidx := 0 + for index := 0; index < numRunes; index++ { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if pattern[pidx] == char { + pidx += 1 + if pidx == plen { + return index - plen + 1, index + 1 + } + } else { + index -= pidx + pidx = 0 + } + } + return -1, -1 +} + +func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + if len(runes) < len(pattern) { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return 0, len(pattern) +} + +func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(strings.TrimRight(*input, " ")) + trimmedLen := len(runes) + diff := trimmedLen - len(pattern) + if diff < 0 { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index+diff] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return trimmedLen - len(pattern), trimmedLen +} diff --git a/src/algo_test.go b/src/algo_test.go new file mode 100644 index 00000000..5da01a6d --- /dev/null +++ b/src/algo_test.go @@ -0,0 +1,44 @@ +package fzf + +import ( + "strings" + "testing" +) + +func assertMatch(t *testing.T, fun func(bool, *string, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { + if !caseSensitive { + pattern = strings.ToLower(pattern) + } + s, e := fun(caseSensitive, &input, []rune(pattern)) + if s != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) + } + if e != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern) + } +} + +func TestFuzzyMatch(t *testing.T) { + assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestExactMatchNaive(t *testing.T) { + assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestPrefixMatch(t *testing.T) { + assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) + assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) +} + +func TestSuffixMatch(t *testing.T) { + assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) + assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) +} diff --git a/src/atomicbool.go b/src/atomicbool.go new file mode 100644 index 00000000..f2f4894f --- /dev/null +++ b/src/atomicbool.go @@ -0,0 +1,27 @@ +package fzf + +import "sync" + +type AtomicBool struct { + mutex sync.Mutex + state bool +} + +func NewAtomicBool(initialState bool) *AtomicBool { + return &AtomicBool{ + mutex: sync.Mutex{}, + state: initialState} +} + +func (a *AtomicBool) Get() bool { + a.mutex.Lock() + defer a.mutex.Unlock() + return a.state +} + +func (a *AtomicBool) Set(newState bool) bool { + a.mutex.Lock() + defer a.mutex.Unlock() + a.state = newState + return a.state +} diff --git a/src/atomicbool_test.go b/src/atomicbool_test.go new file mode 100644 index 00000000..0af45701 --- /dev/null +++ b/src/atomicbool_test.go @@ -0,0 +1,17 @@ +package fzf + +import "testing" + +func TestAtomicBool(t *testing.T) { + if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { + t.Error("Invalid initial value") + } + + ab := NewAtomicBool(true) + if ab.Set(false) { + t.Error("Invalid return value") + } + if ab.Get() { + t.Error("Invalid state") + } +} diff --git a/src/cache.go b/src/cache.go new file mode 100644 index 00000000..340f3258 --- /dev/null +++ b/src/cache.go @@ -0,0 +1,47 @@ +package fzf + +import "sync" + +type QueryCache map[string][]*Item +type ChunkCache struct { + mutex sync.Mutex + cache map[*Chunk]*QueryCache +} + +func NewChunkCache() ChunkCache { + return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)} +} + +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { + if len(key) == 0 || !chunk.IsFull() { + return + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + cc.cache[chunk] = &QueryCache{} + qc = cc.cache[chunk] + } + (*qc)[key] = list +} + +func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { + if len(key) == 0 || !chunk.IsFull() { + return nil, false + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if ok { + list, ok := (*qc)[key] + if ok { + return list, true + } + } + return nil, false +} diff --git a/src/chunklist.go b/src/chunklist.go new file mode 100644 index 00000000..b1f9638d --- /dev/null +++ b/src/chunklist.go @@ -0,0 +1,73 @@ +package fzf + +import "sync" + +const CHUNK_SIZE int = 100 + +type Chunk []*Item // >>> []Item + +type Transformer func(*string, int) *Item + +type ChunkList struct { + chunks []*Chunk + count int + mutex sync.Mutex + trans Transformer +} + +func NewChunkList(trans Transformer) *ChunkList { + return &ChunkList{ + chunks: []*Chunk{}, + count: 0, + mutex: sync.Mutex{}, + trans: trans} +} + +func (c *Chunk) push(trans Transformer, data *string, index int) { + *c = append(*c, trans(data, index)) +} + +func (c *Chunk) IsFull() bool { + return len(*c) == CHUNK_SIZE +} + +func (cl *ChunkList) lastChunk() *Chunk { + return cl.chunks[len(cl.chunks)-1] +} + +func CountItems(cs []*Chunk) int { + if len(cs) == 0 { + return 0 + } + return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) +} + +func (cl *ChunkList) Count() int { + return cl.count +} + +func (cl *ChunkList) Chunks() []*Chunk { + return cl.chunks +} + +func (cl *ChunkList) Push(data string) { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { + newChunk := Chunk(make([]*Item, 0, CHUNK_SIZE)) + cl.chunks = append(cl.chunks, &newChunk) + } + + cl.lastChunk().push(cl.trans, &data, cl.count) + cl.count += 1 +} + +func (cl *ChunkList) Snapshot() []*Chunk { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + ret := make([]*Chunk, len(cl.chunks)) + copy(ret, cl.chunks) + return ret +} diff --git a/src/chunklist_test.go b/src/chunklist_test.go new file mode 100644 index 00000000..a7daa47e --- /dev/null +++ b/src/chunklist_test.go @@ -0,0 +1,66 @@ +package fzf + +import ( + "fmt" + "testing" +) + +func TestChunkList(t *testing.T) { + cl := NewChunkList(func(s *string, i int) *Item { + return &Item{text: s, index: i * 2} + }) + + // Snapshot + snapshot := cl.Snapshot() + if len(snapshot) > 0 { + t.Error("Snapshot should be empty now") + } + + // Add some data + cl.Push("hello") + cl.Push("world") + + // Previously created snapshot should remain the same + if len(snapshot) > 0 { + t.Error("Snapshot should not have changed") + } + + // But the new snapshot should contain the added items + snapshot = cl.Snapshot() + if len(snapshot) != 1 { + t.Error("Snapshot should not be empty now") + } + + // Check the content of the ChunkList + chunk1 := snapshot[0] + if len(*chunk1) != 2 { + t.Error("Snapshot should contain only two items") + } + if *(*chunk1)[0].text != "hello" || (*chunk1)[0].index != 0 || + *(*chunk1)[1].text != "world" || (*chunk1)[1].index != 2 { + t.Error("Invalid data") + } + if chunk1.IsFull() { + t.Error("Chunk should not have been marked full yet") + } + + // Add more data + for i := 0; i < CHUNK_SIZE*2; i++ { + cl.Push(fmt.Sprintf("item %d", i)) + } + + // Previous snapshot should remain the same + if len(snapshot) != 1 { + t.Error("Snapshot should stay the same") + } + + // New snapshot + snapshot = cl.Snapshot() + if len(snapshot) != 3 || !snapshot[0].IsFull() || + !snapshot[1].IsFull() || snapshot[2].IsFull() { + t.Error("Expected two full chunks and one more chunk") + } + if len(*snapshot[2]) != 2 { + t.Error("Unexpected number of items") + } +} diff --git a/src/constants.go b/src/constants.go new file mode 100644 index 00000000..b0b64dbb --- /dev/null +++ b/src/constants.go @@ -0,0 +1,12 @@ +package fzf + +const VERSION = "0.9.0" + +const ( + EVT_READ_NEW EventType = iota + EVT_READ_FIN + EVT_SEARCH_NEW + EVT_SEARCH_PROGRESS + EVT_SEARCH_FIN + EVT_CLOSE +) diff --git a/src/core.go b/src/core.go new file mode 100644 index 00000000..2601397a --- /dev/null +++ b/src/core.go @@ -0,0 +1,153 @@ +package fzf + +import ( + "fmt" + "os" + "runtime" + "time" +) + +const COORDINATOR_DELAY time.Duration = 100 * time.Millisecond + +func initProcs() { + runtime.GOMAXPROCS(runtime.NumCPU()) +} + +/* +Reader -> EVT_READ_FIN +Reader -> EVT_READ_NEW -> Matcher (restart) +Terminal -> EVT_SEARCH_NEW -> Matcher (restart) +Matcher -> EVT_SEARCH_PROGRESS -> Terminal (update info) +Matcher -> EVT_SEARCH_FIN -> Terminal (update list) +*/ + +func Run(options *Options) { + initProcs() + + opts := ParseOptions() + + if opts.Version { + fmt.Println(VERSION) + os.Exit(0) + } + + // Event channel + eventBox := NewEventBox() + + // Chunk list + var chunkList *ChunkList + if len(opts.WithNth) == 0 { + chunkList = NewChunkList(func(data *string, index int) *Item { + return &Item{text: data, index: index} + }) + } else { + chunkList = NewChunkList(func(data *string, index int) *Item { + item := Item{text: data, index: index} + tokens := Tokenize(item.text, opts.Delimiter) + item.origText = item.text + item.text = Transform(tokens, opts.WithNth).whole + return &item + }) + } + + // Reader + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + go reader.ReadSource() + + // Matcher + patternBuilder := func(runes []rune) *Pattern { + return BuildPattern( + opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) + } + matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) + + // Defered-interactive / Non-interactive + // --select-1 | --exit-0 | --filter + if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { + limit := 0 + var patternString string + if filtering { + patternString = *opts.Filter + } else { + if opts.Select1 || opts.Exit0 { + limit = 1 + } + patternString = opts.Query + } + pattern := patternBuilder([]rune(patternString)) + + looping := true + for looping { + eventBox.Wait(func(events *Events) { + for evt, _ := range *events { + switch evt { + case EVT_READ_FIN: + looping = false + return + } + } + }) + time.Sleep(COORDINATOR_DELAY) + } + + matches, cancelled := matcher.scan(MatchRequest{ + chunks: chunkList.Snapshot(), + pattern: pattern}, limit) + + if !cancelled && (filtering || opts.Exit0) { + if opts.PrintQuery { + fmt.Println(patternString) + } + for _, item := range matches { + item.Print() + } + os.Exit(0) + } + } + + // Go interactive + go matcher.Loop() + + // Terminal I/O + terminal := NewTerminal(opts, eventBox) + go terminal.Loop() + + // Event coordination + reading := true + ticks := 0 + for { + delay := true + ticks += 1 + eventBox.Wait(func(events *Events) { + defer events.Clear() + for evt, value := range *events { + switch evt { + + case EVT_READ_NEW, EVT_READ_FIN: + reading = reading && evt == EVT_READ_NEW + terminal.UpdateCount(chunkList.Count(), !reading) + matcher.Reset(chunkList.Snapshot(), terminal.Input(), false) + + case EVT_SEARCH_NEW: + matcher.Reset(chunkList.Snapshot(), terminal.Input(), true) + delay = false + + case EVT_SEARCH_PROGRESS: + switch val := value.(type) { + case float32: + terminal.UpdateProgress(val) + } + + case EVT_SEARCH_FIN: + switch val := value.(type) { + case []*Item: + terminal.UpdateList(val) + } + } + } + }) + if ticks > 3 && delay && reading { + time.Sleep(COORDINATOR_DELAY) + } + } +} diff --git a/src/curses/curses.go b/src/curses/curses.go new file mode 100644 index 00000000..945a3ce4 --- /dev/null +++ b/src/curses/curses.go @@ -0,0 +1,424 @@ +package curses + +// #include <ncurses.h> +// #include <locale.h> +// #cgo LDFLAGS: -lncurses +import "C" + +import ( + "os" + "os/signal" + "syscall" + "time" + "unicode/utf8" +) + +const ( + RUNE = iota + + CTRL_A + CTRL_B + CTRL_C + CTRL_D + CTRL_E + CTRL_F + CTRL_G + CTRL_H + TAB + CTRL_J + CTRL_K + CTRL_L + CTRL_M + CTRL_N + CTRL_O + CTRL_P + CTRL_Q + CTRL_R + CTRL_S + CTRL_T + CTRL_U + CTRL_V + CTRL_W + CTRL_X + CTRL_Y + CTRL_Z + ESC + + INVALID + MOUSE + + BTAB + + DEL + PGUP + PGDN + + ALT_B + ALT_F + ALT_D + ALT_BS +) + +const ( + COL_NORMAL = iota + COL_PROMPT + COL_MATCH + COL_CURRENT + COL_CURRENT_MATCH + COL_SPINNER + COL_INFO + COL_CURSOR + COL_SELECTED +) + +const ( + DOUBLE_CLICK_DURATION = 500 * time.Millisecond +) + +type Event struct { + Type int + Char rune + MouseEvent *MouseEvent +} + +type MouseEvent struct { + Y int + X int + S int + Down bool + Double bool + Mod bool +} + +var ( + _buf []byte + _in *os.File + _color func(int, bool) C.int + _prevDownTime time.Time + _prevDownY int + _clickY []int +) + +func init() { + _prevDownTime = time.Unix(0, 0) + _clickY = []int{} +} + +func attrColored(pair int, bold bool) C.int { + var attr C.int = 0 + if pair > COL_NORMAL { + attr = C.COLOR_PAIR(C.int(pair)) + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func attrMono(pair int, bold bool) C.int { + var attr C.int = 0 + switch pair { + case COL_CURRENT: + if bold { + attr = C.A_REVERSE + } + case COL_MATCH: + attr = C.A_UNDERLINE + case COL_CURRENT_MATCH: + attr = C.A_UNDERLINE | C.A_REVERSE + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func MaxX() int { + return int(C.COLS) +} + +func MaxY() int { + return int(C.LINES) +} + +func getch(nonblock bool) int { + b := make([]byte, 1) + syscall.SetNonblock(int(_in.Fd()), nonblock) + _, err := _in.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func Init(color bool, color256 bool, black bool, mouse bool) { + { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + _in = in + // Break STDIN + // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) + } + + swapOutput() + + C.setlocale(C.LC_ALL, C.CString("")) + C.initscr() + if mouse { + C.mousemask(C.ALL_MOUSE_EVENTS, nil) + } + C.cbreak() + C.noecho() + C.raw() // stty dsusp undef + C.set_tabsize(4) // FIXME + + intChan := make(chan os.Signal, 1) + signal.Notify(intChan, os.Interrupt, os.Kill) + go func() { + <-intChan + Close() + os.Exit(1) + }() + + if color { + C.start_color() + var bg C.short + if black { + bg = C.COLOR_BLACK + } else { + C.use_default_colors() + bg = -1 + } + if color256 { + C.init_pair(COL_PROMPT, 110, bg) + C.init_pair(COL_MATCH, 108, bg) + C.init_pair(COL_CURRENT, 254, 236) + C.init_pair(COL_CURRENT_MATCH, 151, 236) + C.init_pair(COL_SPINNER, 148, bg) + C.init_pair(COL_INFO, 144, bg) + C.init_pair(COL_CURSOR, 161, 236) + C.init_pair(COL_SELECTED, 168, 236) + } else { + C.init_pair(COL_PROMPT, C.COLOR_BLUE, bg) + C.init_pair(COL_MATCH, C.COLOR_GREEN, bg) + C.init_pair(COL_CURRENT, C.COLOR_YELLOW, C.COLOR_BLACK) + C.init_pair(COL_CURRENT_MATCH, C.COL |