m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-17 15:53:39 -05:00

Add --height option

This commit is contained in:
Junegunn Choi
2017-01-08 01:30:31 +09:00
parent fd137a9e87
commit 1448d631a7
24 changed files with 1624 additions and 608 deletions

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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++
}

764
src/tui/light.go Normal file
View File

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

View File

@@ -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 *CursesWindow) Close() {
C.delwin(w.impl)
}
func (w *Window) Close() {
C.delwin(w.win())
func (w *CursesWindow) Enclose(y int, x int) bool {
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
}
func (w *Window) Enclose(y int, x int) bool {
return bool(C.wenclose(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) Move(y int, x int) {
C.wmove(w.win(), 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 {
// ncurses does not support 24-bit colors
if fg.is24() || bg.is24() {
return ColDefault
func (p ColorPair) index() int16 {
if p.id >= 0 {
return p.id
}
key := (int(fg) << 8) + int(bg)
// ncurses does not support 24-bit colors
if p.is24() {
return ColDefault.index()
}
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}}

View File

@@ -18,30 +18,56 @@ import (
"github.com/junegunn/go-runewidth"
)
type ColorPair [2]Color
func (p ColorPair) fg() Color {
return p[0]
}
func (p ColorPair) bg() Color {
return p[1]
}
func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg()))
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
}
type Attr tcell.Style
type WindowTcell struct {
LastX int
LastY int
MoveCursor bool
Border bool
type TcellWindow struct {
color bool
top int
left int
width int
height int
lastX int
lastY int
moveCursor bool
border bool
}
func (w *TcellWindow) Top() int {
return w.top
}
func (w *TcellWindow) Left() int {
return w.left
}
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()
}
}
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()
_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}
r.initScreen()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
}
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 *TcellWindow) X() int {
return w.lastX
}
func (w *Window) X() int {
return w.impl.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()

View File

@@ -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()
type Window struct {
impl *WindowImpl
Top int
Left int
Width int
Height int
GetChar() Event
MaxX() int
MaxY() int
DoesAutoWrap() bool
NewWindow(top int, left int, width int, height int, border bool) Window
}
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
}

View File

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

View File

@@ -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 {

View File

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

View File

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