Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
terminal.go 42.99 KiB
package fzf

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"os/signal"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

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

// import "github.com/pkg/profile"

var placeholder *regexp.Regexp

func init() {
	placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
}

type jumpMode int

const (
	jumpDisabled jumpMode = iota
	jumpEnabled
	jumpAcceptEnabled
)

type previewer struct {
	text    string
	lines   int
	offset  int
	enabled bool
}

type itemLine struct {
	current  bool
	selected bool
	label    string
	queryLen int
	width    int
	result   Result
}

var emptyLine = itemLine{}

// Terminal represents terminal input/output
type Terminal struct {
	initDelay  time.Duration
	inlineInfo bool
	prompt     string
	promptLen  int
	reverse    bool
	fullscreen bool
	hscroll    bool
	hscrollOff int
	wordRubout string
	wordNext   string
	cx         int
	cy         int
	offset     int
	yanked     []rune
	input      []rune
	multi      bool
	sort       bool
	toggleSort bool
	delimiter  Delimiter
	expect     map[int]string
	keymap     map[int][]action
	pressed    string
	printQuery bool
	history    *History
	cycle      bool
	header     []string
	header0    []string
	ansi       bool
	tabstop    int
	margin     [4]sizeSpec
	strong     tui.Attr
	bordered   bool
	cleanExit  bool
	border     tui.Window
	window     tui.Window
	pborder    tui.Window
	pwindow    tui.Window
	count      int
	progress   int
	reading    bool
	success    bool
	jumping    jumpMode
	jumpLabels string
	printer    func(string)
	merger     *Merger
	selected   map[int32]selectedItem
	version    int64
	reqBox     *util.EventBox
	preview    previewOpts
	previewer  previewer
	previewBox *util.EventBox
	eventBox   *util.EventBox
	mutex      sync.Mutex
	initFunc   func()
	prevLines  []itemLine
	suppress   bool
	startChan  chan bool
	slab       *util.Slab
	theme      *tui.ColorTheme
	tui        tui.Renderer
}

type selectedItem struct {
	at   time.Time
	item *Item
}

type byTimeOrder []selectedItem

func (a byTimeOrder) Len() int {
	return len(a)
}

func (a byTimeOrder) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

func (a byTimeOrder) Less(i, j int) bool {
	return a[i].at.Before(a[j].at)
}

var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
const (
	reqPrompt util.EventType = iota
	reqInfo
	reqHeader
	reqList
	reqJump
	reqRefresh
	reqReinit
	reqRedraw
	reqClose
	reqPrintQuery
	reqPreviewEnqueue
	reqPreviewDisplay
	reqPreviewRefresh
	reqQuit
)

type action struct {
	t actionType
	a string
}

type actionType int

const (
	actIgnore actionType = iota
	actInvalid
	actRune
	actMouse
	actBeginningOfLine
	actAbort
	actAccept
	actBackwardChar
	actBackwardDeleteChar
	actBackwardWord
	actCancel
	actClearScreen
	actDeleteChar
	actDeleteCharEOF
	actEndOfLine
	actForwardChar
	actForwardWord
	actKillLine
	actKillWord
	actUnixLineDiscard
	actUnixWordRubout
	actYank
	actBackwardKillWord
	actSelectAll
	actDeselectAll
	actToggle
	actToggleAll
	actToggleDown
	actToggleUp
	actToggleIn
	actToggleOut
	actDown
	actUp
	actPageUp
	actPageDown
	actHalfPageUp
	actHalfPageDown
	actJump
	actJumpAccept
	actPrintQuery
	actToggleSort
	actTogglePreview
	actTogglePreviewWrap
	actPreviewUp
	actPreviewDown
	actPreviewPageUp
	actPreviewPageDown
	actPreviousHistory
	actNextHistory
	actExecute
	actExecuteSilent
	actExecuteMulti // Deprecated
	actSigStop
	actTop
)

func toActions(types ...actionType) []action {
	actions := make([]action, len(types))
	for idx, t := range types {
		actions[idx] = action{t: t, a: ""}
	}
	return actions
}

func defaultKeymap() map[int][]action {
	keymap := make(map[int][]action)
	keymap[tui.Invalid] = toActions(actInvalid)
	keymap[tui.Resize] = toActions(actClearScreen)
	keymap[tui.CtrlA] = toActions(actBeginningOfLine)
	keymap[tui.CtrlB] = toActions(actBackwardChar)
	keymap[tui.CtrlC] = toActions(actAbort)
	keymap[tui.CtrlG] = toActions(actAbort)
	keymap[tui.CtrlQ] = toActions(actAbort)
	keymap[tui.ESC] = toActions(actAbort)
	keymap[tui.CtrlD] = toActions(actDeleteCharEOF)
	keymap[tui.CtrlE] = toActions(actEndOfLine)
	keymap[tui.CtrlF] = toActions(actForwardChar)
	keymap[tui.CtrlH] = toActions(actBackwardDeleteChar)
	keymap[tui.BSpace] = toActions(actBackwardDeleteChar)
	keymap[tui.Tab] = toActions(actToggleDown)
	keymap[tui.BTab] = toActions(actToggleUp)
	keymap[tui.CtrlJ] = toActions(actDown)
	keymap[tui.CtrlK] = toActions(actUp)
	keymap[tui.CtrlL] = toActions(actClearScreen)
	keymap[tui.CtrlM] = toActions(actAccept)
	keymap[tui.CtrlN] = toActions(actDown)
	keymap[tui.CtrlP] = toActions(actUp)
	keymap[tui.CtrlU] = toActions(actUnixLineDiscard)
	keymap[tui.CtrlW] = toActions(actUnixWordRubout)
	keymap[tui.CtrlY] = toActions(actYank)
	if !util.IsWindows() {
		keymap[tui.CtrlZ] = toActions(actSigStop)
	}

	keymap[tui.AltB] = toActions(actBackwardWord)
	keymap[tui.SLeft] = toActions(actBackwardWord)
	keymap[tui.AltF] = toActions(actForwardWord)
	keymap[tui.SRight] = toActions(actForwardWord)
	keymap[tui.AltD] = toActions(actKillWord)
	keymap[tui.AltBS] = toActions(actBackwardKillWord)

	keymap[tui.Up] = toActions(actUp)
	keymap[tui.Down] = toActions(actDown)
	keymap[tui.Left] = toActions(actBackwardChar)
	keymap[tui.Right] = toActions(actForwardChar)

	keymap[tui.Home] = toActions(actBeginningOfLine)
	keymap[tui.End] = toActions(actEndOfLine)
	keymap[tui.Del] = toActions(actDeleteChar)
	keymap[tui.PgUp] = toActions(actPageUp)
	keymap[tui.PgDn] = toActions(actPageDown)

	keymap[tui.Rune] = toActions(actRune)
	keymap[tui.Mouse] = toActions(actMouse)
	keymap[tui.DoubleClick] = toActions(actAccept)
	return keymap
}

