Skip to content
Snippets Groups Projects
light.go 18.1 KiB
Newer Older
Junegunn Choi's avatar
Junegunn Choi committed
package tui

import (
	"fmt"
	"os"
Junegunn Choi's avatar
Junegunn Choi committed
	"strconv"
	"strings"
	"syscall"
	"time"
	"unicode/utf8"

	"github.com/junegunn/fzf/src/util"

	"golang.org/x/crypto/ssh/terminal"
Junegunn Choi's avatar
Junegunn Choi committed
)

const (
	defaultWidth  = 80
	defaultHeight = 24

Junegunn Choi's avatar
Junegunn Choi committed
	defaultEscDelay = 100
Junegunn Choi's avatar
Junegunn Choi committed
	escPollInterval = 5
const consoleDevice string = "/dev/tty"

var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R")

Junegunn Choi's avatar
Junegunn Choi committed
func openTtyIn() *os.File {
	in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
Junegunn Choi's avatar
Junegunn Choi committed
	if err != nil {
		panic("Failed to open " + consoleDevice)
Junegunn Choi's avatar
Junegunn Choi committed
	}
	return in
}

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) {
Junegunn Choi's avatar
Junegunn Choi committed
	bytes := []byte(str)
	runes := []rune{}
	for len(bytes) > 0 {
		r, sz := utf8.DecodeRune(bytes)
		if r == utf8.RuneError || r < 32 &&
			r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
Junegunn Choi's avatar
Junegunn Choi committed
			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
Junegunn Choi's avatar
Junegunn Choi committed
	clearOnExit   bool
Junegunn Choi's avatar
Junegunn Choi committed
	prevDownTime  time.Time
	clickY        []int
	ttyin         *os.File
	buffer        []byte
	origState     *terminal.State
Junegunn Choi's avatar
Junegunn Choi committed
	width         int
	height        int
	yoffset       int
	tabstop       int
	escDelay      int
Junegunn Choi's avatar
Junegunn Choi committed
	upOneLine     bool
	queued        string
Junegunn Choi's avatar
Junegunn Choi committed
	y             int
	x             int
Junegunn Choi's avatar
Junegunn Choi committed
	maxHeightFunc func(int) int
}

type LightWindow struct {
	renderer *LightRenderer
	colored  bool
Junegunn Choi's avatar
Junegunn Choi committed
	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, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
Junegunn Choi's avatar
Junegunn Choi committed
	r := LightRenderer{
		theme:         theme,
		forceBlack:    forceBlack,
		mouse:         mouse,
Junegunn Choi's avatar
Junegunn Choi committed
		clearOnExit:   clearOnExit,
Junegunn Choi's avatar
Junegunn Choi committed
		ttyin:         openTtyIn(),
Junegunn Choi's avatar
Junegunn Choi committed
		tabstop:       tabstop,
Junegunn Choi's avatar
Junegunn Choi committed
		upOneLine:     false,
		maxHeightFunc: maxHeightFunc}
	return &r
}

func (r *LightRenderer) fd() int {
	return int(r.ttyin.Fd())
}

Junegunn Choi's avatar
Junegunn Choi committed
func (r *LightRenderer) defaultTheme() *ColorTheme {
	if strings.Contains(os.Getenv("TERM"), "256") {
		return Dark256
	}
	colors, err := exec.Command("tput", "colors").Output()
Junegunn Choi's avatar
Junegunn Choi committed
	if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
		return Dark256
	}
	return Default16
}

