diff options
| author | Junegunn Choi <junegunn.c@gmail.com> | 2017-01-08 01:30:31 +0900 |
|---|---|---|
| committer | Junegunn Choi <junegunn.c@gmail.com> | 2017-01-08 02:09:56 +0900 |
| commit | 1448d631a7c72905f62dbb343a8f231a1c3cc52c (patch) | |
| tree | 05abfedd2a0777c2640c8259267d3ad855879dfe /src | |
| parent | fd137a9e875ba1fd9feed4903e102951f8098c33 (diff) | |
| download | fzf-1448d631a7c72905f62dbb343a8f231a1c3cc52c.tar.gz | |
Add --height option
Diffstat (limited to 'src')
| -rw-r--r-- | src/options.go | 18 | ||||
| -rw-r--r-- | src/result.go | 2 | ||||
| -rw-r--r-- | src/result_test.go | 15 | ||||
| -rw-r--r-- | src/terminal.go | 225 | ||||
| -rw-r--r-- | src/tui/light.go | 764 | ||||
| -rw-r--r-- | src/tui/ncurses.go | 250 | ||||
| -rw-r--r-- | src/tui/tcell.go | 273 | ||||
| -rw-r--r-- | src/tui/tui.go | 170 | ||||
| -rw-r--r-- | src/tui/tui_test.go | 14 | ||||
| -rw-r--r-- | src/util/util.go | 16 | ||||
| -rw-r--r-- | src/util/util_unix.go | 6 | ||||
| -rw-r--r-- | src/util/util_windows.go | 6 |
12 files changed, 1347 insertions, 412 deletions
diff --git a/src/options.go b/src/options.go index 6fd3f6c6..0c6661f3 100644 --- a/src/options.go +++ b/src/options.go @@ -10,6 +10,7 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" "github.com/junegunn/go-shellwords" ) @@ -46,6 +47,8 @@ const usage = `usage: fzf [options] --jump-labels=CHARS Label characters for jump and jump-accept Layout + --height=HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen --reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query @@ -147,6 +150,7 @@ type Options struct { Theme *tui.ColorTheme Black bool Bold bool + Height sizeSpec Reverse bool Cycle bool Hscroll bool @@ -760,6 +764,14 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { return sizeSpec{val, percent} } +func parseHeight(str string) sizeSpec { + if util.IsWindows() { + errorExit("--height options is currently not supported on Windows") + } + size := parseSize(str, 100, "height") + return size +} + func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight @@ -1003,6 +1015,10 @@ func parseOptions(opts *Options, allArgs []string) { case "--preview-window": parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) + case "--height": + opts.Height = parseHeight(nextString(allArgs, &i, "height required: [HEIGHT[%]]")) + case "--no-height": + opts.Height = sizeSpec{} case "--no-margin": opts.Margin = defaultMargin() case "--margin": @@ -1029,6 +1045,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "--height="); match { + opts.Height = parseHeight(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/result.go b/src/result.go index e2d7c755..3d79176f 100644 --- a/src/result.go +++ b/src/result.go @@ -166,7 +166,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: tui.PairFor(fg, bg), + color: tui.NewColorPair(fg, bg), attr: ansi.color.attr.Merge(attr)}) } } diff --git a/src/result_test.go b/src/result_test.go index 15b1bdbb..0e91fc87 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -105,7 +105,8 @@ func TestColorOffset(t *testing.T) { 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}] - colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true) + pair := tui.NewColorPair(99, 199) + colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true) assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { var attr tui.Attr if bold { @@ -116,10 +117,10 @@ func TestColorOffset(t *testing.T) { t.Error(o) } } - assert(0, 0, 5, tui.ColUser, false) - assert(1, 5, 15, 99, false) - assert(2, 15, 20, tui.ColUser, false) - assert(3, 22, 25, tui.ColUser+1, true) - assert(4, 25, 35, 99, false) - assert(5, 35, 40, tui.ColUser+2, true) + assert(0, 0, 5, tui.NewColorPair(1, 5), false) + assert(1, 5, 15, pair, false) + assert(2, 15, 20, tui.NewColorPair(1, 5), false) + assert(3, 22, 25, tui.NewColorPair(2, 6), true) + assert(4, 25, 35, pair, false) + assert(5, 35, 40, tui.NewColorPair(4, 8), true) } diff --git a/src/terminal.go b/src/terminal.go index 5b482b06..80029236 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -15,8 +15,6 @@ import ( "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" - - "github.com/junegunn/go-runewidth" ) // import "github.com/pkg/profile" @@ -42,6 +40,14 @@ type previewer struct { enabled bool } +type itemLine struct { + current bool + label string + result Result +} + +var emptyLine = itemLine{} + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -69,11 +75,12 @@ type Terminal struct { header []string header0 []string ansi bool + tabstop int margin [4]sizeSpec strong tui.Attr - window *tui.Window - bwindow *tui.Window - pwindow *tui.Window + window tui.Window + bwindow tui.Window + pwindow tui.Window count int progress int reading bool @@ -89,10 +96,12 @@ type Terminal struct { eventBox *util.EventBox mutex sync.Mutex initFunc func() + prevLines []itemLine suppress bool startChan chan bool slab *util.Slab theme *tui.ColorTheme + tui tui.Renderer } type selectedItem struct { @@ -115,7 +124,6 @@ func (a byTimeOrder) Less(i, j int) bool { } var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} -var _runeWidths = make(map[rune]int) var _tabStop int const ( @@ -247,7 +255,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { header = reverseStringArray(opts.Header) } - _tabStop = opts.Tabstop var delay time.Duration if opts.Tac { delay = initialDelayTac @@ -262,6 +269,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if !opts.Bold { strongAttr = tui.AttrRegular } + var renderer tui.Renderer + if opts.Height.size > 0 { + maxHeightFunc := func(termHeight int) int { + var maxHeight int + if opts.Height.percent { + maxHeight = int(opts.Height.size * float64(termHeight) / 100.0) + } else { + maxHeight = util.Min(int(opts.Height.size), termHeight) + } + if opts.InlineInfo { + return util.Max(maxHeight, 3) + } + return util.Max(maxHeight, 4) + } + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) + } else { + renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -290,6 +315,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header: header, header0: header, ansi: opts.Ansi, + tabstop: opts.Tabstop, reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, @@ -306,9 +332,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), - initFunc: func() { - tui.Init(opts.Theme, opts.Black, opts.Mouse) - }} + tui: renderer, + initFunc: func() { renderer.Init() }} } // Input returns current query string @@ -401,22 +426,10 @@ func (t *Terminal) sortSelected() []selectedItem { return sels } -func runeWidth(r rune, prefixWidth int) int { - if r == '\t' { - return _tabStop - prefixWidth%_tabStop - } else if w, found := _runeWidths[r]; found { - return w - } else { - w := runewidth.RuneWidth(r) - _runeWidths[r] = w - return w - } -} - -func displayWidth(runes []rune) int { +func (t *Terminal) displayWidth(runes []rune) int { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) } return l } @@ -437,9 +450,10 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int { } func (t *Terminal) resizeWindows() { - screenWidth := tui.MaxX() - screenHeight := tui.MaxY() + screenWidth := t.tui.MaxX() + screenHeight := t.tui.MaxY() marginInt := [4]int{} + t.prevLines = make([]itemLine, screenHeight) for idx, sizeSpec := range t.margin { if sizeSpec.percent { var max float64 @@ -487,40 +501,40 @@ func (t *Terminal) resizeWindows() { height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = tui.NewWindow(y, x, w, h, true) + t.bwindow = t.tui.NewWindow(y, x, w, h, true) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one // column larger than the desired value. - if !t.preview.wrap && tui.DoesAutoWrap() { + if !t.preview.wrap && t.tui.DoesAutoWrap() { pwidth += 1 } - t.pwindow = tui.NewWindow(y+1, x+2, pwidth, h-2, false) + t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, @@ -530,7 +544,7 @@ func (t *Terminal) resizeWindows() { func (t *Terminal) move(y int, x int, clear bool) { if !t.reverse { - y = t.window.Height - y - 1 + y = t.window.Height() - y - 1 } if clear { @@ -541,7 +555,7 @@ func (t *Terminal) move(y int, x int, clear bool) { } func (t *Terminal) placeCursor() { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input[:t.cx]), false) } func (t *Terminal) printPrompt() { @@ -552,7 +566,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { if t.inlineInfo { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true) if t.reading { t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { @@ -589,7 +603,7 @@ func (t *Terminal) printHeader() { if len(t.header) == 0 { return } - max := t.window.Height + max := t.window.Height() var state *ansiState for idx, lineStr := range t.header { line := idx + 2 @@ -616,19 +630,25 @@ func (t *Terminal) printList() { maxy := t.maxItems() count := t.merger.Length() - t.offset - for i := 0; i < maxy; i++ { + for j := 0; j < maxy; j++ { + i := j + if !t.reverse { + i = maxy - 1 - j + } line := i + 2 + len(t.header) if t.inlineInfo { line-- } - t.move(line, 0, true) if i < count { - t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset) + } else if t.prevLines[i] != emptyLine { + t.prevLines[i] = emptyLine + t.move(line, 0, true) } } } -func (t *Terminal) printItem(result *Result, i int, current bool) { +func (t *Terminal) printItem(result *Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] label := " " @@ -641,6 +661,15 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } + + // Avoid unnecessary redraw + newLine := itemLine{current, label, *result} + if t.prevLines[i] == newLine { + return + } + t.prevLines[i] = newLine + + t.move(line, 0, true) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -659,11 +688,11 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } } -func trimRight(runes []rune, width int) ([]rune, int) { +func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { // We start from the beginning to handle tab characters l := 0 for idx, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > width { return runes[:idx], len(runes) - idx } @@ -671,10 +700,10 @@ func trimRight(runes []rune, width int) ([]rune, int) { return runes, 0 } -func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { +func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { l := 0 for _, r := range runes { - l += runeWidth(r, l+prefixWidth) + l += util.RuneWidth(r, l+prefixWidth, t.tabstop) if l > limit { // Early exit return l @@ -683,27 +712,27 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { return l } -func trimLeft(runes []rune, width int) ([]rune, int32) { +func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { if len(runes) > maxDisplayWidthCalc && len(runes) > width { trimmed := len(runes) - width return runes[trimmed:], int32(trimmed) } - currentWidth := displayWidth(runes) + currentWidth := t.displayWidth(runes) var trimmed int32 for currentWidth > width && len(runes) > 0 { runes = runes[1:] trimmed++ - currentWidth = displayWidthWithLimit(runes, 2, width) + currentWidth = t.displayWidthWithLimit(runes, 2, width) } return runes, trimmed } -func overflow(runes []rune, max int) bool { +func (t *Terminal) overflow(runes []rune, max int) bool { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > max { return true } @@ -737,22 +766,22 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) - maxWidth := t.window.Width - 3 + maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - if overflow(text, maxWidth) { + if t.overflow(text, maxWidth) { if t.hscroll { // Stri.. - if !overflow(text[:maxe], maxWidth-2) { - text, _ = trimRight(text, maxWidth-2) + if !t.overflow(text[:maxe], maxWidth-2) { + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) } else { // Stri.. - if overflow(text[maxe:], 2) { + if t.overflow(text[maxe:], 2) { text = append(text[:maxe], []rune("..")...) } // ..ri.. var diff int32 - text, diff = trimLeft(text, maxWidth-2) + text, diff = t.trimLeft(text, maxWidth-2) // Transform offsets for idx, offset := range offsets { @@ -766,7 +795,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo text = append([]rune(".."), text...) } } else { - text, _ = trimRight(text, maxWidth-2) + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) for idx, offset := range offsets { @@ -784,11 +813,11 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo b := util.Constrain32(offset.offset[0], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset) - substr, prefixWidth = processTabs(text[index:b], prefixWidth) + substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) t.window.CPrint(col1, attr, substr) if b < e { - substr, prefixWidth = processTabs(text[b:e], prefixWidth) + substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) t.window.CPrint(offset.color, offset.attr, substr) } @@ -798,7 +827,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } } if index < maxOffset { - substr, _ = processTabs(text[index:], prefixWidth) + substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } } @@ -835,38 +864,44 @@ func (t *Terminal) printPreview() { return true } } - if !t.preview.wrap { - lines := strings.Split(str, "\n") - for i, line := range lines { - limit := t.pwindow.Width - if tui.DoesAutoWrap() { - limit -= 1 - } - if i == 0 { - limit -= t.pwindow.X() - } - trimmed, _ := trimRight([]rune(line), limit) - lines[i], _ = processTabs(trimmed, 0) + lines := strings.Split(str, "\n") + for i, line := range lines { + limit := t.pwindow.Width() + if t.tui.DoesAutoWrap() { + limit -= 1 } + if i == 0 { + limit -= t.pwindow.X() + } + trimmed := []rune(line) + if !t.preview.wrap { + trimmed, _ = t.trimRight(trimmed, limit) + } + lines[i], _ = t.processTabs(trimmed, 0) str = strings.Join(lines, "\n") } if ansi != nil && ansi.colored() { - return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) + return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } return t.pwindow.Fill(str) }) - if t.previewer.lines > t.pwindow.Height { + t.pwindow.FinishFill() + 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)) + pos := t.pwindow.Width() - len(offset) + if t.tui.DoesAutoWrap() { + pos -= 1 + } + t.pwindow.Move(0, pos) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } -func processTabs(runes []rune, prefixWidth int) (string, int) { +func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { var strbuf bytes.Buffer l := prefixWidth for _, r := range runes { - w := runeWidth(r, l) + w := util.RuneWidth(r, l, t.tabstop) l += w if r == '\t' { strbuf.WriteString(strings.Repeat(" ", w)) @@ -889,9 +924,9 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { if t.isPreviewEnabled() { - tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window}) + t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) } else { - tui.RefreshWindows([]*tui.Window{t.window}) + t.tui.RefreshWindows([]tui.Window{t.window}) } } } @@ -1013,9 +1048,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - tui.Pause() + t.tui.Pause() cmd.Run() - if tui.Resume() { + if t.tui.Resume() { t.printAll() } t.refresh() @@ -1162,11 +1197,11 @@ func (t *Terminal) Loop() { case reqRefresh: t.suppress = false case reqRedraw: - tui.Clear() - tui.Refresh() + t.tui.Clear() + t.tui.Refresh() t.printAll() case reqClose: - tui.Close() + t.tui.Close() if t.output() { exit(exitOk) } @@ -1179,11 +1214,11 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - tui.Close() + t.tui.Close() t.printer(string(t.input)) exit(exitOk) case reqQuit: - tui.Close() + t.tui.Close() exit(exitInterrupt) } } @@ -1196,7 +1231,7 @@ func (t *Terminal) Loop() { looping := true for looping { - event := tui.GetChar() + event := t.tui.GetChar() t.mutex.Lock() previousInput := t.input @@ -1288,11 +1323,11 @@ func (t *Terminal) Loop() { } case actPreviewPageUp: if t.isPreviewEnabled() { - scrollPreview(-t.pwindow.Height) + scrollPreview(-t.pwindow.Height()) } case actPreviewPageDown: if t.isPreviewEnabled() { - scrollPreview(t.pwindow.Height) + scrollPreview(t.pwindow.Height()) } case actBeginningOfLine: t.cx = 0 @@ -1466,11 +1501,11 @@ func (t *Terminal) Loop() { scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { - mx -= t.window.Left - my -= t.window.Top - mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) + mx -= t.window.Left() + my -= t.window.Top() + mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input)) if !t.reverse { - my = t.window.Height - my - 1 + my = t.window.Height() - my - 1 } min := 2 + len(t.header) if t.inlineInfo { @@ -1582,7 +1617,7 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { - max := t.window.Height - 2 - len(t.header) + max := t.window.Height() - 2 - len(t.header) if t.inlineInfo { max++ } diff --git a/src/tui/light.go b/src/tui/light.go new file mode 100644 index 00000000..1273c8fb --- /dev/null +++ b/src/tui/light.go @@ -0,0 +1,764 @@ +package tui + +import ( + "fmt" + "os" + "strconv" + "strings" + "syscall" + "time" + "unicode/utf8" + + "github.com/junegunn/fzf/src/util" +) + +const ( + defaultWidth = 80 + defaultHeight = 24 + + escPollInterval = 5 +) + +func openTtyIn() *os.File { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + return in +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderr(str string) { + bytes := []byte(str) + runes := []rune{} + for len(bytes) > 0 { + r, sz := utf8.DecodeRune(bytes) + if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 { + runes = append(runes, '?') + } else { + runes = append(runes, r) + } + bytes = bytes[sz:] + } + r.queued += string(runes) +} + +func (r *LightRenderer) csi(code string) { + r.stderr("\x1b[" + code) +} + +func (r *LightRenderer) flush() { + if len(r.queued) > 0 { + fmt.Fprint(os.Stderr, r.queued) + r.queued = "" + } +} + +// Light renderer +type LightRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int + ttyin *os.File + buffer []byte + ostty string + width int + height int + yoffset int + tabstop int + escDelay int + upOneLine bool + queued string + maxHeightFunc func(int) int +} + +type LightWindow struct { + renderer *LightRenderer + colored bool + border bool + top int + left int + width int + height int + posx int + posy int + tabstop int + bg Color +} + +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer { + r := LightRenderer{ + theme: theme, + forceBlack: forceBlack, + mouse: mouse, + ttyin: openTtyIn(), + yoffset: -1, + tabstop: tabstop, + upOneLine: false, + maxHeightFunc: maxHeightFunc} + return &r +} + +func (r *LightRenderer) defaultTheme() *ColorTheme { + colors, err := util.ExecCommand("tput colors").Output() + if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { + return Dark256 + } + return Default16 +} + +func stty(cmd string) string { + out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output() + if err != nil { + // Not sure how to handle this + panic("stty " + cmd + ": " + err.Error()) + } + return strings.TrimSpace(string(out)) +} + +func (r *LightRenderer) findOffset() (row int, col int) { + r.csi("6n") + r.flush() + bytes := r.getBytesInternal([]byte{}) + + // ^[[*;*R + if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { + nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") + if len(nums) == 2 { + return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1 + } + return -1, -1 + } + + // No idea + return -1, -1 +} + +func repeat(s string, times int) string { + if times > 0 { + return strings.Repeat(s, times) + } + return "" +} + +func atoi(s string, defaultValue int) int { + value, err := strconv.Atoi(s) + if err != nil { + return defaultValue + } + return value +} + +func (r *LightRenderer) Init() { + delay := 100 + delayEnv := os.Getenv("ESCDELAY") + if len(delayEnv) > 0 { + num, err := strconv.Atoi(delayEnv) + if err == nil && num >= 0 { + delay = num + } + } + r.escDelay = delay + + r.ostty = stty("-g") + stty("raw") + r.updateTerminalSize() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + + _, x := r.findOffset() + if x > 0 { + r.upOneLine = true + r.stderr("\n") + } + for i := 1; i < r.MaxY(); i++ { + r.stderr("\n") + r.csi("G") + } + + if r.mouse { + r.csi("?1000h") + } + r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) + r.csi("G") + r.csi("s") + r.yoffset, _ = r.findOffset() +} + +func (r *LightRenderer) updateTerminalSize() { + sizes := strings.Split(stty("size"), " ") + if len(sizes) < 2 { + r.width = defaultWidth + r.height = r.maxHeightFunc(defaultHeight) + } else { + r.width = atoi(sizes[1], defaultWidth) + r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight)) + } +} + +func (r *LightRenderer) getch(nonblock bool) int { + b := make([]byte, 1) + util.SetNonblock(r.ttyin, nonblock) + _, err := r.ttyin.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func (r *LightRenderer) getBytes() []byte { + return r.getBytesInternal(r.buffer) +} + +func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { + c := r.getch(false) + + retries := 0 + if c == ESC { + retries = r.escDelay / escPollInterval + } + buffer = append(buffer, byte(c)) + + for { + c = r.getch(true) + if c == -1 { + if retries > 0 { + retries-- + time.Sleep(escPollInterval * time.Millisecond) + continue + } + break + } + retries = 0 + buffer = append(buffer, byte(c)) + } + + return buffer +} + +func (r *LightRenderer) GetChar() Event { + if len(r.buffer) == 0 { + r.buffer = r.getBytes() + } + if len(r.buffer) == 0 { + panic("Empty buffer") + } + + sz := 1 + defer func() { + r.buffer = r.buffer[sz:] + }() + + switch r.buffer[0] { + case CtrlC: + return Event{CtrlC, 0, nil} + case CtrlG: + return Event{CtrlG, 0, nil} + case CtrlQ: + return Event{CtrlQ, 0, nil} + case 127: + return Event{BSpace, 0, nil} + case ESC: + ev := r.escSequence(&sz) + // Second chance + if ev.Type == Invalid { + r.buffer = r.getBytes() + ev = r.escSequence(&sz) + } + return ev + } + + // CTRL-A ~ CTRL-Z + if r.buffer[0] <= CtrlZ { + return Event{int(r.buffer[0]), 0, nil} + } + char, rsz := utf8.DecodeRune(r.buffer) + if char == utf8.RuneError { + return Event{ESC, 0, nil} + } + sz = rsz + return Event{Rune, char, nil} +} + +func (r *LightRenderer) escSequence(sz *int) Event { + if len(r.buffer) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch r.buffer[1] { + case 13: + return Event{AltEnter, 0, nil} + case 32: + return Event{AltSpace, 0, nil} + case 47: + 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: + return Event{AltBS, 0, nil} + case 91, 79: + if len(r.buffer) < 3 { + return Event{Invalid, 0, nil} + } + *sz = 3 + switch r.buffer[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 r.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(r.buffer) < 4 { + return Event{Invalid, 0, nil} + } + *sz = 4 + switch r.buffer[2] { + case 50: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[3] { + case 48: + return Event{F9, 0, nil} + case 49: + return Event{F10, 0, nil} + case 51: + return Event{F11, 0, nil} + case 52: + return Event{F12, 0, nil} + } + } + // Bracketed paste mode \e[200~ / \e[201 + if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[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 r.buffer[3] { + case 126: + return Event{Home, 0, nil} + case 53, 55, 56, 57: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[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(r.buffer) != 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[4] { + case 50: + switch r.buffer[5] { + case 68: + return Event{Home, 0, nil} + case 67: + return Event{End, 0, nil} + } + case 53: + switch r.buffer[5] { + case 68: + return Event{SLeft, 0, nil} + case 67: + return Event{SRight, 0, nil} + } + } // r.buffer[4] + } // r.buffer[3] + } // r.buffer[2] + } // r.buffer[2] + } // r.buffer[1] + if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { + return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) mouseSequence(sz *int) Event { + if len(r.buffer) < 6 || r.yoffset < 0 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := r.buffer[3] >= 36 + down := r.buffer[3]%2 == 0 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + double := false + if down { + now := time.Now() + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) + } else { + r.clickY = []int{y} + } + r.prevDownTime = now + } else { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.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 := r.buffer[3] >= 100 + s := 1 - int(r.buffer[3]%2)*2 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) Pause() { + stty(fmt.Sprintf("%q", r.ostty)) + r.csi("?1049h") + r.flush() +} + +func (r *LightRenderer) Resume() bool { + stty("raw") + r.csi("?1049l") + r.flush() + // Should redraw + return true +} + +func (r *LightRenderer) Clear() { + r.csi("u") + r.csi("J") + r.flush() +} + +func (r *LightRenderer) RefreshWindows(windows []Window) { + r.flush() +} + +func (r *LightRenderer) Refresh() { + r.updateTerminalSize() +} + +func (r *LightRenderer) Close() { + r.csi("u") + r.csi("J") + if r.mouse { + r.csi("?1000l") + } + if r.upOneLine { + r.csi("A") + } + r.flush() + stty(fmt.Sprintf("%q", r.ostty)) +} + +func (r *LightRenderer) MaxX() int { + return r.width +} + +func (r *LightRenderer) MaxY() int { + return r.height +} + +func (r *LightRenderer) DoesAutoWrap() bool { + return true +} + +func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { + w := &LightWindow{ + renderer: r, + colored: r.theme != nil, + border: border, + top: top, + left: left, + width: width, + height: height, + tabstop: r.tabstop, + bg: colDefault} + if r.theme != nil { + w.bg = r.theme.Bg + } + if w.border { + w.drawBorder() + } + return w +} + +func (w *LightWindow) drawBorder() { + w.Move(0, 0) + w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") + for y := 1; y < w.height-1; y++ { + w.Move(y, 0) + w.CPrint(ColBorder, AttrRegular, "│") + w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2)) + w.CPrint(ColBorder, AttrRegular, "│") + } + w.Move(w.height-1, 0) + w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘") +} + +func (w *LightWindow) csi(code string) { + w.renderer.csi(code) +} + +func (w *LightWindow) stderr(str string) { + w.renderer.stderr(str) +} + +func (w *LightWindow) Top() int { + return w.top +} + +func (w *LightWindow) Left() int { + return w.left +} + +func (w *LightWindow) Width() int { + return w.width +} + +func (w *LightWindow) Height() int { + return w.height +} + +func (w *LightWindow) Refresh() { +} + +func (w *LightWindow) Close() { +} + +func (w *LightWindow) X() int { + return w.posx +} + +func (w *LightWindow) 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 *LightWindow) Move(y int, x int) { + w.posx = x + w.posy = y + + w.csi("u") + y += w.Top() + if y > 0 { + w.csi(fmt.Sprintf("%dB", y)) + } + x += w.Left() + if x > 0 { + w.csi(fmt.Sprintf("%dC", x)) + } +} + +func (w *LightWindow) MoveAndClear(y int, x int) { + w.Move(y, x) + // We should not delete preview window on the right + // csi("K") + w.Print(repeat(" ", w.width-x)) + w.Move(y, x) +} + +func attrCodes(attr Attr) []string { + codes := []string{} + if (attr & Bold) > 0 { + codes = append(codes, "1") + } + if (attr & Dim) > 0 { + codes = append(codes, "2") + } + if (attr & Italic) > 0 { + codes = append(codes, "3") + } + if (attr & Underline) > 0 { + codes = append(codes, "4") + } + if (attr & Blink) > 0 { + codes = append(codes, "5") + } + if (attr & Reverse) > 0 { + codes = append(codes, "7") + } + return codes +} + +func colorCodes(fg Color, bg Color) []string { + codes := []string{} + appendCode := func(c Color, offset int) { + if c == colDefault { + return + } + if c.is24() { + r := (c >> 16) & 0xff + g := (c >> 8) & 0xff + b := (c) & 0xff + codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b)) + } else if c >= colBlack && c <= colWhite { + codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset)) + } else if c > colWhite && c < 16 { + codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8)) + } else if c >= 16 && c < 256 { + codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c)) + } + } + appendCode(fg, 0) + appendCode(bg, 10) + return codes +} + +func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { + codes := append(attrCodes(attr), colorCodes(fg, bg)...) + w.csi(";" + strings.Join(codes, ";") + "m") + return len(codes) > 0 +} + +func (w *LightWindow) Print(text string) { + w.cprint2(colDefault, w.bg, AttrRegular, text) +} + +func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { + if !w.colored { + w.csiColor(colDefault, colDefault, attrFor(pair, attr)) + } else { + w.csiColor(pair.Fg(), pair.Bg(), attr) + } + w.stderr(text) + w.csi("m") +} + +func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { + if w.csiColor(fg, bg, attr) { + defer w.csi("m") + } + w.stderr(text) +} + +type wrappedLine struct { + text string + displayWidth int +} + +func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { + lines := []wrappedLine{} + width := 0 + line := "" + for _, r := range input { + w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1) + width += w + str := string(r) + if r == '\t' { + str = repeat(" ", w) + } + if prefixLength+width <= max { + line += str + } else { + lines = append(lines, wrappedLine{string(line), width - w}) + line = str + prefixLength = 0 + width = util.RuneWidth(r, prefixLength, 8) + } + } + lines = append(lines, wrappedLine{string(line), width}) + return lines +} + +func (w *LightWindow) fill(str string, onMove func()) bool { + allLines := strings.Split(str, "\n") + for i, line := range allLines { + lines := wrapLine(line, w.posx, w.width, w.tabstop) + for j, wl := range lines { + w.stderr(wl.text) + w.posx += wl.displayWidth + if j < len(lines)-1 || i < len(allLines)-1 { + if w.posy+1 >= w.height { + return false + } + w.MoveAndClear(w.posy+1, 0) + onMove() + } + } + } + return true +} + +func (w *LightWindow) setBg() { + if w.bg != colDefault { + w.csiColor(colDefault, w.bg, AttrRegular) + } +} + +func (w *LightWindow) Fill(text string) bool { + w.MoveAndClear(w.posy, w.posx) + w.setBg() + return w.fill(text, w.setBg) +} + +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool { + w.MoveAndClear(w.posy, w.posx) + if bg == colDefault { + bg = w.bg + } + if w.csiColor(fg, bg, attr) { + return w.fill(text, func() { w.csiColor(fg, bg, attr) }) + defer w.csi("m") + } + return w.fill(text, w.setBg) +} + +func (w *LightWindow) FinishFill() { + for y := w.posy + 1; y < w.height; y++ { + w.MoveAndClear(y, 0) + } +} + +func (w *LightWindow) Erase() { + if w.border { + w.drawBorder() + } + // We don't erase the window here to avoid flickering during scroll + w.Move(0, 0) +} diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 7f515b2f..b1606920 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -33,9 +33,39 @@ import ( "unicode/utf8" ) -type ColorPair int16 type Attr C.uint -type WindowImpl C.WINDOW + +type CursesWindow struct { + impl *C.WINDOW + top int + left int + width int + height int +} + +func (w *CursesWindow) Top() int { + return w.top +} + +func (w *CursesWindow) Left() int { + return w.left +} + +func (w *CursesWindow) Width() int { + return w.width +} + +func (w *CursesWindow) Height() int { + return w.height +} + +func (w *CursesWindow) Refresh() { + C.wnoutrefresh(w.impl) +} + +func (w *CursesWindow) FinishFill() { + // NO-OP +} const ( Bold Attr = C.A_BOLD @@ -51,31 +81,14 @@ const ( AttrRegular Attr = 0 ) -// Pallete -const ( - ColDefault ColorPair = iota - ColNormal - ColPrompt - ColMatch - ColCurrent - ColCurrentMatch - ColSpinner - ColInfo - ColCursor - ColSelected - ColHeader - ColBorder - ColUser // Should be the last entry -) - var ( _screen *C.SCREEN - _colorMap map[int]ColorPair + _colorMap map[int]int16 _colorFn func(ColorPair, Attr) (C.short, C.int) ) func init() { - _colorMap = make(map[int]ColorPair) + _colorMap = make(map[int]int16) if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { Italic = C.A_NORMAL } @@ -85,14 +98,14 @@ func (a Attr) Merge(b Attr) Attr { return a | b } -func DefaultTheme() *ColorTheme { +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if C.tigetnum(C.CString("colors")) >= 256 { return Dark256 } return Default16 } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { @@ -105,7 +118,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { os.Exit(2) } C.set_term(_screen) - if mouse { + if r.mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mouseinterval(0) } @@ -124,14 +137,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } C.set_escdelay(C.int(delay)) - _color = theme != nil - if _color { + if r.theme != nil { C.start_color() - InitTheme(theme, black) - initPairs(theme) - C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + initPairs(r.theme) + C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) _colorFn = attrColored } else { + initTheme(r.theme, nil, r.forceBlack) _colorFn = attrMono } @@ -145,39 +158,39 @@ func Init(theme *ColorTheme, black bool, mouse bool) { func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - initPair := func(group ColorPair, fg Color, bg Color) { - C.init_pair(C.short(group), C.short(fg), C.short(bg)) + for _, pair := range []ColorPair{ + ColNormal, + ColPrompt, + ColMatch, + ColCurrent, + ColCurrentMatch, + ColSpinner, + ColInfo, + ColCursor, + ColSelected, + ColHeader, + ColBorder} { + C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg())) } - initPair(ColNormal, theme.Fg, theme.Bg) - initPair(ColPrompt, theme.Prompt, theme.Bg) - initPair(ColMatch, theme.Match, theme.Bg) - initPair(ColCurrent, theme.Current, theme.DarkBg) - initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) - initPair(ColSpinner, theme.Spinner, theme.Bg) - initPair(ColInfo, theme.Info, theme.Bg) - initPair(ColCursor, theme.Cursor, theme.DarkBg) - initPair(ColSelected, theme.Selected, theme.DarkBg) - initPair(ColHeader, theme.Header, theme.Bg) - initPair(ColBorder, theme.Border, theme.Bg) -} - -func Pause() { +} + +func (r *FullscreenRenderer) Pause() { C.endwin() } -func Resume() bool { +func (r *FullscreenRenderer) Resume() bool { return false } -func Close() { +func (r *FullscreenRenderer) Close() { C.endwin() C.delscreen(_screen) } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) 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 r.theme != nil { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) } if border { pair, attr := _colorFn(ColBorder, 0) @@ -188,66 +201,50 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { C.wcolor_set(win, 0, nil) } - return &Window{ - impl: (*WindowImpl)(win), - Top: top, - Left: left, - Width: width, - Height: height, + return &CursesWindow{ + impl: win, + top: top, + left: left, + width: width, + height: height, } } -func attrColored(pair ColorPair, a Attr) (C.short, C.int) { - return C.short(pair), C.int(a) +func attrColored(color ColorPair, a Attr) (C.short, C.int) { + return C.short(color.index()), C.int(a) } -func attrMono(pair ColorPair, a Attr) (C.short, C.int) { - var attr C.int - switch pair { - case ColCurrent: - 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 0, attr +func attrMono(color ColorPair, a Attr) (C.short, C.int) { + return 0, C.int(attrFor(color, a)) } -func MaxX() int { +func (r *FullscreenRenderer) MaxX() int { return int(C.COLS) } -func MaxY() int { +func (r *FullscreenRenderer) 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 *CursesWindow) Close() { + C.delwin(w.impl) } -func (w *Window) Enclose(y int, x int) bool { - return bool(C.wenclose(w.win(), C.int(y), C.int(x))) +func (w *CursesWindow) Enclose(y int, x int) bool { + return bool(C.wenclose(w.impl, 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 *CursesWindow) Move(y int, x int) { + C.wmove(w.impl, C.int(y), C.int(x)) } -func (w *Window) MoveAndClear(y int, x int) { +func (w *CursesWindow) MoveAndClear(y int, x int) { w.Move(y, x) - C.wclrtoeol(w.win()) + C.wclrtoeol(w.impl) } -func (w *Window) Print(text string) { - C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { +func (w *CursesWindow) Print(text string) { + C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } @@ -255,69 +252,74 @@ func (w *Window) Print(text string) { }, text))) } -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) +func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) { + p, a := _colorFn(color, attr) + C.wcolor_set(w.impl, p, nil) + C.wattron(w.impl, a) w.Print(text) - C.wattroff(w.win(), a) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, a) + C.wcolor_set(w.impl, 0, nil) } -func Clear() { +func (r *FullscreenRenderer) Clear() { C.clear() C.endwin() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { C.refresh() } -func (w *Window) Erase() { - C.werase(w.win()) +func (w *CursesWindow) Erase() { + C.werase(w.impl) } -func (w *Window) X() int { - return int(C.c_getcurx(w.win())) +func (w *CursesWindow) X() int { + return int(C.c_getcurx(w.impl)) } -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } -func (w *Window) Fill(str string) bool { - return C.waddstr(w.win(), C.CString(str)) == C.OK +func (w *CursesWindow) Fill(str string) bool { + return C.waddstr(w.impl, C.CString(str)) == C.OK } -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)) +func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool { + index := ColorPair{fg, bg, -1}.index() + C.wcolor_set(w.impl, C.short(index), nil) + C.wattron(w.impl, C.int(attr)) ret := w.Fill(str) - C.wattroff(w.win(), C.int(attr)) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, C.int(attr)) + C.wcolor_set(w.impl, 0, nil) return ret } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { for _, w := range windows { - C.wnoutrefresh(w.win()) + w.Refresh() } C.doupdate() } -func PairFor(fg Color, bg Color) ColorPair { +func (p ColorPair) index() int16 { + if p.id >= 0 { + return p.id + } + // ncurses does not support 24-bit colors - if fg.is24() || bg.is24() { - return ColDefault + if p.is24() { + return ColDefault.index() } - key := (int(fg) << 8) + int(bg) + + key := p.key() 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)) + id := int16(len(_colorMap)) + ColUser.id + C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg())) _colorMap[key] = id return id } @@ -369,7 +371,7 @@ func escSequence() Event { return Event{Invalid, 0, nil} } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: @@ -435,17 +437,17 @@ func GetChar() Event { /* 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) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) } else { - _clickY = []int{y} - _prevDownTime = now + r.clickY = []int{y} + r.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 { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 1793836a..460bfd5b 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -18,30 +18,56 @@ import ( "github.com/junegunn/go-runewidth" ) -type ColorPair [2]Color +func (p ColorPair) style() tcell.Style { + style := tcell.StyleDefault + return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) +} -func (p ColorPair) fg() Color { - return p[0] +type Attr tcell.Style + +type TcellWindow struct { + color bool + top int + left int + width int + height int + lastX int + lastY int + moveCursor bool + border bool } -func (p ColorPair) bg() Color { - return p[1] +func (w *TcellWindow) Top() int { + return w.top } -func (p ColorPair) style() tcell.Style { - style := tcell.StyleDefault - return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg())) +func (w *TcellWindow) Left() int { + return w.left } -type Attr tcell.Style +func (w *TcellWindow) Width() int { + return w.width +} + +func (w *TcellWindow) Height() int { + return w.height +} + +func (w *TcellWindow) Refresh() { + if w.moveCursor { + _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + w.moveCursor = false + } + w.lastX = 0 + w.lastY = 0 + if w.border { + w.drawBorder() + } +} -type WindowTcell struct { - LastX int - LastY int - MoveCursor bool - Border bool +func (w *TcellWindow) FinishFill() { + // NO-OP } -type WindowImpl WindowTcell const ( Bold Attr = Attr(tcell.AttrBold) @@ -56,33 +82,13 @@ 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 { +func (r *FullscreenRenderer) 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, @@ -112,10 +118,9 @@ func (a Attr) Merge(b Attr) Attr { var ( _screen tcell.Screen - _mouse bool ) -func initScreen() { +func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) @@ -125,7 +130,7 @@ func initScreen() { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(2) } - if _mouse { + if r.mouse { s.EnableMouse() } else { s.DisableMouse() @@ -133,63 +138,41 @@ func initScreen() { _screen = s } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { encoding.Register() - _mouse = mouse - initScreen() + r.initScreen() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) +} - _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 { +func (r *FullscreenRenderer) MaxX() int { ncols, _ := _screen.Size() return int(ncols) } -func MaxY() int { +func (r *FullscreenRenderer) MaxY() int { _, nlines := _screen.Size() return int(nlines) } -func (w *Window) win() *WindowTcell { - return (*WindowTcell)(w.impl) -} - -func (w *Window) X() int { - return w.impl.LastX +func (w *TcellWindow) X() int { + return w.lastX } -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } -func Clear() { +func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { // noop } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: @@ -213,15 +196,15 @@ func GetChar() Event { double := false if down { now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, x) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, x) } else { - _clickY = []int{x} - _prevDownTime = now + r.clickY = []int{x} + r.prevDownTime = now } } else { - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } } @@ -368,49 +351,39 @@ func GetChar() Event { return Event{Invalid, 0, nil} } -func Pause() { +func (r *FullscreenRenderer) Pause() { _screen.Fini() } -func Resume() bool { - initScreen() +func (r *FullscreenRenderer) Resume() bool { + r.initScreen() return true } -func Close() { +func (r *FullscreenRenderer) Close() { _screen.Fini() } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) 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() - } + w.Refresh() } _screen.Show() } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) 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, - } + return &TcellWindow{ + color: r.theme != nil, + top: top, + left: left, + width: width, + height: height, + border: border} } -func (w *Window) Close() { +func (w *TcellWindow) Close() { // TODO } @@ -422,40 +395,40 @@ func fill(x, y, w, h int, r rune) { } } -func (w *Window) Erase() { +func (w *TcellWindow) Erase() { // TODO - fill(w.Left, w.Top, w.Width, w.Height, ' ') + 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 *TcellWindow) 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 *TcellWindow) Move(y int, x int) { + w.lastX = x + w.lastY = y + w.moveCursor = true } -func (w *Window) MoveAndClear(y int, x int) { +func (w *TcellWindow) 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()) + for i := w.lastX; i < w.width; i++ { + _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) } - w.win().LastX = x + w.lastX = x } -func (w *Window) Print(text string) { - w.PrintString(text, ColDefault, 0) +func (w *TcellWindow) Print(text string) { + w.printString(text, ColDefault, 0) } -func (w *Window) PrintString(text string, pair ColorPair, a Attr) { +func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { t := text lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style(). Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) @@ -481,7 +454,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { } if r == '\n' { - w.win().LastY++ + w.lastY++ lx = 0 } else { @@ -489,26 +462,26 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { 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) { + var xPos = w.left + w.lastX + lx + var yPos = w.top + w.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 + w.lastX += lx } -func (w *Window) CPrint(pair ColorPair, a Attr, text string) { - w.PrintString(text, pair, a) +func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { + w.printString(text, pair, attr) } -func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { +func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style() } else { style = ColDefault.style() @@ -522,22 +495,22 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { for _, r := range text { if r == '\n' { - w.win().LastY++ - w.win().LastX = 0 + w.lastY++ + w.lastX = 0 lx = 0 } else { - var xPos = w.Left + w.win().LastX + lx + var xPos = w.left + w.lastX + lx // word wrap: - if xPos >= (w.Left + w.Width) { - w.win().LastY++ - w.win().LastX = 0 + if xPos >= (w.left + w.width) { + w.lastY++ + w.lastX = 0 lx = 0 - xPos = w.Left + xPos = w.left } - var yPos = w.Top + w.win().LastY + var yPos = w.top + w.lastY - if yPos >= (w.Top + w.Height) { + if yPos >= (w.top + w.height) { return false } @@ -545,27 +518,27 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { lx += runewidth.RuneWidth(r) } } - w.win().LastX += lx + w.lastX += lx return true } -func (w *Window) Fill(str string) bool { - return w.FillString(str, ColDefault, 0) +func (w *TcellWindow) 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 *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool { + return w.fillString(str, ColorPair{fg, bg, -1}, a) } -func (w *Window) DrawBorder() { - left := w.Left - right := left + w.Width - top := w.Top - bot := top + w.Height +func (w *TcellWindow) drawBorder() { + left := w.left + right := left + w.width + top := w.top + bot := top + w.height var style tcell.Style - if _color { + if w.color { style = ColBorder.style() } else { style = ColDefault.style() diff --git a/src/tui/tui.go b/src/tui/tui.go index 125611cf..859eed7a 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -115,6 +115,32 @@ const ( colWhite ) +type ColorPair struct { + fg Color + bg Color + id int16 +} + +func NewColorPair(fg Color, bg Color) ColorPair { + return ColorPair{fg, bg, -1} +} + +func (p ColorPair) Fg() Color { + return p.fg +} + +func (p ColorPair) Bg() Color { + return p.bg +} + +func (p ColorPair) key() int { + return (int(p.Fg()) << 8) + int(p.Bg()) +} + +func (p ColorPair) is24() bool { + return p.Fg().is24() || p.Bg().is24() +} + type ColorTheme struct { Fg Color Bg Color @@ -146,23 +172,84 @@ type MouseEvent struct { Mod bool } -var ( - _color bool - _prevDownTime time.Time - _clickY []int - Default16 *ColorTheme - Dark256 *ColorTheme - Light256 *ColorTheme -) +type Renderer interface { + Init() + Pause() + Resume() bool + Clear() + RefreshWindows(windows []Window) + Refresh() + Close() + + GetChar() Event + + MaxX() int + MaxY() int + DoesAutoWrap() bool + + NewWindow(top int, left int, width int, height int, border bool) Window +} -type Window struct { - impl *WindowImpl - Top int - Left int - Width int - Height int +type Window interface { + Top() int + Left() int + Width() int + Height() int + + Refresh() + FinishFill() + Close() + + X() int + Enclose(y int, x int) bool + + Move(y int, x int) + MoveAndClear(y int, x int) + Print(text string) + CPrint(color ColorPair, attr Attr, text string) + Fill(text string) bool + CFill(fg Color, bg Color, attr Attr, text string) bool + Erase() +} + +type FullscreenRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int } +func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { + r := &FullscreenRenderer{ + theme: theme, + mouse: mouse, + forceBlack: forceBlack, + prevDownTime: time.Unix(0, 0), + clickY: []int{}} + return r +} + +var ( + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme + + ColDefault ColorPair + 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 EmptyTheme() *ColorTheme { return &ColorTheme{ Fg: colUndefined, @@ -181,8 +268,6 @@ func EmptyTheme() *ColorTheme { } func init() { - _prevDownTime = time.Unix(0, 0) - _clickY = []int{} Default16 = &ColorTheme{ Fg: colDefault, Bg: colDefault, @@ -227,14 +312,13 @@ func init() { Border: 145} } -func InitTheme(theme *ColorTheme, black bool) { - _color = theme != nil - if !_color { +func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { + if theme == nil { + initPalette(theme) return } - baseTheme := DefaultTheme() - if black { + if forceBlack { theme.Bg = colBlack } @@ -257,4 +341,48 @@ func InitTheme(theme *ColorTheme, black bool) { theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Header = o(baseTheme.Header, theme.Header) theme.Border = o(baseTheme.Border, theme.Border) + + initPalette(theme) +} + +func initPalette(theme *ColorTheme) { + ColDefault = ColorPair{colDefault, colDefault, 0} + if theme != nil { + ColNormal = ColorPair{theme.Fg, theme.Bg, 1} + ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} + ColMatch = ColorPair{theme.Match, theme.Bg, 3} + ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} + ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} + ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} + ColInfo = ColorPair{theme.Info, theme.Bg, 7} + ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} + ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} + ColHeader = ColorPair{theme.Header, theme.Bg, 10} + ColBorder = ColorPair{theme.Border, theme.Bg, 11} + } else { + ColNormal = ColorPair{colDefault, colDefault, 1} + ColPrompt = ColorPair{colDefault, colDefault, 2} + ColMatch = ColorPair{colDefault, colDefault, 3} + ColCurrent = ColorPair{colDefault, colDefault, 4} + ColCurrentMatch = ColorPair{colDefault, colDefault, 5} + ColSpinner = ColorPair{colDefault, colDefault, 6} + ColInfo = ColorPair{colDefault, colDefault, 7} + ColCursor = ColorPair{colDefault, colDefault, 8} + ColSelected = ColorPair{colDefault, colDefault, 9} + ColHeader = ColorPair{colDefault, colDefault, 10} + ColBorder = ColorPair{colDefault, colDefault, 11} + } + ColUser = ColorPair{colDefault, colDefault, 12} +} + +func attrFor(color ColorPair, attr Attr) Attr { + switch color { + case ColCurrent: + return attr | Reverse + case ColMatch: + return attr | Underline + case ColCurrentMatch: + return attr | Underline | Reverse + } + return attr } diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go deleted file mode 100644 index 4a2fee91..00000000 --- a/src/tui/tui_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package tui - -import ( - "testing" -) - -func TestPairFor(t *testing.T) { - if PairFor(30, 50) != PairFor(30, 50) { - t.Fail() - } - if PairFor(-1, 10) != PairFor(-1, 10) { - t.Fail() - } -} diff --git a/src/util/util.go b/src/util/util.go index 2a1607ce..29e80176 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -6,8 +6,24 @@ import ( "time" "github.com/junegunn/go-isatty" + "github.com/junegunn/go-runewidth" ) +var _runeWidths = make(map[rune]int) + +// RuneWidth returns rune width +func RuneWidth(r rune, prefixWidth int, tabstop int) int { + if r == '\t' { + return tabstop - prefixWidth%tabstop + } else if w, found := _runeWidths[r]; found { + return w + } else { + w := runewidth.RuneWidth(r) + _runeWidths[r] = w + return w + } +} + // Max returns the largest integer func Max(first int, second int) int { if first >= second { diff --git a/src/util/util_unix.go b/src/util/util_unix.go index 29e0d30d..bc1b7b52 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" ) // ExecCommand executes the given command with $SHELL @@ -20,3 +21,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return false } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(int(file.Fd()), nonblock) +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 3aa86606..9ba4f79e 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" "github.com/junegunn/go-shellwords" ) @@ -26,3 +27,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return true } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) +} |