func trimQuery(query string) []rune {
	return []rune(strings.Replace(query, "\t", " ", -1))
}

// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
	input := trimQuery(opts.Query)
	var header []string
	if opts.Reverse {
		header = opts.Header
	} else {
		header = reverseStringArray(opts.Header)
	}
	var delay time.Duration
	if opts.Tac {
		delay = initialDelayTac
	} else {
		delay = initialDelay
	}
	var previewBox *util.EventBox
	if len(opts.Preview.command) > 0 {
		previewBox = util.NewEventBox()
	}
	strongAttr := tui.Bold
	if !opts.Bold {
		strongAttr = tui.AttrRegular
	}
	var renderer tui.Renderer
	fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100
	if fullscreen {
		if tui.HasFullscreenRenderer() {
			renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
		} else {
			renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
				true, func(h int) int { return h })
		}
	} else {
		maxHeightFunc := func(termHeight int) int {
			var maxHeight int
			if opts.Height.percent {
				maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
			} else {
				maxHeight = int(opts.Height.size)
			}

			effectiveMinHeight := minHeight
			if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
				effectiveMinHeight *= 2
			}
			if opts.InlineInfo {
				effectiveMinHeight -= 1
			}
			if opts.Bordered {
				effectiveMinHeight += 2
			}
			return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
		}
		renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
	}
	wordRubout := "[^[:alnum:]][[:alnum:]]"
	wordNext := "[[:alnum:]][^[:alnum:]]|(.$)"
	if opts.FileWord {
		sep := regexp.QuoteMeta(string(os.PathSeparator))
		wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
		wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
	}
	t := Terminal{
		initDelay:  delay,
		inlineInfo: opts.InlineInfo,
		reverse:    opts.Reverse,
		fullscreen: fullscreen,
		hscroll:    opts.Hscroll,
		hscrollOff: opts.HscrollOff,
		wordRubout: wordRubout,
		wordNext:   wordNext,
		cx:         len(input),
		cy:         0,
		offset:     0,
		yanked:     []rune{},
		input:      input,
		multi:      opts.Multi,
		sort:       opts.Sort > 0,
		toggleSort: opts.ToggleSort,
		delimiter:  opts.Delimiter,
		expect:     opts.Expect,
		keymap:     opts.Keymap,
		pressed:    "",
		printQuery: opts.PrintQuery,
		history:    opts.History,
		margin:     opts.Margin,
		bordered:   opts.Bordered,
		cleanExit:  opts.ClearOnExit,
		strong:     strongAttr,
		cycle:      opts.Cycle,
		header:     header,
		header0:    header,
		ansi:       opts.Ansi,
		tabstop:    opts.Tabstop,
		reading:    true,
		success:    true,
		jumping:    jumpDisabled,
		jumpLabels: opts.JumpLabels,
		printer:    opts.Printer,
		merger:     EmptyMerger,
		selected:   make(map[int32]selectedItem),
		reqBox:     util.NewEventBox(),
		preview:    opts.Preview,
		previewer:  previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
		previewBox: previewBox,
		eventBox:   eventBox,
		mutex:      sync.Mutex{},
		suppress:   true,
		slab:       util.MakeSlab(slab16Size, slab32Size),
		theme:      opts.Theme,
		startChan:  make(chan bool, 1),
		tui:        renderer,
		initFunc:   func() { renderer.Init() }}
	t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0)
	return &t
}

// Input returns current query string
func (t *Terminal) Input() []rune {
	t.mutex.Lock()
	defer t.mutex.Unlock()
	return copySlice(t.input)
}

// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
	t.mutex.Lock()
	t.count = cnt
	t.reading = !final
	t.success = success
	t.mutex.Unlock()
	t.reqBox.Set(reqInfo, nil)
	if final {
		t.reqBox.Set(reqRefresh, nil)
	}
}

func reverseStringArray(input []string) []string {
	size := len(input)
	reversed := make([]string, size)
	for idx, str := range input {
		reversed[size-idx-1] = str
	}
	return reversed
}

// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) {
	t.mutex.Lock()
	t.header = append(append([]string{}, t.header0...), header...)
	t.mutex.Unlock()
	t.reqBox.Set(reqHeader, nil)
}

// UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) {
	t.mutex.Lock()
	newProgress := int(progress * 100)
	changed := t.progress != newProgress
	t.progress = newProgress
	t.mutex.Unlock()

	if changed {
		t.reqBox.Set(reqInfo, nil)
	}
}

// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
	t.mutex.Lock()
	t.progress = 100
	t.merger = merger
	t.mutex.Unlock()
	t.reqBox.Set(reqInfo, nil)
	t.reqBox.Set(reqList, nil)
}

