mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-18 16:45:38 -05:00
Add --height option
This commit is contained in:
764
src/tui/light.go
Normal file
764
src/tui/light.go
Normal 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)
|
||||
}
|
||||
@@ -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}}
|
||||
|
||||
275
src/tui/tcell.go
275
src/tui/tcell.go
@@ -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()
|
||||
|
||||
170
src/tui/tui.go
170
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user