Compare commits

...

21 Commits

Author SHA1 Message Date
Junegunn Choi
a71c471405 0.15.9 2016-11-26 12:36:24 +09:00
Junegunn Choi
3858086047 Always print scroll indicator in preview window 2016-11-26 12:34:16 +09:00
Junegunn Choi
dffef3d9f3 Update build instructions for ncurses 6 and tcell
Close #357
Close #738
2016-11-26 11:41:57 +09:00
Junegunn Choi
de1c6b8727 [tcell] 24-bit color support
TAGS=tcell make install

    printf "\x1b[38;2;100;200;250mTRUECOLOR\x1b[m\n" |
        TERM=xterm-truecolor fzf --ansi
2016-11-26 00:36:38 +09:00
Junegunn Choi
6f17f412ba Workaround for rendering glitch in case of short-lived input process
: | fzf --preview 'echo foo'
2016-11-25 14:05:37 +09:00
Junegunn Choi
746961bf43 [ncurses6] Suppress tui.Italic on ncurses 5 2016-11-24 13:42:14 +09:00
Junegunn Choi
182a6d99fd [ncurses6] Support italics 2016-11-24 00:13:10 +09:00
Junegunn Choi
af31088481 [ncurses6] Use wcolor_set to support more than 256 color pairs
To build fzf with ncurses 6 on macOS:

    brew install homebrew/dupes/ncurses
    LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
2016-11-24 00:12:43 +09:00
Junegunn Choi
43425158f4 Make escape delay configurable via ncurses standard $ESCDELAY
Also reduce the default delay to 50ms. We should not set it to 0ms as it
breaks escape sequences on WSL. If 50ms is not enough, one can increase
the delay by setting $ESCDELAY to a larger value.
2016-11-23 02:28:03 +09:00
Junegunn Choi
8524ea7441 Do not ignore resize event from ncurses and tcell 2016-11-23 01:58:46 +09:00
Junegunn Choi
6a65006f55 0.15.8 2016-11-19 23:13:26 +09:00
Junegunn Choi
d75ed841a9 Fix --no-bold on --no-color 2016-11-19 23:12:28 +09:00
Junegunn Choi
3cd2547e91 Reduce ESC delay to 100ms 2016-11-19 23:03:27 +09:00
Junegunn Choi
8c661d4e8c Revamp escape sequence processing for WSL
Also add support for alt-[0-9] and f1[12]
2016-11-19 22:42:15 +09:00
Junegunn Choi
4b332d831e Add --no-bold option 2016-11-15 23:57:32 +09:00
Junegunn Choi
22487810ba Update README: link to wiki page 2016-11-15 23:44:04 +09:00
Junegunn Choi
c49e65d926 [shell] Fix pruning condition of find command for CTRL-T and ALT-C
`-fstype dev` is invalid. It's devfs on macOS and devtmpfs on Linux.
2016-11-15 01:52:54 +09:00
Junegunn Choi
2e8814bb57 Add WSL to .github/ISSUE_TEMPLATE.md 2016-11-14 12:26:46 +09:00
Junegunn Choi
dc557c0d4c Update ANSI processor to handle more VT-100 escape sequences
The updated regular expression should include not all but most of the
frequently used ANSI sequences. Close #735.
2016-11-14 02:15:23 +09:00
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
20 changed files with 416 additions and 326 deletions

View File

@@ -11,6 +11,7 @@
- [ ] Linux
- [ ] Mac OS X
- [ ] Windows
- [ ] Windows Subsystem for Linux
- [ ] Etc.
- Shell
- [ ] bash

View File