func (t *Terminal) output() bool {
	if t.printQuery {
		t.printer(string(t.input))
	}
	if len(t.expect) > 0 {
		t.printer(t.pressed)
	}
	found := len(t.selected) > 0
	if !found {
		current := t.currentItem()
		if current != nil {
			t.printer(current.AsString(t.ansi))
			found = true
		}
	} else {
		for _, sel := range t.sortSelected() {
			t.printer(sel.item.AsString(t.ansi))
		}
	}
	return found
}

func (t *Terminal) sortSelected() []selectedItem {
	sels := make([]selectedItem, 0, len(t.selected))
	for _, sel := range t.selected {
		sels = append(sels, sel)
	}
	sort.Sort(byTimeOrder(sels))
	return sels
}

func (t *Terminal) displayWidth(runes []rune) int {
	l := 0
	for _, r := range runes {
		l += util.RuneWidth(r, l, t.tabstop)
	}
	return l
}

const (
	minWidth  = 16
	minHeight = 4

	maxDisplayWidthCalc = 1024
)

func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
	max := base - margin
	if size.percent {
		return util.Constrain(int(float64(base)*0.01*size.size), minSize, max)
	}
	return util.Constrain(int(size.size), minSize, max)
}

func (t *Terminal) resizeWindows() {
	screenWidth := t.tui.MaxX()
	screenHeight := t.tui.MaxY()
	marginInt := [4]int{}
	t.prevLines = make([]itemLine, screenHeight)
	for idx, sizeSpec := range t.margin {
		if sizeSpec.percent {
			var max float64
			if idx%2 == 0 {
				max = float64(screenHeight)
			} else {
				max = float64(screenWidth)
			}
			marginInt[idx] = int(max * sizeSpec.size * 0.01)
		} else {
			marginInt[idx] = int(sizeSpec.size)
		}
		if t.bordered && idx%2 == 0 {
			marginInt[idx] += 1
		}
	}
	adjust := func(idx1 int, idx2 int, max int, min int) {
		if max >= min {
			margin := marginInt[idx1] + marginInt[idx2]
			if max-margin < min {
				desired := max - min
				marginInt[idx1] = desired * marginInt[idx1] / margin
				marginInt[idx2] = desired * marginInt[idx2] / margin
			}
		}
	}

	previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
	minAreaWidth := minWidth
	minAreaHeight := minHeight
	if previewVisible {
		switch t.preview.position {
		case posUp, posDown:
			minAreaHeight *= 2
		case posLeft, posRight:
			minAreaWidth *= 2
		}
	}
	adjust(1, 3, screenWidth, minAreaWidth)
	adjust(0, 2, screenHeight, minAreaHeight)
	if t.border != nil {
		t.border.Close()
	}
	if t.window != nil {
		t.window.Close()
	}
	if t.pborder != nil {
		t.pborder.Close()
		t.pwindow.Close()
	}

	width := screenWidth - marginInt[1] - marginInt[3]
	height := screenHeight - marginInt[0] - marginInt[2]
	if t.bordered {
		t.border = t.tui.NewWindow(
			marginInt[0]-1,
			marginInt[3],
			width,
			height+2, tui.BorderHorizontal)
	}
	if previewVisible {
		createPreviewWindow := func(y int, x int, w int, h int) {
			t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround)
			pwidth := w - 4
			// ncurses auto-wraps the line when the cursor reaches the right-end of
			// the window. To prevent unintended line-wraps, we use the width one
			// column larger than the desired value.
			if !t.preview.wrap && t.tui.DoesAutoWrap() {
				pwidth += 1
			}
			t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone)
			os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2))
		}
		switch t.preview.position {
		case posUp:
			pheight := calculateSize(height, t.preview.size, minHeight, 3)
			t.window = t.tui.NewWindow(
				marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone)
			createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
		case posDown:
			pheight := calculateSize(height, t.preview.size, minHeight, 3)
			t.window = t.tui.NewWindow(
				marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone)
			createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
		case posLeft:
			pwidth := calculateSize(width, t.preview.size, minWidth, 5)
			t.window = t.tui.NewWindow(
				marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone)
			createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
		case posRight:
			pwidth := calculateSize(width, t.preview.size, minWidth, 5)
			t.window = t.tui.NewWindow(
				marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone)
			createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
		}
	} else {
		t.window = t.tui.NewWindow(
			marginInt[0],
			marginInt[3],
			width,
			height, tui.BorderNone)
	}
	for i := 0; i < t.window.Height(); i++ {
		t.window.MoveAndClear(i, 0)
	}
	t.truncateQuery()
}

func (t *Terminal) move(y int, x int, clear bool) {
	if !t.reverse {
		y = t.window.Height() - y - 1
	}

	if clear {
		t.window.MoveAndClear(y, x)
	} else {
		t.window.Move(y, x)
	}
}

func (t *Terminal) placeCursor() {
	t.move(0, t.promptLen+t.displayWidth(t.input[:t.cx]), false)
}

func (t *Terminal) printPrompt() {
	t.move(0, 0, true)
	t.window.CPrint(tui.ColPrompt, t.strong, t.prompt)
	t.window.CPrint(tui.ColNormal, t.strong, string(t.input))
}

