mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-15 09:54:03 +00:00
22cbd9fa58
Close #2953
1027 lines
21 KiB
Go
1027 lines
21 KiB
Go
package tui
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
"github.com/rivo/uniseg"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const (
|
|
defaultWidth = 80
|
|
defaultHeight = 24
|
|
|
|
defaultEscDelay = 100
|
|
escPollInterval = 5
|
|
offsetPollTries = 10
|
|
maxInputBuffer = 1024 * 1024
|
|
)
|
|
|
|
const consoleDevice string = "/dev/tty"
|
|
|
|
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
|
|
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
|
|
|
func (r *LightRenderer) stderr(str string) {
|
|
r.stderrInternal(str, true)
|
|
}
|
|
|
|
// FIXME: Need better handling of non-displayable characters
|
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
|
bytes := []byte(str)
|
|
runes := []rune{}
|
|
for len(bytes) > 0 {
|
|
r, sz := utf8.DecodeRune(bytes)
|
|
nlcr := r == '\n' || r == '\r'
|
|
if r >= 32 || r == '\x1b' || nlcr {
|
|
if r == utf8.RuneError || nlcr && !allowNLCR {
|
|
runes = append(runes, ' ')
|
|
} else {
|
|
runes = append(runes, r)
|
|
}
|
|
}
|
|
bytes = bytes[sz:]
|
|
}
|
|
r.queued.WriteString(string(runes))
|
|
}
|
|
|
|
func (r *LightRenderer) csi(code string) {
|
|
r.stderr("\x1b[" + code)
|
|
}
|
|
|
|
func (r *LightRenderer) flush() {
|
|
if r.queued.Len() > 0 {
|
|
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
|
|
r.queued.Reset()
|
|
}
|
|
}
|
|
|
|
// Light renderer
|
|
type LightRenderer struct {
|
|
theme *ColorTheme
|
|
mouse bool
|
|
forceBlack bool
|
|
clearOnExit bool
|
|
prevDownTime time.Time
|
|
clickY []int
|
|
ttyin *os.File
|
|
buffer []byte
|
|
origState *term.State
|
|
width int
|
|
height int
|
|
yoffset int
|
|
tabstop int
|
|
escDelay int
|
|
fullscreen bool
|
|
upOneLine bool
|
|
queued strings.Builder
|
|
y int
|
|
x int
|
|
maxHeightFunc func(int) int
|
|
|
|
// Windows only
|
|
ttyinChannel chan byte
|
|
inHandle uintptr
|
|
outHandle uintptr
|
|
origStateInput uint32
|
|
origStateOutput uint32
|
|
}
|
|
|
|
type LightWindow struct {
|
|
renderer *LightRenderer
|
|
colored bool
|
|
preview bool
|
|
border BorderStyle
|
|
top int
|
|
left int
|
|
width int
|
|
height int
|
|
posx int
|
|
posy int
|
|
tabstop int
|
|
fg Color
|
|
bg Color
|
|
}
|
|
|
|
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
|
|
r := LightRenderer{
|
|
theme: theme,
|
|
forceBlack: forceBlack,
|
|
mouse: mouse,
|
|
clearOnExit: clearOnExit,
|
|
ttyin: openTtyIn(),
|
|
yoffset: 0,
|
|
tabstop: tabstop,
|
|
fullscreen: fullscreen,
|
|
upOneLine: false,
|
|
maxHeightFunc: maxHeightFunc}
|
|
return &r
|
|
}
|
|
|
|
func repeat(r rune, times int) string {
|
|
if times > 0 {
|
|
return strings.Repeat(string(r), 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() {
|
|
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
|
|
|
|
if err := r.initPlatform(); err != nil {
|
|
errorExit(err.Error())
|
|
}
|
|
r.updateTerminalSize()
|
|
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
|
|
|
if r.fullscreen {
|
|
r.smcup()
|
|
} else {
|
|
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
|
// So we do not clear the lower bottom of the screen.
|
|
if r.clearOnExit {
|
|
r.csi("J")
|
|
}
|
|
y, x := r.findOffset()
|
|
r.mouse = r.mouse && y >= 0
|
|
// When --no-clear is used for repetitive relaunching, there is a small
|
|
// time frame between fzf processes where the user keystrokes are not
|
|
// captured by either of fzf process which can cause x offset to be
|
|
// increased and we're left with unwanted extra new line.
|
|
if x > 0 && r.clearOnExit {
|
|
r.upOneLine = true
|
|
r.makeSpace()
|
|
}
|
|
for i := 1; i < r.MaxY(); i++ {
|
|
r.makeSpace()
|
|
}
|
|
}
|
|
|
|
if r.mouse {
|
|
r.csi("?1000h")
|
|
r.csi("?1006h")
|
|
}
|
|
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
|
r.csi("G")
|
|
r.csi("K")
|
|
if !r.clearOnExit && !r.fullscreen {
|
|
r.csi("s")
|
|
}
|
|
if !r.fullscreen && r.mouse {
|
|
r.yoffset, _ = r.findOffset()
|
|
}
|
|
}
|
|
|
|
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
|
|
r.maxHeightFunc = maxHeightFunc
|
|
}
|
|
|
|
func (r *LightRenderer) makeSpace() {
|
|
r.stderr("\n")
|
|
r.csi("G")
|
|
}
|
|
|
|
func (r *LightRenderer) move(y int, x int) {
|
|
// w.csi("u")
|
|
if r.y < y {
|
|
r.csi(fmt.Sprintf("%dB", y-r.y))
|
|
} else if r.y > y {
|
|
r.csi(fmt.Sprintf("%dA", r.y-y))
|
|
}
|
|
r.stderr("\r")
|
|
if x > 0 {
|
|
r.csi(fmt.Sprintf("%dC", x))
|
|
}
|
|
r.y = y
|
|
r.x = x
|
|
}
|
|
|
|
func (r *LightRenderer) origin() {
|
|
r.move(0, 0)
|
|
}
|
|
|
|
func getEnv(name string, defaultValue int) int {
|
|
env := os.Getenv(name)
|
|
if len(env) == 0 {
|
|
return defaultValue
|
|
}
|
|
return atoi(env, defaultValue)
|
|
}
|
|
|
|
func (r *LightRenderer) getBytes() []byte {
|
|
return r.getBytesInternal(r.buffer, false)
|
|
}
|
|
|
|
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
|
c, ok := r.getch(nonblock)
|
|
if !nonblock && !ok {
|
|
r.Close()
|
|
errorExit("Failed to read " + consoleDevice)
|
|
}
|
|
|
|
retries := 0
|
|
if c == ESC.Int() || nonblock {
|
|
retries = r.escDelay / escPollInterval
|
|
}
|
|
buffer = append(buffer, byte(c))
|
|
|
|
pc := c
|
|
for {
|
|
c, ok = r.getch(true)
|
|
if !ok {
|
|
if retries > 0 {
|
|
retries--
|
|
time.Sleep(escPollInterval * time.Millisecond)
|
|
continue
|
|
}
|
|
break
|
|
} else if c == ESC.Int() && pc != c {
|
|
retries = r.escDelay / escPollInterval
|
|
} else {
|
|
retries = 0
|
|
}
|
|
buffer = append(buffer, byte(c))
|
|
pc = c
|
|
|
|
// This should never happen under normal conditions,
|
|
// so terminate fzf immediately.
|
|
if len(buffer) > maxInputBuffer {
|
|
r.Close()
|
|
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
|
|
}
|
|
}
|
|
|
|
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.Byte():
|
|
return Event{CtrlC, 0, nil}
|
|
case CtrlG.Byte():
|
|
return Event{CtrlG, 0, nil}
|
|
case CtrlQ.Byte():
|
|
return Event{CtrlQ, 0, nil}
|
|
case 127:
|
|
return Event{BSpace, 0, nil}
|
|
case 0:
|
|
return Event{CtrlSpace, 0, nil}
|
|
case 28:
|
|
return Event{CtrlBackSlash, 0, nil}
|
|
case 29:
|
|
return Event{CtrlRightBracket, 0, nil}
|
|
case 30:
|
|
return Event{CtrlCaret, 0, nil}
|
|
case 31:
|
|
return Event{CtrlSlash, 0, nil}
|
|
case ESC.Byte():
|
|
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.Byte() {
|
|
return Event{EventType(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}
|
|
}
|
|
|
|
loc := offsetRegexpBegin.FindIndex(r.buffer)
|
|
if loc != nil && loc[0] == 0 {
|
|
*sz = loc[1]
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
|
|
*sz = 2
|
|
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
|
|
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
|
|
}
|
|
alt := false
|
|
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() {
|
|
r.buffer = r.buffer[1:]
|
|
alt = true
|
|
}
|
|
switch r.buffer[1] {
|
|
case ESC.Byte():
|
|
return Event{ESC, 0, nil}
|
|
case 127:
|
|
return Event{AltBS, 0, nil}
|
|
case '[', 'O':
|
|
if len(r.buffer) < 3 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
*sz = 3
|
|
switch r.buffer[2] {
|
|
case 'D':
|
|
if alt {
|
|
return Event{AltLeft, 0, nil}
|
|
}
|
|
return Event{Left, 0, nil}
|
|
case 'C':
|
|
if alt {
|
|
// Ugh..
|
|
return Event{AltRight, 0, nil}
|
|
}
|
|
return Event{Right, 0, nil}
|
|
case 'B':
|
|
if alt {
|
|
return Event{AltDown, 0, nil}
|
|
}
|
|
return Event{Down, 0, nil}
|
|
case 'A':
|
|
if alt {
|
|
return Event{AltUp, 0, nil}
|
|
}
|
|
return Event{Up, 0, nil}
|
|
case 'Z':
|
|
return Event{BTab, 0, nil}
|
|
case 'H':
|
|
return Event{Home, 0, nil}
|
|
case 'F':
|
|
return Event{End, 0, nil}
|
|
case '<':
|
|
return r.mouseSequence(sz)
|
|
case 'P':
|
|
return Event{F1, 0, nil}
|
|
case 'Q':
|
|
return Event{F2, 0, nil}
|
|
case 'R':
|
|
return Event{F3, 0, nil}
|
|
case 'S':
|
|
return Event{F4, 0, nil}
|
|
case '1', '2', '3', '4', '5', '6':
|
|
if len(r.buffer) < 4 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
*sz = 4
|
|
switch r.buffer[2] {
|
|
case '2':
|
|
if r.buffer[3] == '~' {
|
|
return Event{Insert, 0, nil}
|
|
}
|
|
if len(r.buffer) > 4 && r.buffer[4] == '~' {
|
|
*sz = 5
|
|
switch r.buffer[3] {
|
|
case '0':
|
|
return Event{F9, 0, nil}
|
|
case '1':
|
|
return Event{F10, 0, nil}
|
|
case '3':
|
|
return Event{F11, 0, nil}
|
|
case '4':
|
|
return Event{F12, 0, nil}
|
|
}
|
|
}
|
|
// Bracketed paste mode: \e[200~ ... \e[201~
|
|
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
|
// Immediately discard the sequence from the buffer and reread input
|
|
r.buffer = r.buffer[6:]
|
|
*sz = 0
|
|
return r.GetChar()
|
|
}
|
|
return Event{Invalid, 0, nil} // INS
|
|
case '3':
|
|
return Event{Del, 0, nil}
|
|
case '4':
|
|
return Event{End, 0, nil}
|
|
case '5':
|
|
return Event{PgUp, 0, nil}
|
|
case '6':
|
|
return Event{PgDn, 0, nil}
|
|
case '1':
|
|
switch r.buffer[3] {
|
|
case '~':
|
|
return Event{Home, 0, nil}
|
|
case '1', '2', '3', '4', '5', '7', '8', '9':
|
|
if len(r.buffer) == 5 && r.buffer[4] == '~' {
|
|
*sz = 5
|
|
switch r.buffer[3] {
|
|
case '1':
|
|
return Event{F1, 0, nil}
|
|
case '2':
|
|
return Event{F2, 0, nil}
|
|
case '3':
|
|
return Event{F3, 0, nil}
|
|
case '4':
|
|
return Event{F4, 0, nil}
|
|
case '5':
|
|
return Event{F5, 0, nil}
|
|
case '7':
|
|
return Event{F6, 0, nil}
|
|
case '8':
|
|
return Event{F7, 0, nil}
|
|
case '9':
|
|
return Event{F8, 0, nil}
|
|
}
|
|
}
|
|
return Event{Invalid, 0, nil}
|
|
case ';':
|
|
if len(r.buffer) < 6 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
*sz = 6
|
|
switch r.buffer[4] {
|
|
case '1', '2', '3', '5':
|
|
alt := r.buffer[4] == '3'
|
|
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
|
|
char := r.buffer[5]
|
|
if altShift {
|
|
if len(r.buffer) < 7 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
*sz = 7
|
|
char = r.buffer[6]
|
|
}
|
|
switch char {
|
|
case 'A':
|
|
if alt {
|
|
return Event{AltUp, 0, nil}
|
|
}
|
|
if altShift {
|
|
return Event{AltSUp, 0, nil}
|
|
}
|
|
return Event{SUp, 0, nil}
|
|
case 'B':
|
|
if alt {
|
|
return Event{AltDown, 0, nil}
|
|
}
|
|
if altShift {
|
|
return Event{AltSDown, 0, nil}
|
|
}
|
|
return Event{SDown, 0, nil}
|
|
case 'C':
|
|
if alt {
|
|
return Event{AltRight, 0, nil}
|
|
}
|
|
if altShift {
|
|
return Event{AltSRight, 0, nil}
|
|
}
|
|
return Event{SRight, 0, nil}
|
|
case 'D':
|
|
if alt {
|
|
return Event{AltLeft, 0, nil}
|
|
}
|
|
if altShift {
|
|
return Event{AltSLeft, 0, nil}
|
|
}
|
|
return Event{SLeft, 0, nil}
|
|
}
|
|
} // r.buffer[4]
|
|
} // r.buffer[3]
|
|
} // r.buffer[2]
|
|
} // r.buffer[2]
|
|
} // r.buffer[1]
|
|
rest := bytes.NewBuffer(r.buffer[1:])
|
|
c, size, err := rest.ReadRune()
|
|
if err == nil {
|
|
*sz = 1 + size
|
|
return AltKey(c)
|
|
}
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
|
|
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
|
|
func (r *LightRenderer) mouseSequence(sz *int) Event {
|
|
// "\e[<0;0;0M"
|
|
if len(r.buffer) < 9 || !r.mouse {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
|
|
rest := r.buffer[*sz:]
|
|
end := bytes.IndexAny(rest, "mM")
|
|
if end == -1 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
|
|
elems := strings.SplitN(string(rest[:end]), ";", 3)
|
|
if len(elems) != 3 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
|
|
t := atoi(elems[0], -1)
|
|
x := atoi(elems[1], -1) - 1
|
|
y := atoi(elems[2], -1) - 1 - r.yoffset
|
|
if t < 0 || x < 0 || y < 0 {
|
|
return Event{Invalid, 0, nil}
|
|
}
|
|
*sz += end + 1
|
|
|
|
down := rest[end] == 'M'
|
|
|
|
scroll := 0
|
|
if t >= 64 {
|
|
t -= 64
|
|
if t&0b1 == 1 {
|
|
scroll = -1
|
|
} else {
|
|
scroll = 1
|
|
}
|
|
}
|
|
|
|
// middle := t & 0b1
|
|
left := t&0b11 == 0
|
|
|
|
// shift := t & 0b100
|
|
// ctrl := t & 0b1000
|
|
mod := t&0b1100 > 0
|
|
|
|
if scroll != 0 {
|
|
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
|
}
|
|
|
|
double := false
|
|
if down {
|
|
now := time.Now()
|
|
if !left { // Right double click is not allowed
|
|
r.clickY = []int{}
|
|
} else 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.Since(r.prevDownTime) < doubleClickDuration {
|
|
double = true
|
|
}
|
|
}
|
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
|
}
|
|
|
|
func (r *LightRenderer) smcup() {
|
|
r.csi("?1049h")
|
|
}
|
|
|
|
func (r *LightRenderer) rmcup() {
|
|
r.csi("?1049l")
|
|
}
|
|
|
|
func (r *LightRenderer) Pause(clear bool) {
|
|
r.restoreTerminal()
|
|
if clear {
|
|
if r.fullscreen {
|
|
r.rmcup()
|
|
} else {
|
|
r.smcup()
|
|
r.csi("H")
|
|
}
|
|
r.flush()
|
|
}
|
|
}
|
|
|
|
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
|
r.setupTerminal()
|
|
if clear {
|
|
if r.fullscreen {
|
|
r.smcup()
|
|
} else {
|
|
r.rmcup()
|
|
}
|
|
r.flush()
|
|
} else if sigcont && !r.fullscreen && r.mouse {
|
|
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
|
// It's highly likely that the offset we obtained at the beginning is
|
|
// no longer correct, so we simply disable mouse input.
|
|
r.csi("?1000l")
|
|
r.csi("?1006l")
|
|
r.mouse = false
|
|
}
|
|
}
|
|
|
|
func (r *LightRenderer) Clear() {
|
|
if r.fullscreen {
|
|
r.csi("H")
|
|
}
|
|
// r.csi("u")
|
|
r.origin()
|
|
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")
|
|
if r.clearOnExit {
|
|
if r.fullscreen {
|
|
r.rmcup()
|
|
} else {
|
|
r.origin()
|
|
if r.upOneLine {
|
|
r.csi("A")
|
|
}
|
|
r.csi("J")
|
|
}
|
|
} else if !r.fullscreen {
|
|
r.csi("u")
|
|
}
|
|
if r.mouse {
|
|
r.csi("?1000l")
|
|
r.csi("?1006l")
|
|
}
|
|
r.flush()
|
|
r.closePlatform()
|
|
r.restoreTerminal()
|
|
}
|
|
|
|
func (r *LightRenderer) MaxX() int {
|
|
return r.width
|
|
}
|
|
|
|
func (r *LightRenderer) MaxY() int {
|
|
if r.height == 0 {
|
|
r.updateTerminalSize()
|
|
}
|
|
return r.height
|
|
}
|
|
|
|
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
|
w := &LightWindow{
|
|
renderer: r,
|
|
colored: r.theme.Colored,
|
|
preview: preview,
|
|
border: borderStyle,
|
|
top: top,
|
|
left: left,
|
|
width: width,
|
|
height: height,
|
|
tabstop: r.tabstop,
|
|
fg: colDefault,
|
|
bg: colDefault}
|
|
if preview {
|
|
w.fg = r.theme.PreviewFg.Color
|
|
w.bg = r.theme.PreviewBg.Color
|
|
} else {
|
|
w.fg = r.theme.Fg.Color
|
|
w.bg = r.theme.Bg.Color
|
|
}
|
|
w.drawBorder()
|
|
return w
|
|
}
|
|
|
|
func (w *LightWindow) drawBorder() {
|
|
switch w.border.shape {
|
|
case BorderRounded, BorderSharp:
|
|
w.drawBorderAround()
|
|
case BorderHorizontal:
|
|
w.drawBorderHorizontal(true, true)
|
|
case BorderVertical:
|
|
w.drawBorderVertical(true, true)
|
|
case BorderTop:
|
|
w.drawBorderHorizontal(true, false)
|
|
case BorderBottom:
|
|
w.drawBorderHorizontal(false, true)
|
|
case BorderLeft:
|
|
w.drawBorderVertical(true, false)
|
|
case BorderRight:
|
|
w.drawBorderVertical(false, true)
|
|
}
|
|
}
|
|
|
|
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
|
color := ColBorder
|
|
if w.preview {
|
|
color = ColPreviewBorder
|
|
}
|
|
if top {
|
|
w.Move(0, 0)
|
|
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
|
}
|
|
if bottom {
|
|
w.Move(w.height-1, 0)
|
|
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
|
}
|
|
}
|
|
|
|
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
|
width := w.width - 2
|
|
if !left || !right {
|
|
width++
|
|
}
|
|
color := ColBorder
|
|
if w.preview {
|
|
color = ColPreviewBorder
|
|
}
|
|
for y := 0; y < w.height; y++ {
|
|
w.Move(y, 0)
|
|
if left {
|
|
w.CPrint(color, string(w.border.vertical))
|
|
}
|
|
w.CPrint(color, repeat(' ', width))
|
|
if right {
|
|
w.CPrint(color, string(w.border.vertical))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *LightWindow) drawBorderAround() {
|
|
w.Move(0, 0)
|
|
color := ColBorder
|
|
if w.preview {
|
|
color = ColPreviewBorder
|
|
}
|
|
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
|
|
for y := 1; y < w.height-1; y++ {
|
|
w.Move(y, 0)
|
|
w.CPrint(color, string(w.border.vertical))
|
|
w.CPrint(color, repeat(' ', w.width-2))
|
|
w.CPrint(color, string(w.border.vertical))
|
|
}
|
|
w.Move(w.height-1, 0)
|
|
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
|
|
}
|
|
|
|
func (w *LightWindow) csi(code string) {
|
|
w.renderer.csi(code)
|
|
}
|
|
|
|
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
|
w.renderer.stderrInternal(str, allowNLCR)
|
|
}
|
|
|
|
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) Y() int {
|
|
return w.posy
|
|
}
|
|
|
|
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.renderer.move(w.Top()+y, w.Left()+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 & AttrClear) > 0 {
|
|
return codes
|
|
}
|
|
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")
|
|
}
|
|
if (attr & StrikeThrough) > 0 {
|
|
codes = append(codes, "9")
|
|
}
|
|
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 cleanse(str string) string {
|
|
return strings.Replace(str, "\x1b", "", -1)
|
|
}
|
|
|
|
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
|
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
|
w.stderrInternal(cleanse(text), false)
|
|
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.stderrInternal(cleanse(text), false)
|
|
}
|
|
|
|
type wrappedLine struct {
|
|
text string
|
|
displayWidth int
|
|
}
|
|
|
|
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
|
lines := []wrappedLine{}
|
|
width := 0
|
|
line := ""
|
|
gr := uniseg.NewGraphemes(input)
|
|
for gr.Next() {
|
|
rs := gr.Runes()
|
|
str := string(rs)
|
|
var w int
|
|
if len(rs) == 1 && rs[0] == '\t' {
|
|
w = tabstop - (prefixLength+width)%tabstop
|
|
str = repeat(' ', w)
|
|
} else {
|
|
w = runewidth.StringWidth(str)
|
|
}
|
|
width += w
|
|
|
|
if prefixLength+width <= max {
|
|
line += str
|
|
} else {
|
|
lines = append(lines, wrappedLine{string(line), width - w})
|
|
line = str
|
|
prefixLength = 0
|
|
width = w
|
|
}
|
|
}
|
|
lines = append(lines, wrappedLine{string(line), width})
|
|
return lines
|
|
}
|
|
|
|
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
|
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.stderrInternal(wl.text, false)
|
|
w.posx += wl.displayWidth
|
|
|
|
// Wrap line
|
|
if j < len(lines)-1 || i < len(allLines)-1 {
|
|
if w.posy+1 >= w.height {
|
|
return FillSuspend
|
|
}
|
|
w.MoveAndClear(w.posy, w.posx)
|
|
w.Move(w.posy+1, 0)
|
|
onMove()
|
|
}
|
|
}
|
|
}
|
|
if w.posx+1 >= w.Width() {
|
|
if w.posy+1 >= w.height {
|
|
return FillSuspend
|
|
}
|
|
w.Move(w.posy+1, 0)
|
|
onMove()
|
|
return FillNextLine
|
|
}
|
|
return FillContinue
|
|
}
|
|
|
|
func (w *LightWindow) setBg() {
|
|
if w.bg != colDefault {
|
|
w.csiColor(colDefault, w.bg, AttrRegular)
|
|
}
|
|
}
|
|
|
|
func (w *LightWindow) Fill(text string) FillReturn {
|
|
w.Move(w.posy, w.posx)
|
|
w.setBg()
|
|
return w.fill(text, w.setBg)
|
|
}
|
|
|
|
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
|
w.Move(w.posy, w.posx)
|
|
if fg == colDefault {
|
|
fg = w.fg
|
|
}
|
|
if bg == colDefault {
|
|
bg = w.bg
|
|
}
|
|
if w.csiColor(fg, bg, attr) {
|
|
defer w.csi("m")
|
|
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
|
}
|
|
return w.fill(text, w.setBg)
|
|
}
|
|
|
|
func (w *LightWindow) FinishFill() {
|
|
w.MoveAndClear(w.posy, w.posx)
|
|
for y := w.posy + 1; y < w.height; y++ {
|
|
w.MoveAndClear(y, 0)
|
|
}
|
|
}
|
|
|
|
func (w *LightWindow) Erase() {
|
|
w.drawBorder()
|
|
// We don't erase the window here to avoid flickering during scroll
|
|
w.Move(0, 0)
|
|
}
|