func (r *LightRenderer) findOffset() (row int, col int) {
	r.csi("6n")
	r.flush()
	bytes := []byte{}
	for tries := 0; tries < offsetPollTries; tries++ {
		bytes = r.getBytesInternal(bytes, tries > 0)
		offsets := offsetRegexp.FindSubmatch(bytes)
		if len(offsets) > 2 {
			return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
Junegunn Choi's avatar
Junegunn Choi committed
		}
	}
	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() {
Junegunn Choi's avatar
Junegunn Choi committed
	r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
	fd := r.fd()
	origState, err := terminal.GetState(fd)
	if err != nil {
		errorExit(err.Error())
	}
	r.origState = origState
	terminal.MakeRaw(fd)
Junegunn Choi's avatar
Junegunn Choi committed
	initTheme(r.theme, r.defaultTheme(), r.forceBlack)

	if r.fullscreen {
		r.smcup()
	} else {
		r.csi("J")
		y, x := r.findOffset()
		r.mouse = r.mouse && y >= 0
		if x > 0 {
			r.upOneLine = true
			r.makeSpace()
		}
		for i := 1; i < r.MaxY(); i++ {
			r.makeSpace()
		}
Junegunn Choi's avatar
Junegunn Choi committed
	}

	if r.mouse {
		r.csi("?1000h")
	}
	r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
	r.csi("G")
Junegunn Choi's avatar
Junegunn Choi committed
	// r.csi("s")
	if !r.fullscreen && r.mouse {
func (r *LightRenderer) makeSpace() {
	r.stderr("\n")
	r.csi("G")
}

Junegunn Choi's avatar
Junegunn Choi committed
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) updateTerminalSize() {
	width, height, err := terminal.GetSize(r.fd())
	if err == nil {
		r.width = width
		r.height = r.maxHeightFunc(height)
Junegunn Choi's avatar
Junegunn Choi committed
	} else {
		r.width = getEnv("COLUMNS", defaultWidth)
		r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
Junegunn Choi's avatar
Junegunn Choi committed
	b := make([]byte, 1)
Junegunn Choi's avatar
Junegunn Choi committed
	util.SetNonblock(r.ttyin, nonblock)
	_, err := util.Read(fd, b)
Junegunn Choi's avatar
Junegunn Choi committed
	if err != nil {
Junegunn Choi's avatar
Junegunn Choi committed
	}
Junegunn Choi's avatar
Junegunn Choi committed
}

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 {
		errorExit("Failed to read " + consoleDevice)
Junegunn Choi's avatar
Junegunn Choi committed

	retries := 0
Junegunn Choi's avatar
Junegunn Choi committed
		retries = r.escDelay / escPollInterval
	}
	buffer = append(buffer, byte(c))

	for {
		c, ok = r.getch(true)
		if !ok {
Junegunn Choi's avatar
Junegunn Choi committed
			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 0:
		return Event{CtrlSpace, 0, nil}
Junegunn Choi's avatar
Junegunn Choi committed
	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
	if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
		return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
	}
Junegunn Choi's avatar
Junegunn Choi committed
	switch r.buffer[1] {
	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.mouse {
Junegunn Choi's avatar
Junegunn Choi committed
		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) smcup() {
	r.csi("?1049h")
}

func (r *LightRenderer) rmcup() {
	r.csi("?1049l")
}

Junegunn Choi's avatar
Junegunn Choi committed
func (r *LightRenderer) Pause(clear bool) {
	terminal.Restore(r.fd(), r.origState)
Junegunn Choi's avatar
Junegunn Choi committed
	if clear {
		if r.fullscreen {
			r.rmcup()
		} else {
			r.smcup()
			r.csi("H")
		}
		r.flush()
Junegunn Choi's avatar
Junegunn Choi committed
func (r *LightRenderer) Resume(clear bool) {
Junegunn Choi's avatar
Junegunn Choi committed
	if clear {
		if r.fullscreen {
			r.smcup()
		} else {
			r.rmcup()
		}
		r.flush()
	} else if !r.fullscreen && r.mouse {
		// NOTE: Resume(false) is only called on SIGCONT after SIGSTOP.
		// And It's highly likely that the offset we obtained at the beginning will
		// no longer be correct, so we simply disable mouse input.
		r.csi("?1000l")
		r.mouse = false
Junegunn Choi's avatar
Junegunn Choi committed
}

func (r *LightRenderer) Clear() {
Junegunn Choi's avatar
Junegunn Choi committed
	if r.fullscreen {
		r.csi("H")
	}
Junegunn Choi's avatar
Junegunn Choi committed
	// r.csi("u")
	r.origin()
Junegunn Choi's avatar
Junegunn Choi committed
	r.csi("J")
	r.flush()
}

func (r *LightRenderer) RefreshWindows(windows []Window) {
	r.flush()
}

func (r *LightRenderer) Refresh() {
	r.updateTerminalSize()
}

func (r *LightRenderer) Close() {
Junegunn Choi's avatar
Junegunn Choi committed
	// r.csi("u")
Junegunn Choi's avatar
Junegunn Choi committed
	if r.clearOnExit {
		if r.fullscreen {
			r.rmcup()
		} else {
			r.origin()
			if r.upOneLine {
				r.csi("A")
			}
			r.csi("J")
Junegunn Choi's avatar
Junegunn Choi committed
	} else if r.fullscreen {
		r.csi("G")
	} else {
		r.move(r.height, 0)
Junegunn Choi's avatar
Junegunn Choi committed
	if r.mouse {
		r.csi("?1000l")
	}
	r.flush()
	terminal.Restore(r.fd(), r.origState)
Junegunn Choi's avatar
Junegunn Choi committed
}

func (r *LightRenderer) MaxX() int {
	return r.width
}

func (r *LightRenderer) MaxY() int {
	return r.height
}

func (r *LightRenderer) DoesAutoWrap() bool {
	return false
func (r *LightRenderer) IsOptimized() bool {
	return false
}

func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
Junegunn Choi's avatar
Junegunn Choi committed
	w := &LightWindow{
		renderer: r,
		colored:  r.theme != nil,
Junegunn Choi's avatar
Junegunn Choi committed
		top:      top,
		left:     left,
		width:    width,
		height:   height,
		tabstop:  r.tabstop,
		bg:       colDefault}
	if r.theme != nil {
		w.bg = r.theme.Bg
	}
Junegunn Choi's avatar
Junegunn Choi committed
	return w
}

func (w *LightWindow) drawBorder() {
	switch w.border {
	case BorderAround:
		w.drawBorderAround()
	case BorderHorizontal:
		w.drawBorderHorizontal()
	}
}

func (w *LightWindow) drawBorderHorizontal() {
	w.Move(0, 0)
	w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
	w.Move(w.height-1, 0)
	w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
}

func (w *LightWindow) drawBorderAround() {
Junegunn Choi's avatar
Junegunn Choi committed
	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) stderrInternal(str string, allowNLCR bool) {
	w.renderer.stderrInternal(str, allowNLCR)
}

Junegunn Choi's avatar
Junegunn Choi committed
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

Junegunn Choi's avatar
Junegunn Choi committed
	w.renderer.move(w.Top()+y, w.Left()+x)
Junegunn Choi's avatar
Junegunn Choi committed
}

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 cleanse(str string) string {
	return strings.Replace(str, "\x1b", "?", -1)
}

Junegunn Choi's avatar
Junegunn Choi committed
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.stderrInternal(cleanse(text), false)
Junegunn Choi's avatar
Junegunn Choi committed
	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)
Junegunn Choi's avatar
Junegunn Choi committed
}

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()) FillReturn {
Junegunn Choi's avatar
Junegunn Choi committed
	allLines := strings.Split(str, "\n")
	for i, line := range allLines {
		lines := wrapLine(line, w.posx, w.width, w.tabstop)
		for j, wl := range lines {
			if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
				if w.posy < w.height-1 {
					w.MoveAndClear(w.posy+1, 0)
				}
				return FillNextLine
			}
			w.stderrInternal(wl.text, false)
Junegunn Choi's avatar
Junegunn Choi committed
			w.posx += wl.displayWidth
			if j < len(lines)-1 || i < len(allLines)-1 {
				if w.posy+1 >= w.height {
Junegunn Choi's avatar
Junegunn Choi committed
				}
				w.MoveAndClear(w.posy+1, 0)
				onMove()
			}
		}
	}
Junegunn Choi's avatar
Junegunn Choi committed
}

func (w *LightWindow) setBg() {
	if w.bg != colDefault {
		w.csiColor(colDefault, w.bg, AttrRegular)
	}
}

func (w *LightWindow) Fill(text string) FillReturn {
Junegunn Choi's avatar
Junegunn Choi committed
	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) FillReturn {
Junegunn Choi's avatar
Junegunn Choi committed
	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() {
Junegunn Choi's avatar
Junegunn Choi committed
	// We don't erase the window here to avoid flickering during scroll
	w.Move(0, 0)
}