func (t *Terminal) printInfo() {
	pos := 0
	if t.inlineInfo {
		pos = t.promptLen + t.displayWidth(t.input) + 1
		if pos+len(" < ") > t.window.Width() {
			return
		}
		t.move(0, pos, true)
		if t.reading {
			t.window.CPrint(tui.ColSpinner, t.strong, " < ")
		} else {
			t.window.CPrint(tui.ColPrompt, t.strong, " < ")
		}
		pos += len(" < ")
	} else {
		t.move(1, 0, true)
		if t.reading {
			duration := int64(spinnerDuration)
			idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
			t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
		}
		t.move(1, 2, false)
		pos = 2
	}

	output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
	if t.toggleSort {
		if t.sort {
			output += " +S"
		} else {
			output += " -S"
		}
	}
	if t.multi && len(t.selected) > 0 {
		output += fmt.Sprintf(" (%d)", len(t.selected))
	}
	if t.progress > 0 && t.progress < 100 {
		output += fmt.Sprintf(" (%d%%)", t.progress)
	}
	if !t.success && t.count == 0 {
		output += " [ERROR]"
	}
	if pos+len(output) <= t.window.Width() {
		t.window.CPrint(tui.ColInfo, 0, output)
	}
}
func (t *Terminal) printHeader() {
	if len(t.header) == 0 {
		return
	}
	max := t.window.Height()
	var state *ansiState
	for idx, lineStr := range t.header {
		line := idx + 2
		if t.inlineInfo {
			line--
		}
		if line >= max {
			continue
		}
		trimmed, colors, newState := extractColor(lineStr, state, nil)
		state = newState
		item := &Item{
			text:   util.ToChars([]byte(trimmed)),
			colors: colors}

		t.move(line, 2, true)
		t.printHighlighted(Result{item: item},
			tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false)
	}
}

func (t *Terminal) printList() {
	t.constrain()

	maxy := t.maxItems()
	count := t.merger.Length() - t.offset
	for j := 0; j < maxy; j++ {
		i := j
		if !t.reverse {
			i = maxy - 1 - j
		}
		line := i + 2 + len(t.header)
		if t.inlineInfo {
			line--
		}
		if i < count {
			t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset)
		} else if t.prevLines[i] != emptyLine {
			t.prevLines[i] = emptyLine
			t.move(line, 0, true)
		}
	}
}

func (t *Terminal) printItem(result Result, line int, i int, current bool) {
	item := result.item
	_, selected := t.selected[item.Index()]
	label := " "
	if t.jumping != jumpDisabled {
		if i < len(t.jumpLabels) {
			// Striped
			current = i%2 == 0
			label = t.jumpLabels[i : i+1]
		}
	} else if current {
		label = ">"
	}

	// Avoid unnecessary redraw
	newLine := itemLine{current: current, selected: selected, label: label,
		result: result, queryLen: len(t.input), width: 0}
	prevLine := t.prevLines[i]
	if prevLine.current == newLine.current &&
		prevLine.selected == newLine.selected &&
		prevLine.label == newLine.label &&
		prevLine.queryLen == newLine.queryLen &&
		prevLine.result == newLine.result {
		return
	}

	t.move(line, 0, false)
	t.window.CPrint(tui.ColCursor, t.strong, label)
	if current {
		if selected {
			t.window.CPrint(tui.ColSelected, t.strong, ">")
		} else {
			t.window.CPrint(tui.ColCurrent, t.strong, " ")
		}
		newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
	} else {
		if selected {
			t.window.CPrint(tui.ColSelected, t.strong, ">")
		} else {
			t.window.Print(" ")
		}
		newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true)
	}
	fillSpaces := prevLine.width - newLine.width
	if fillSpaces > 0 {
		t.window.Print(strings.Repeat(" ", fillSpaces))
	}
	t.prevLines[i] = newLine
}

func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) {
	// We start from the beginning to handle tab characters
	l := 0
	for idx, r := range runes {
		l += util.RuneWidth(r, l, t.tabstop)
		if l > width {
			return runes[:idx], len(runes) - idx
		}
	}
	return runes, 0
}

func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
	l := 0
	for _, r := range runes {
		l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
		if l > limit {
			// Early exit
			return l
		}
	}
	return l
}

func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
	if len(runes) > maxDisplayWidthCalc && len(runes) > width {
		trimmed := len(runes) - width
		return runes[trimmed:], int32(trimmed)
	}

	currentWidth := t.displayWidth(runes)
	var trimmed int32

	for currentWidth > width && len(runes) > 0 {
		runes = runes[1:]
		trimmed++
		currentWidth = t.displayWidthWithLimit(runes, 2, width)
	}
	return runes, trimmed
}
func (t *Terminal) overflow(runes []rune, max int) bool {
	return t.displayWidthWithLimit(runes, 0, max) > max
}

func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
	item := result.item

	// Overflow
	text := make([]rune, item.text.Length())
	copy(text, item.text.ToRunes())
	matchOffsets := []Offset{}
	var pos *[]int
	if match && t.merger.pattern != nil {
		_, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab)
	}
	charOffsets := matchOffsets
	if pos != nil {
		charOffsets = make([]Offset, len(*pos))
		for idx, p := range *pos {
			offset := Offset{int32(p), int32(p + 1)}
			charOffsets[idx] = offset
		}
		sort.Sort(ByOrder(charOffsets))
	}
	var maxe int
	for _, offset := range charOffsets {
		maxe = util.Max(maxe, int(offset[1]))
	}

	offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current)
	maxWidth := t.window.Width() - 3
	maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
	displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
	if displayWidth > maxWidth {
		if t.hscroll {
			// Stri..
			if !t.overflow(text[:maxe], maxWidth-2) {
				text, _ = t.trimRight(text, maxWidth-2)
				text = append(text, []rune("..")...)
			} else {
				// Stri..
				if t.overflow(text[maxe:], 2) {
					text = append(text[:maxe], []rune("..")...)
				}
				// ..ri..
				var diff int32
				text, diff = t.trimLeft(text, maxWidth-2)

				// Transform offsets
				for idx, offset := range offsets {
					b, e := offset.offset[0], offset.offset[1]
					b += 2 - diff
					e += 2 - diff
					b = util.Max32(b, 2)
					offsets[idx].offset[0] = b
					offsets[idx].offset[1] = util.Max32(b, e)
				}
				text = append([]rune(".."), text...)
			}
		} else {
			text, _ = t.trimRight(text, maxWidth-2)
			text = append(text, []rune("..")...)

			for idx, offset := range offsets {
				offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
				offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
			}
		}
		displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
	}

	var index int32
	var substr string
	var prefixWidth int
	maxOffset := int32(len(text))
	for _, offset := range offsets {
		b := util.Constrain32(offset.offset[0], index, maxOffset)
		e := util.Constrain32(offset.offset[1], index, maxOffset)

		substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
		t.window.CPrint(col1, attr, substr)

		if b < e {
			substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
			t.window.CPrint(offset.color, offset.attr, substr)
		}

		index = e
		if index >= maxOffset {
			break
		}
	}
	if index < maxOffset {
		substr, _ = t.processTabs(text[index:], prefixWidth)
		t.window.CPrint(col1, attr, substr)
	}
	return displayWidth
}