@@ -1,6 +1,32 @@
CHANGELOG
=========
0.15.9
------
- Fixed rendering glitches introduced in 0.15.8
- The default escape delay is reduced to 50ms and is configurable via
`$ESCDELAY`
- Scroll indicator at the top-right corner of the preview window is always
displayed when there's overflow
- Can now be built with ncurses 6 or tcell to support extra features
- *ncurses 6*
- Supports more than 256 color pairs
- Supports italics
- *tcell*
- 24-bit color support
- See https://github.com/junegunn/fzf/blob/master/src/README.md#build
0.15.8
------
- Updated ANSI processor to handle more VT-100 escape sequences
- Added `--no-bold` (and `--bold`) option
- Improved escape sequence processing for WSL
- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect`
0.15.7
------
- Fixed panic when color is disabled and header lines contain ANSI colors
0.15.6
------
- Windows binaries! (@kelleyma49)

View File

@@ -209,6 +209,8 @@ If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
Fuzzy completion for bash and zsh
---------------------------------

View File

@@ -2,8 +2,8 @@
set -u
[[ "$@" =~ --pre ]] && version=0.15.6 pre=1 ||
version=0.15.6 pre=0
[[ "$@" =~ --pre ]] && version=0.15.9 pre=1 ||
version=0.15.9 pre=0
auto_completion=
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
THE SOFTWARE.
..
.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.6" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.9" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
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
THE SOFTWARE.
..
.TH fzf 1 "Nov 2016" "fzf 0.15.6" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Nov 2016" "fzf 0.15.9" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -216,6 +216,9 @@ e.g. \fBfzf --color=bg+:24\fR
\fBheader \fRHeader
.RE
.TP
.B "--no-bold"
Do not use bold text
.TP
.B "--black"
Use black background
.SS History
@@ -388,7 +391,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-10]\fR
\fIalt-[0-9]\fR
\fIf[1-12]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)

View File

@@ -1,7 +1,7 @@
# Key bindings
# ------------
__fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
@@ -41,7 +41,7 @@ fzf-file-widget() {
__fzf_cd__() {
local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir"
}

View File

@@ -15,7 +15,7 @@ function fzf_key_bindings
function fzf-file-widget
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"
@@ -34,7 +34,7 @@ function fzf_key_bindings
function fzf-cd-widget
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"
# Fish hangs if the command before pipe redirects (2> /dev/null)
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result"

View File

@@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
@@ -33,7 +33,7 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"

View File

@@ -70,10 +70,10 @@ clean:
cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32)
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps
cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64)
cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY) $(BINDIR)

View File

@@ -61,6 +61,45 @@ make install
make linux
```
### With ncurses 6
The official binaries of fzf are built with ncurses 5 because it's widely
supported by different platforms. However ncurses 5 is old and has a number of
limitations.
1. Does not support more than 256 color pairs (See [357][357])
2. Does not support italics
3. Does not support 24-bit color
[357]: https://github.com/junegunn/fzf/issues/357
But you can manually build fzf with ncurses 6 to overcome some of these
limitations. ncurses 6 supports up to 32767 color pairs (1), and supports
italics (2). To build fzf with ncurses 6, you have to install it first. On
macOS, you can use Homebrew to install it.
```sh
brew install homebrew/dupes/ncurses
LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
```
### With tcell
[tcell][tcell] is a portable alternative to ncurses and we currently use it to
build Windows binaries. tcell has many benefits but most importantly, it
supports 24-bit colors. To build fzf with tcell:
```sh
TAGS=tcell make install
```
However, note that tcell has its own issues.
- Poor rendering performance compared to ncurses
- Does not support bracketed-paste mode
- Does not support italics unlike ncurses 6
- Some wide characters are not correctly displayed
Test
----
@@ -88,6 +127,8 @@ Third-party libraries used
- 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)
- [tcell](https://github.com/gdamore/tcell)
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
License
-------
@@ -99,4 +140,4 @@ License
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/
[req]: http://golang.org/doc/install
[termbox]: https://github.com/nsf/termbox-go
[tcell]: https://github.com/gdamore/tcell

View File

@@ -35,7 +35,16 @@ func (s *ansiState) equals(t *ansiState) bool {
var ansiRegex *regexp.Regexp
func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*.|[\x0e\x0f]")
/*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x08\x0e\x0f]")
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -100,7 +109,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
}
if ansiCode[0] != '\x1b' || ansiCode[len(ansiCode)-1] != 'm' {
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
return state
}
@@ -134,15 +143,17 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49:
state.bg = -1
case 1:
state.attr = tui.Bold
state.attr = state.attr | tui.Bold
case 2:
state.attr = tui.Dim
state.attr = state.attr | tui.Dim
case 3:
state.attr = state.attr | tui.Italic
case 4:
state.attr = tui.Underline
state.attr = state.attr | tui.Underline
case 5:
state.attr = tui.Blink
state.attr = state.attr | tui.Blink
case 7:
state.attr = tui.Reverse
state.attr = state.attr | tui.Reverse
case 0:
init()
default:
@@ -158,6 +169,8 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
}
case 1:
switch num {
case 2:
state256 = 10 // MAGIC
case 5:
state256++
default:
@@ -166,8 +179,20 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 2:
*ptr = tui.Color(num)
state256 = 0
case 10:
*ptr = tui.Color(1<<24) | tui.Color(num<<16)
state256++
case 11:
*ptr = *ptr | tui.Color(num<<8)
state256++
case 12:
*ptr = *ptr | tui.Color(num)
state256 = 0
}
}
}
if state256 > 0 {
*ptr = -1
}
return state
}

View File

@@ -26,7 +26,7 @@ func TestExtractColor(t *testing.T) {
output, ansiOffsets, newState := extractColor(src, state, nil)
state = newState
if output != "hello world" {
t.Errorf("Invalid output: {}", output)
t.Errorf("Invalid output: %s %s", output, []rune(output))
}
fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets, state)
@@ -56,7 +56,7 @@ func TestExtractColor(t *testing.T) {
})
state = nil
src = "\x1b[1mhello \x1b[mworld"
src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()

View File

@@ -8,7 +8,7 @@ import (
const (
// Current version
version = "0.15.6"
version = "0.15.9"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -57,6 +57,7 @@ const usage = `usage: fzf [options]
--ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--no-bold Do not use bold text
History
--history=FILE History file
@@ -144,6 +145,7 @@ type Options struct {
Mouse bool
Theme *tui.ColorTheme
Black bool
Bold bool
Reverse bool
Cycle bool
Hscroll bool
@@ -189,6 +191,7 @@ func defaultOptions() *Options {
Mouse: true,
Theme: tui.EmptyTheme(),
Black: false,
Bold: true,
Reverse: false,
Cycle: false,
Hscroll: true,
@@ -327,6 +330,10 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
func parseAlgo(str string) algo.Algo {
switch str {
case "v1":
@@ -403,11 +410,17 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.DoubleClick
case "f10":
chord = tui.F10
case "f11":
chord = tui.F11
case "f12":
chord = tui.F12
default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) {
chord = tui.Alt0 + int(lkey[4]) - '0'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = tui.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 {
@@ -910,6 +923,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Black = true
case "--no-black":
opts.Black = false
case "--bold":
opts.Bold = true
case "--no-bold":
opts.Bold = false
case "--reverse":
opts.Reverse = true
case "--no-reverse":

View File

@@ -147,19 +147,21 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = theme.Current
} else {
fg = theme.Fg
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = theme.DarkBg
} else {
bg = theme.Bg
if theme != nil {
if fg == -1 {
if current {
fg = theme.Current
} else {
fg = theme.Fg
}
}
if bg == -1 {
if current {
bg = theme.DarkBg
} else {
bg = theme.Bg
}
}
}
colors = append(colors, colorOffset{

View File

@@ -70,6 +70,7 @@ type Terminal struct {
header0 []string
ansi bool
margin [4]sizeSpec
strong tui.Attr
window *tui.Window
bwindow *tui.Window
pwindow *tui.Window
@@ -189,6 +190,7 @@ const (
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[tui.Invalid] = actInvalid
keymap[tui.Resize] = actClearScreen
keymap[tui.CtrlA] = actBeginningOfLine
keymap[tui.CtrlB] = actBackwardChar
keymap[tui.CtrlC] = actAbort
@@ -256,6 +258,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if len(opts.Preview.command) > 0 {
previewBox = util.NewEventBox()
}
strongAttr := tui.Bold
if !opts.Bold {
strongAttr = tui.AttrRegular
}
return &Terminal{
initDelay: delay,
inlineInfo: opts.InlineInfo,
@@ -279,6 +285,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
strong: strongAttr,
cycle: opts.Cycle,
header: header,
header0: header,
@@ -532,24 +539,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt)
t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input))
t.window.CPrint(tui.ColPrompt, t.strong, t.prompt)
t.window.CPrint(tui.ColNormal, t.strong, string(t.input))
}
func (t *Terminal) printInfo() {
if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading {
t.window.CPrint(tui.ColSpinner, tui.Bold, " < ")
t.window.CPrint(tui.ColSpinner, t.strong, " < ")
} else {
t.window.CPrint(tui.ColPrompt, tui.Bold, " < ")
t.window.CPrint(tui.ColPrompt, t.strong, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx])
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
}
t.move(1, 2, false)
}
@@ -627,17 +634,17 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current {
label = ">"
}
t.window.CPrint(tui.ColCursor, tui.Bold, label)
t.window.CPrint(tui.ColCursor, t.strong, label)
if current {
if selected {
t.window.CPrint(tui.ColSelected, tui.Bold, ">")
t.window.CPrint(tui.ColSelected, t.strong, ">")
} else {
t.window.CPrint(tui.ColCurrent, tui.Bold, " ")
t.window.CPrint(tui.ColCurrent, t.strong, " ")
}
t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true)
t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true)
} else {
if selected {
t.window.CPrint(tui.ColSelected, tui.Bold, ">")
t.window.CPrint(tui.ColSelected, t.strong, ">")
} else {
t.window.Print(" ")
}
@@ -826,7 +833,7 @@ func (t *Terminal) printPreview() {
}
return t.pwindow.Fill(str)
})
if t.previewer.offset > 0 {
if t.previewer.lines > t.pwindow.Height {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
t.pwindow.Move(0, t.pwindow.Width-len(offset))
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)

View File

@@ -10,8 +10,12 @@ package tui
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
SCREEN *c_newterm () {
return newterm(NULL, stderr, stdin);
FILE* c_tty() {
return fopen("/dev/tty", "r");
}
SCREEN* c_newterm(FILE* tty) {
return newterm(NULL, stderr, tty);
}
*/
import "C"
@@ -19,24 +23,26 @@ import "C"
import (
"fmt"
"os"
"strconv"
"strings"
"syscall"
"time"
"unicode/utf8"
)
type ColorPair int16
type Attr C.int
type Attr C.uint
type WindowImpl C.WINDOW
const (
Bold = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
Bold Attr = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
var Italic Attr = C.A_VERTICAL << 1 // FIXME
const (
AttrRegular Attr = 0
)
@@ -59,14 +65,16 @@ const (
)
var (
_in *os.File
_screen *C.SCREEN
_colorMap map[int]ColorPair
_colorFn func(ColorPair, Attr) C.int
_colorFn func(ColorPair, Attr) (C.short, C.int)
)
func init() {
_colorMap = make(map[int]ColorPair)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL
}
}
func (a Attr) Merge(b Attr) Attr {
@@ -81,18 +89,13 @@ func DefaultTheme() *ColorTheme {
}
func Init(theme *ColorTheme, 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()))
}
C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.c_newterm()
tty := C.c_tty()
if tty == nil {
fmt.Println("Failed to open /dev/tty")
os.Exit(2)
}
_screen = C.c_newterm(tty)
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2)
@@ -100,9 +103,22 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.set_term(_screen)
if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
}
C.noecho()
C.raw() // stty dsusp undef
C.nonl()
C.keypad(C.stdscr, true)
delay := 50
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
C.set_escdelay(C.int(delay))
_color = theme != nil
if _color {
@@ -114,6 +130,13 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} else {
_colorFn = attrMono
}
C.nodelay(C.stdscr, true)
ch := C.getch()
if ch != C.ERR {
C.ungetch(ch)
}
C.nodelay(C.stdscr, false)
}
func initPairs(theme *ColorTheme) {
@@ -153,10 +176,12 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
}
if border {
attr := _colorFn(ColBorder, 0)
pair, attr := _colorFn(ColBorder, 0)
C.wcolor_set(win, pair, nil)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
C.wcolor_set(win, 0, nil)
}
return &Window{
@@ -168,21 +193,15 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window {
}
}
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 attrColored(pair ColorPair, a Attr) (C.short, C.int) {
return C.short(pair), C.int(a)
}
func attrMono(pair ColorPair, a Attr) C.int {
func attrMono(pair ColorPair, a Attr) (C.short, C.int) {
var attr C.int
switch pair {
case ColCurrent:
if C.int(a)&C.A_BOLD == C.A_BOLD {
attr = C.A_REVERSE
}
attr = C.A_REVERSE
case ColMatch:
attr = C.A_UNDERLINE
case ColCurrentMatch:
@@ -191,7 +210,7 @@ func attrMono(pair ColorPair, a Attr) C.int {
if C.int(a)&C.A_BOLD == C.A_BOLD {
attr = attr | C.A_BOLD
}
return attr
return 0, attr
}
func MaxX() int {
@@ -232,11 +251,13 @@ func (w *Window) Print(text string) {
}, text)))
}
func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
attr := _colorFn(pair, a)
C.wattron(w.win(), attr)
func (w *Window) CPrint(pair ColorPair, attr Attr, text string) {
p, a := _colorFn(pair, attr)
C.wcolor_set(w.win(), p, nil)
C.wattron(w.win(), a)
w.Print(text)
C.wattroff(w.win(), attr)
C.wattroff(w.win(), a)
C.wcolor_set(w.win(), 0, nil)
}
func Clear() {
@@ -256,11 +277,13 @@ 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)
func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool {
pair := PairFor(fg, bg)
C.wcolor_set(w.win(), C.short(pair), nil)
C.wattron(w.win(), C.int(attr))
ret := w.Fill(str)
C.wattroff(w.win(), attr)
C.wattroff(w.win(), C.int(attr))
C.wcolor_set(w.win(), 0, nil)
return ret
}
@@ -272,6 +295,10 @@ func RefreshWindows(windows []*Window) {
}
func PairFor(fg Color, bg Color) ColorPair {
// ncurses does not support 24-bit colors
if fg.is24() || bg.is24() {
return ColDefault
}
key := (int(fg) << 8) + int(bg)
if found, prs := _colorMap[key]; prs {
return found
@@ -283,246 +310,169 @@ func PairFor(fg Color, bg Color) ColorPair {
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 {
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))
for {
c = getch(true)
if c == -1 {
if retries > 0 {
retries--
time.Sleep(10 * time.Millisecond)
continue
}
break
func consume(expects ...rune) bool {
for _, r := range expects {
if int(C.getch()) != int(r) {
return false
}
retries = 0
_buf = append(_buf, byte(c))
}
return _buf
return true
}
// 27 (91 79) 77 type x y
func mouseSequence(sz *int) Event {
if len(_buf) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := _buf[3] >= 36
down := _buf[3]%2 == 0
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
}
_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}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
}
return Event{Invalid, 0, nil}
}
func escSequence(sz *int) Event {
if len(_buf) < 2 {
func escSequence() Event {
C.nodelay(C.stdscr, true)
defer func() {
C.nodelay(C.stdscr, false)
}()
c := C.getch()
switch c {
case C.ERR:
return Event{ESC, 0, nil}
}
*sz = 2
switch _buf[1] {
case 13:
case CtrlM:
return Event{AltEnter, 0, nil}
case 32:
return Event{AltSpace, 0, nil}
case 47:
case '/':
return Event{AltSlash, 0, nil}
case 98:
return Event{AltB, 0, nil}
case 100:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
case ' ':
return Event{AltSpace, 0, nil}
case 127, C.KEY_BACKSPACE:
return Event{AltBS, 0, nil}
case 91, 79:
if len(_buf) < 3 {
case '[':
// Bracketed paste mode (printf "\e[?2004h")
// \e[200~ TEXT \e[201~
if consume('2', '0', '0', '~') {
return Event{Invalid, 0, nil}
}
*sz = 3
switch _buf[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{Invalid, 0, nil}
}
*sz = 4
switch _buf[2] {
case 50:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch _buf[3] {
case 126:
return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59:
if len(_buf) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[4] {
case 50:
switch _buf[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
if c >= 'a' && c <= 'z' {
return Event{AltA + int(c) - 'a', 0, nil}
}
if c >= '0' && c <= '9' {
return Event{Alt0 + int(c) - '0', 0, nil}
}
// Don't care. Ignore the rest.
for ; c != C.ERR; c = C.getch() {
}
return Event{Invalid, 0, nil}
}
func GetChar() Event {
if len(_buf) == 0 {
_buf = GetBytes()
}
if len(_buf) == 0 {
panic("Empty _buffer")
}
sz := 1
defer func() {
_buf = _buf[sz:]
}()
switch _buf[0] {
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
c := C.getch()
switch c {
case C.ERR:
return Event{Invalid, 0, nil}
case C.KEY_UP:
return Event{Up, 0, nil}
case C.KEY_DOWN:
return Event{Down, 0, nil}
case C.KEY_LEFT:
return Event{Left, 0, nil}
case C.KEY_RIGHT:
return Event{Right, 0, nil}
case C.KEY_HOME:
return Event{Home, 0, nil}
case C.KEY_END:
return Event{End, 0, nil}
case C.KEY_BACKSPACE:
return Event{BSpace, 0, nil}
case C.KEY_F0 + 1:
return Event{F1, 0, nil}
case C.KEY_F0 + 2:
return Event{F2, 0, nil}
case C.KEY_F0 + 3:
return Event{F3, 0, nil}
case C.KEY_F0 + 4:
return Event{F4, 0, nil}
case C.KEY_F0 + 5:
return Event{F5, 0, nil}
case C.KEY_F0 + 6:
return Event{F6, 0, nil}
case C.KEY_F0 + 7:
return Event{F7, 0, nil}
case C.KEY_F0 + 8:
return Event{F8, 0, nil}
case C.KEY_F0 + 9:
return Event{F9, 0, nil}
case C.KEY_F0 + 10:
return Event{F10, 0, nil}
case C.KEY_F0 + 11:
return Event{F11, 0, nil}
case C.KEY_F0 + 12:
return Event{F12, 0, nil}
case C.KEY_DC:
return Event{Del, 0, nil}
case C.KEY_PPAGE:
return Event{PgUp, 0, nil}
case C.KEY_NPAGE:
return Event{PgDn, 0, nil}
case C.KEY_BTAB:
return Event{BTab, 0, nil}
case C.KEY_ENTER:
return Event{CtrlM, 0, nil}
case C.KEY_SLEFT:
return Event{SLeft, 0, nil}
case C.KEY_SRIGHT:
return Event{SRight, 0, nil}
case C.KEY_MOUSE:
var me C.MEVENT
if C.getmouse(&me) != C.ERR {
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
x := int(me.x)
y := int(me.y)
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
_prevDownTime = now
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
}
}
return Event{Invalid, 0, nil}
case C.KEY_RESIZE:
return Event{Resize, 0, nil}
case ESC:
return escSequence()
case 127:
return Event{BSpace, 0, nil}
case ESC:
return escSequence(&sz)
}
// CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ {
return Event{int(c), 0, nil}
}
// CTRL-A ~ CTRL-Z
if _buf[0] <= CtrlZ {
return Event{int(_buf[0]), 0, nil}
// Multi-byte character
buffer := []byte{byte(c)}
for {
r, _ := utf8.DecodeRune(buffer)
if r != utf8.RuneError {
return Event{Rune, r, nil}
}
c := C.getch()
if c == C.ERR {
break
}
if c >= C.KEY_CODE_YES {
C.ungetch(c)
break
}
buffer = append(buffer, byte(c))
}
r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil}
return Event{Invalid, 0, nil}
}

View File

@@ -11,8 +11,9 @@ import (
"runtime"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/encoding"
// https://github.com/gdamore/tcell/pull/135
"github.com/junegunn/tcell"
"github.com/junegunn/tcell/encoding"
"github.com/junegunn/go-runewidth"
)
@@ -43,11 +44,12 @@ type WindowTcell struct {
type WindowImpl WindowTcell
const (
Bold = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
Bold Attr = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
Italic = Attr(tcell.AttrNone) // Not supported
)
const (
@@ -183,7 +185,7 @@ func GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
return Event{Invalid, 0, nil}
return Event{Resize, 0, nil}
// process mouse events:
case *tcell.EventMouse:
@@ -207,8 +209,8 @@ func GetChar() Event {
_clickY = append(_clickY, x)
} else {
_clickY = []int{x}
_prevDownTime = now
}
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
@@ -326,9 +328,9 @@ func GetChar() Event {
case tcell.KeyF10:
return Event{F10, 0, nil}
case tcell.KeyF11:
return Event{Invalid, 0, nil}
return Event{F11, 0, nil}
case tcell.KeyF12:
return Event{Invalid, 0, nil}
return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space:
case tcell.KeyRune:
@@ -343,6 +345,9 @@ func GetChar() Event {
if r >= 'a' && r <= 'z' {
return Event{AltA + int(r) - 'a', 0, nil}
}
if r >= '0' && r <= '9' {
return Event{Alt0 + int(r) - '0', 0, nil}
}
}
return Event{Rune, r, nil}

View File

@@ -37,6 +37,7 @@ const (
ESC
Invalid
Resize
Mouse
DoubleClick
@@ -67,18 +68,24 @@ const (
F8
F9
F10
F11
F12
AltEnter
AltSpace
AltSlash
AltBS
AltA
Alt0
)
const ( // Reset iota
AltA = Alt0 + 'a' - '0' + iota
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
@@ -86,7 +93,11 @@ const (
doubleClickDuration = 500 * time.Millisecond
)
type Color int16
type Color int32
func (c Color) is24() bool {
return c > 0 && (c&(1<<24)) > 0
}
const (
colUndefined Color = -2
@@ -136,7 +147,6 @@ type MouseEvent struct {
}
var (
_buf []byte
_color bool
_prevDownTime time.Time
_clickY []int