func numLinesMax(str string, max int) int {
	lines := 0
	for lines < max {
		idx := strings.Index(str, "\n")
		if idx < 0 {
			break
		}
		str = str[idx+1:]
		lines++
	}
	return lines
}

func (t *Terminal) printPreview() {
	if !t.hasPreviewWindow() {
		return
	}
	t.pwindow.Erase()

	maxWidth := t.pwindow.Width()
	if t.tui.DoesAutoWrap() {
		maxWidth -= 1
	}
	reader := bufio.NewReader(strings.NewReader(t.previewer.text))
	lineNo := -t.previewer.offset
	height := t.pwindow.Height()
	var ansi *ansiState
	for {
		line, err := reader.ReadString('\n')
		eof := err == io.EOF
		if !eof {
			line = line[:len(line)-1]
		}
		lineNo++
		if lineNo > height ||
			t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
			break
		} else if lineNo > 0 {
			var fillRet tui.FillReturn
			_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
				trimmed := []rune(str)
				if !t.preview.wrap {
					trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
				}
				str, _ = t.processTabs(trimmed, 0)
				if t.theme != nil && ansi != nil && ansi.colored() {
					fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
				} else {
					fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str)
				}
				return fillRet == tui.FillContinue
			})
			switch fillRet {
			case tui.FillNextLine:
				continue
			case tui.FillSuspend:
				break
			}
			t.pwindow.Fill("\n")
		}
		if eof {
			break
		}
	}
	t.pwindow.FinishFill()
	if t.previewer.lines > height {
		offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
		pos := t.pwindow.Width() - len(offset)
		if t.tui.DoesAutoWrap() {
			pos -= 1
		}
		t.pwindow.Move(0, pos)
		t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
	}
}

func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
	var strbuf bytes.Buffer
	l := prefixWidth
	for _, r := range runes {
		w := util.RuneWidth(r, l, t.tabstop)
		l += w
		if r == '\t' {
			strbuf.WriteString(strings.Repeat(" ", w))
		} else {
			strbuf.WriteRune(r)
		}
	}
	return strbuf.String(), l
}

func (t *Terminal) printAll() {
	t.resizeWindows()
	t.printList()
	t.printPrompt()
	t.printInfo()
	t.printHeader()
	t.printPreview()
}

func (t *Terminal) refresh() {
	if !t.suppress {
		windows := make([]tui.Window, 0, 4)
		if t.bordered {
			windows = append(windows, t.border)
		}
		if t.hasPreviewWindow() {
			windows = append(windows, t.pborder, t.pwindow)
		}
		windows = append(windows, t.window)
		t.tui.RefreshWindows(windows)
	}
}

func (t *Terminal) delChar() bool {
	if len(t.input) > 0 && t.cx < len(t.input) {
		t.input = append(t.input[:t.cx], t.input[t.cx+1:]...)
		return true
	}
	return false
}

func findLastMatch(pattern string, str string) int {
	rx, err := regexp.Compile(pattern)
	if err != nil {
		return -1
	}
	locs := rx.FindAllStringIndex(str, -1)
	if locs == nil {
		return -1
	}
	return locs[len(locs)-1][0]
}

func findFirstMatch(pattern string, str string) int {
	rx, err := regexp.Compile(pattern)
	if err != nil {
		return -1
	}
	loc := rx.FindStringIndex(str)
	if loc == nil {
		return -1
	}
	return loc[0]
}

func copySlice(slice []rune) []rune {
	ret := make([]rune, len(slice))
	copy(ret, slice)
	return ret
}

func (t *Terminal) rubout(pattern string) {
	pcx := t.cx
	after := t.input[t.cx:]
	t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1
	t.yanked = copySlice(t.input[t.cx:pcx])
	t.input = append(t.input[:t.cx], after...)
}

func keyMatch(key int, event tui.Event) bool {
	return event.Type == key ||
		event.Type == tui.Rune && int(event.Char) == key-tui.AltZ ||
		event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
}

func quoteEntryCmd(entry string) string {
	escaped := strings.Replace(entry, `\`, `\\`, -1)
	escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
	r, _ := regexp.Compile(`[&|<>()@^%!"]`)
	return r.ReplaceAllStringFunc(escaped, func(match string) string {
		return "^" + match
	})
}

func quoteEntry(entry string) string {
	if util.IsWindows() {
		return quoteEntryCmd(entry)
	}
	return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}

func hasPlusFlag(template string) bool {
	for _, match := range placeholder.FindAllString(template, -1) {
		if match[0] == '\\' {
			continue
		}
		if match[1] == '+' {
			return true
		}
	}
	return false
}

func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
	current := allItems[:1]
	selected := allItems[1:]
	if current[0] == nil {
		current = []*Item{}
	}
	if selected[0] == nil {
		selected = []*Item{}
	}
	return placeholder.ReplaceAllStringFunc(template, func(match string) string {
		// Escaped pattern
		if match[0] == '\\' {
			return match[1:]
		}

		// Current query
		if match == "{q}" {
			return quoteEntry(query)
		}

		plusFlag := forcePlus
		if match[1] == '+' {
			match = "{" + match[2:]
			plusFlag = true
		}
		items := current
		if plusFlag {
			items = selected
		}

		replacements := make([]string, len(items))

		if match == "{}" {
			for idx, item := range items {
				replacements[idx] = quoteEntry(item.AsString(stripAnsi))
			}
			return strings.Join(replacements, " ")
		}

		tokens := strings.Split(match[1:len(match)-1], ",")
		ranges := make([]Range, len(tokens))
		for idx, s := range tokens {
			r, ok := ParseRange(&s)
			if !ok {
				// Invalid expression, just return the original string in the template
				return match
			}
			ranges[idx] = r
		}

		for idx, item := range items {
			tokens := Tokenize(item.AsString(stripAnsi), delimiter)
			trans := Transform(tokens, ranges)
			str := string(joinTokens(trans))
			if delimiter.str != nil {
				str = strings.TrimSuffix(str, *delimiter.str)
			} else if delimiter.regex != nil {
				delims := delimiter.regex.FindAllStringIndex(str, -1)
				if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
					str = str[:delims[len(delims)-1][0]]
				}
			}
			str = strings.TrimSpace(str)
			replacements[idx] = quoteEntry(str)
		}
		return strings.Join(replacements, " ")
	})
}

func (t *Terminal) redraw() {
	t.tui.Clear()
	t.tui.Refresh()
	t.printAll()
}

func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) {
	valid, list := t.buildPlusList(template, forcePlus)
	if !valid {
		return
	}
	command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
	cmd := util.ExecCommand(command)
	if !background {
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		t.tui.Pause(true)
		cmd.Run()
		t.tui.Resume(true)
		t.redraw()
		t.refresh()
	} else {
		cmd.Run()
	}
}

func (t *Terminal) hasPreviewer() bool {
	return t.previewBox != nil
}

func (t *Terminal) isPreviewEnabled() bool {
	return t.hasPreviewer() && t.previewer.enabled
}

func (t *Terminal) hasPreviewWindow() bool {
	return t.pwindow != nil && t.isPreviewEnabled()
}

func (t *Terminal) currentItem() *Item {
	cnt := t.merger.Length()
	if cnt > 0 && cnt > t.cy {
		return t.merger.Get(t.cy).item
	}
	return nil
}

func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
	current := t.currentItem()
	if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
		return current != nil, []*Item{current, current}
	}
	sels := make([]*Item, len(t.selected)+1)
	sels[0] = current
	for i, sel := range t.sortSelected() {
		sels[i+1] = sel.item
	}
	return true, sels
}

func (t *Terminal) truncateQuery() {
	maxPatternLength := util.Max(1, t.window.Width()-t.promptLen-1)
	t.input, _ = t.trimRight(t.input, maxPatternLength)
	t.cx = util.Constrain(t.cx, 0, len(t.input))
}

func (t *Terminal) selectItem(item *Item) {
	t.selected[item.Index()] = selectedItem{time.Now(), item}
	t.version++
}

func (t *Terminal) deselectItem(item *Item) {
	delete(t.selected, item.Index())
	t.version++
}

func (t *Terminal) toggleItem(item *Item) {
	if _, found := t.selected[item.Index()]; !found {
		t.selectItem(item)
	} else {
		t.deselectItem(item)
	}
}

// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
	// prof := profile.Start(profile.ProfilePath("/tmp/"))
	<-t.startChan
	{ // Late initialization
		intChan := make(chan os.Signal, 1)
		signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM)
		go func() {
			<-intChan
			t.reqBox.Set(reqQuit, nil)
		}()

		contChan := make(chan os.Signal, 1)
		notifyOnCont(contChan)
		go func() {
			for {
				<-contChan
				t.reqBox.Set(reqReinit, nil)
			}
		}()

		resizeChan := make(chan os.Signal, 1)
		notifyOnResize(resizeChan) // Non-portable
		go func() {
			for {
				<-resizeChan
				t.reqBox.Set(reqRedraw, nil)
			}
		}()

		t.mutex.Lock()
		t.initFunc()
		t.resizeWindows()
		t.printPrompt()
		t.placeCursor()
		t.refresh()
		t.printInfo()
		t.printHeader()
		t.mutex.Unlock()
		go func() {
			timer := time.NewTimer(t.initDelay)
			<-timer.C
			t.reqBox.Set(reqRefresh, nil)
		}()

		// Keep the spinner spinning
		go func() {
			for {
				t.mutex.Lock()
				reading := t.reading
				t.mutex.Unlock()
				if !reading {
					break
				}
				time.Sleep(spinnerDuration)
				t.reqBox.Set(reqInfo, nil)
			}
		}()
	}

	if t.hasPreviewer() {
		go func() {
			for {
				var request []*Item
				t.previewBox.Wait(func(events *util.Events) {
					for req, value := range *events {
						switch req {
						case reqPreviewEnqueue:
							request = value.([]*Item)
						}
					}
					events.Clear()
				})
				// We don't display preview window if no match
				if request[0] != nil {
					command := replacePlaceholder(t.preview.command,
						t.ansi, t.delimiter, false, string(t.input), request)
					cmd := util.ExecCommand(command)
					out, _ := cmd.CombinedOutput()
					t.reqBox.Set(reqPreviewDisplay, string(out))
				} else {
					t.reqBox.Set(reqPreviewDisplay, "")
				}
			}
		}()
	}

	exit := func(getCode func() int) {
		if !t.cleanExit && t.fullscreen && t.inlineInfo {
			t.placeCursor()
		}
		t.tui.Close()
		code := getCode()
		if code <= exitNoMatch && t.history != nil {
			t.history.append(string(t.input))
		}
		// prof.Stop()
		os.Exit(code)
	}

	go func() {
		var focused *Item
		var version int64
		for {
			t.reqBox.Wait(func(events *util.Events) {
				defer events.Clear()
				t.mutex.Lock()
				for req, value := range *events {
					switch req {
					case reqPrompt:
						t.printPrompt()
						if t.inlineInfo {
							t.printInfo()
						}
					case reqInfo:
						t.printInfo()
					case reqList:
						t.printList()
						currentFocus := t.currentItem()
						if currentFocus != focused || version != t.version {
							version = t.version
							focused = currentFocus
							if t.isPreviewEnabled() {
								_, list := t.buildPlusList(t.preview.command, false)
								t.previewBox.Set(reqPreviewEnqueue, list)
							}
						}
					case reqJump:
						if t.merger.Length() == 0 {
							t.jumping = jumpDisabled
						}
						t.printList()
					case reqHeader:
						t.printHeader()
					case reqRefresh:
						t.suppress = false
					case reqReinit:
						t.tui.Resume(t.fullscreen)
						t.redraw()
					case reqRedraw:
						t.redraw()
					case reqClose:
						exit(func() int {
							if t.output() {
								return exitOk
							}
							return exitNoMatch
						})
					case reqPreviewDisplay:
						t.previewer.text = value.(string)
						t.previewer.lines = strings.Count(t.previewer.text, "\n")
						t.previewer.offset = 0
						t.printPreview()
					case reqPreviewRefresh:
						t.printPreview()
					case reqPrintQuery:
						exit(func() int {
							t.printer(string(t.input))
							return exitOk
						})
					case reqQuit:
						exit(func() int { return exitInterrupt })
					}
				}
				t.placeCursor()
				t.mutex.Unlock()
			})
			t.refresh()
		}
	}()

	looping := true
	for looping {
		event := t.tui.GetChar()

		t.mutex.Lock()
		previousInput := t.input
		events := []util.EventType{reqPrompt}
		req := func(evts ...util.EventType) {
			for _, event := range evts {
				events = append(events, event)
				if event == reqClose || event == reqQuit {
					looping = false
				}
			}
		}
		toggle := func() {
			if t.cy < t.merger.Length() {
				t.toggleItem(t.merger.Get(t.cy).item)
				req(reqInfo)
			}
		}
		scrollPreview := func(amount int) {
			t.previewer.offset = util.Constrain(
				t.previewer.offset+amount, 0, t.previewer.lines-1)
			req(reqPreviewRefresh)
		}
		for key, ret := range t.expect {
			if keyMatch(key, event) {
				t.pressed = ret
				t.reqBox.Set(reqClose, nil)
				t.mutex.Unlock()
				return
			}
		}

		var doAction func(action, int) bool
		doActions := func(actions []action, mapkey int) bool {
			for _, action := range actions {
				if !doAction(action, mapkey) {
					return false
				}
			}
			return true
		}
		doAction = func(a action, mapkey int) bool {
			switch a.t {
			case actIgnore:
			case actExecute, actExecuteSilent:
				t.executeCommand(a.a, false, a.t == actExecuteSilent)
			case actExecuteMulti:
				t.executeCommand(a.a, true, false)
			case actInvalid:
				t.mutex.Unlock()
				return false
			case actTogglePreview:
				if t.hasPreviewer() {
					t.previewer.enabled = !t.previewer.enabled
					t.tui.Clear()
					t.resizeWindows()
					if t.previewer.enabled {
						valid, list := t.buildPlusList(t.preview.command, false)
						if valid {
							t.previewBox.Set(reqPreviewEnqueue, list)
						}
					}
					req(reqList, reqInfo, reqHeader)
				}
			case actTogglePreviewWrap:
				if t.hasPreviewWindow() {
					t.preview.wrap = !t.preview.wrap
					req(reqPreviewRefresh)
				}
			case actToggleSort:
				t.sort = !t.sort
				t.eventBox.Set(EvtSearchNew, t.sort)
				t.mutex.Unlock()
				return false
			case actPreviewUp:
				if t.hasPreviewWindow() {
					scrollPreview(-1)
				}
			case actPreviewDown:
				if t.hasPreviewWindow() {
					scrollPreview(1)
				}
			case actPreviewPageUp:
				if t.hasPreviewWindow() {
					scrollPreview(-t.pwindow.Height())
				}
			case actPreviewPageDown:
				if t.hasPreviewWindow() {
					scrollPreview(t.pwindow.Height())
				}
			case actBeginningOfLine:
				t.cx = 0
			case actBackwardChar:
				if t.cx > 0 {
					t.cx--
				}
			case actPrintQuery:
				req(reqPrintQuery)
			case actAbort:
				req(reqQuit)
			case actDeleteChar:
				t.delChar()
			case actDeleteCharEOF:
				if !t.delChar() && t.cx == 0 {
					req(reqQuit)
				}
			case actEndOfLine:
				t.cx = len(t.input)
			case actCancel:
				if len(t.input) == 0 {
					req(reqQuit)
				} else {
					t.yanked = t.input
					t.input = []rune{}
					t.cx = 0
				}
			case actForwardChar:
				if t.cx < len(t.input) {
					t.cx++
				}
			case actBackwardDeleteChar:
				if t.cx > 0 {
					t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
					t.cx--
				}
			case actSelectAll:
				if t.multi {
					for i := 0; i < t.merger.Length(); i++ {
						t.selectItem(t.merger.Get(i).item)
					}
					req(reqList, reqInfo)
				}
			case actDeselectAll:
				if t.multi {
					t.selected = make(map[int32]selectedItem)
					t.version++
					req(reqList, reqInfo)
				}
			case actToggle:
				if t.multi && t.merger.Length() > 0 {
					toggle()
					req(reqList)
				}
			case actToggleAll:
				if t.multi {
					for i := 0; i < t.merger.Length(); i++ {
						t.toggleItem(t.merger.Get(i).item)
					}
					req(reqList, reqInfo)
				}
			case actToggleIn:
				if t.reverse {
					return doAction(action{t: actToggleUp}, mapkey)
				}
				return doAction(action{t: actToggleDown}, mapkey)
			case actToggleOut:
				if t.reverse {
					return doAction(action{t: actToggleDown}, mapkey)
				}
				return doAction(action{t: actToggleUp}, mapkey)
			case actToggleDown:
				if t.multi && t.merger.Length() > 0 {
					toggle()
					t.vmove(-1, true)
					req(reqList)
				}
			case actToggleUp:
				if t.multi && t.merger.Length() > 0 {
					toggle()
					t.vmove(1, true)
					req(reqList)
				}
			case actDown:
				t.vmove(-1, true)
				req(reqList)
			case actUp:
				t.vmove(1, true)
				req(reqList)
			case actAccept:
				req(reqClose)
			case actClearScreen:
				req(reqRedraw)
			case actTop:
				t.vset(0)
				req(reqList)
			case actUnixLineDiscard:
				if t.cx > 0 {
					t.yanked = copySlice(t.input[:t.cx])
					t.input = t.input[t.cx:]
					t.cx = 0
				}
			case actUnixWordRubout:
				if t.cx > 0 {
					t.rubout("\\s\\S")
				}
			case actBackwardKillWord:
				if t.cx > 0 {
					t.rubout(t.wordRubout)
				}
			case actYank:
				suffix := copySlice(t.input[t.cx:])
				t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
				t.cx += len(t.yanked)
			case actPageUp:
				t.vmove(t.maxItems()-1, false)
				req(reqList)
			case actPageDown:
				t.vmove(-(t.maxItems() - 1), false)
				req(reqList)
			case actHalfPageUp:
				t.vmove(t.maxItems()/2, false)
				req(reqList)
			case actHalfPageDown:
				t.vmove(-(t.maxItems() / 2), false)
				req(reqList)
			case actJump:
				t.jumping = jumpEnabled
				req(reqJump)
			case actJumpAccept:
				t.jumping = jumpAcceptEnabled
				req(reqJump)
			case actBackwardWord:
				t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1
			case actForwardWord:
				t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
			case actKillWord:
				ncx := t.cx +
					findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
				if ncx > t.cx {
					t.yanked = copySlice(t.input[t.cx:ncx])
					t.input = append(t.input[:t.cx], t.input[ncx:]...)
				}
			case actKillLine:
				if t.cx < len(t.input) {
					t.yanked = copySlice(t.input[t.cx:])
					t.input = t.input[:t.cx]
				}
			case actRune:
				prefix := copySlice(t.input[:t.cx])
				t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
				t.cx++
			case actPreviousHistory:
				if t.history != nil {
					t.history.override(string(t.input))
					t.input = trimQuery(t.history.previous())
					t.cx = len(t.input)
				}
			case actNextHistory:
				if t.history != nil {
					t.history.override(string(t.input))
					t.input = trimQuery(t.history.next())
					t.cx = len(t.input)
				}
			case actSigStop:
				p, err := os.FindProcess(os.Getpid())
				if err == nil {
					t.tui.Clear()
					t.tui.Pause(t.fullscreen)
					notifyStop(p)
					t.mutex.Unlock()
					return false
				}
			case actMouse:
				me := event.MouseEvent
				mx, my := me.X, me.Y
				if me.S != 0 {
					// Scroll
					if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
						if t.multi && me.Mod {
							toggle()
						}
						t.vmove(me.S, true)
						req(reqList)
					} else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
						scrollPreview(-me.S)
					}
				} else if t.window.Enclose(my, mx) {
					mx -= t.window.Left()
					my -= t.window.Top()
					mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
					if !t.reverse {
						my = t.window.Height() - my - 1
					}
					min := 2 + len(t.header)
					if t.inlineInfo {
						min--
					}
					if me.Double {
						// Double-click
						if my >= min {
							if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
								return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick)
							}
						}
					} else if me.Down {
						if my == 0 && mx >= 0 {
							// Prompt
							t.cx = mx
						} else if my >= min {
							// List
							if t.vset(t.offset+my-min) && t.multi && me.Mod {
								toggle()
							}
							req(reqList)
						}
					}
				}
			}
			return true
		}
		changed := false
		mapkey := event.Type
		if t.jumping == jumpDisabled {
			actions := t.keymap[mapkey]
			if mapkey == tui.Rune {
				mapkey = int(event.Char) + int(tui.AltZ)
				if act, prs := t.keymap[mapkey]; prs {
					actions = act
				}
			}
			if !doActions(actions, mapkey) {
				continue
			}
			t.truncateQuery()
			changed = string(previousInput) != string(t.input)
			if onChanges, prs := t.keymap[tui.Change]; changed && prs {
				if !doActions(onChanges, tui.Change) {
					continue
				}
			}
		} else {
			if mapkey == tui.Rune {
				if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
					t.cy = idx + t.offset
					if t.jumping == jumpAcceptEnabled {
						req(reqClose)
					}
				}
			}
			t.jumping = jumpDisabled
			req(reqList)
		}
		t.mutex.Unlock() // Must be unlocked before touching reqBox

		if changed {
			t.eventBox.Set(EvtSearchNew, t.sort)
		}
		for _, event := range events {
			t.reqBox.Set(event, nil)
		}
	}
}

func (t *Terminal) constrain() {
	count := t.merger.Length()
	height := t.maxItems()
	diffpos := t.cy - t.offset

	t.cy = util.Constrain(t.cy, 0, count-1)
	t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy)
	// Adjustment
	if count-t.offset < height {
		t.offset = util.Max(0, count-height)
		t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
	}
	t.offset = util.Max(0, t.offset)
}

func (t *Terminal) vmove(o int, allowCycle bool) {
	if t.reverse {
		o *= -1
	}
	dest := t.cy + o
	if t.cycle && allowCycle {
		max := t.merger.Length() - 1
		if dest > max {
			if t.cy == max {
				dest = 0
			}
		} else if dest < 0 {
			if t.cy == 0 {
				dest = max
			}
		}
	}
	t.vset(dest)
}

func (t *Terminal) vset(o int) bool {
	t.cy = util.Constrain(o, 0, t.merger.Length()-1)
	return t.cy == o
}

func (t *Terminal) maxItems() int {
	max := t.window.Height() - 2 - len(t.header)
	if t.inlineInfo {
		max++
	}
	return util.Max(max, 0)
}