@ 261102.1
current
package fzf

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"maps"
	"math"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"regexp"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"
	"unicode"

	"github.com/rivo/uniseg"

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

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

/*
Placeholder regex is used to extract placeholders from fzf's template
strings. Acts as input validation for parsePlaceholder function.
Describes the syntax, but it is fairly lenient.

The following pseudo regex has been reverse engineered from the
implementation. It is overly strict, but better describes what's possible.
As such it is not useful for validation, but rather to generate test
cases for example.

	\\?(?:                                      # escaped type
	    {\+?s?f?r?RANGE(?:,RANGE)*}             # token type
	    {q[:s?RANGE]}                           # query type
	    |{\+?n?f?}                              # item type (notice no mandatory element inside brackets)
	)
	RANGE = (?:
	    (?:-?[0-9]+)?\.\.(?:-?[0-9]+)?          # ellipsis syntax for token range (x..y)
	    |-?[0-9]+                               # shorthand syntax (x..x)
	)
*/
var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var passThroughBeginRegex *regexp.Regexp
var passThroughEndTmuxRegex *regexp.Regexp
var sixelBeginRegex *regexp.Regexp
var cursorBackRegex *regexp.Regexp
var ttyin *os.File

var inTmux = len(os.Getenv("TMUX")) > 0

const clearCode string = "\x1b[2J"

// Number of maximum focus events to process synchronously
const maxFocusEvents = 10000

// execute-silent and transform* actions will block user input for this duration.
// After this duration, users can press CTRL-C to terminate the command.
const blockDuration = 1 * time.Second

// Skip exporting FZF_CURRENT_ITEM when the item is larger than this, so a huge
// item cannot overflow ARG_MAX and break exec for preview and other commands.
const maxCurrentItemEnvSize = 64 * 1024

func init() {
	placeholder = regexp.MustCompile(`\\?(?:{[+*sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{[+*]?f?nf?})`)
	whiteSuffix = regexp.MustCompile(`\s*$`)
	offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
	offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)

	// Parts of the preview output that should be passed through to the terminal
	// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
	// * https://sw.kovidgoyal.net/kitty/graphics-protocol
	// * https://en.wikipedia.org/wiki/Sixel
	// * https://iterm2.com/documentation-images.html
	/*
		passThroughRegex = regexp.MustCompile(`
			  \x1bPtmux;\x1b\x1b  .*?  [^\x1b]\x1b\\
			| \x1b(_G|P[0-9;]*q)  .*?  \x1b\\\r?
			| \x1b]1337;          .*?  (\a|\x1b\\)
		`)
	*/
	passThroughBeginRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b|\x1b(_G|P[0-9;]*q)|\x1b]1337;`)
	passThroughEndTmuxRegex = regexp.MustCompile(`[^\x1b]\x1b\\`)
	sixelBeginRegex = regexp.MustCompile(`^\x1bP[0-9;]*q`)

	// CUB right before an IND, used to return to the column a row started on
	cursorBackRegex = regexp.MustCompile(`\x1b\[([0-9]*)D$`)
}

type jumpMode int

const (
	jumpDisabled jumpMode = iota
	jumpEnabled
	jumpAcceptEnabled
)

type resumableState int

const (
	disabledState resumableState = iota
	pausedState
	enabledState
)

func (s resumableState) Enabled() bool {
	return s == enabledState
}

func (s *resumableState) Force(flag bool) {
	if flag {
		*s = enabledState
	} else {
		*s = disabledState
	}
}

func (s *resumableState) Set(flag bool) {
	if *s == disabledState {
		return
	}

	if flag {
		*s = enabledState
	} else {
		*s = pausedState
	}
}

type commandSpec struct {
	command   string
	tempFiles []string
}

type quitSignal struct {
	code int
	err  error
}

type waitState struct {
	blocked   bool
	blockedAt time.Time
	pending   []*action
	searching bool // a search is in progress or the input is still loading
}

type previewer struct {
	version    int64
	lines      []string
	offset     int
	scrollable bool
	final      bool
	following  resumableState
	spinner    string
	bar        []bool
	xw         [2]int
}

type previewed struct {
	version   int64
	numLines  int
	offset    int
	filled    bool
	image     bool
	wipe      bool
	wireframe bool
}

type eachLine struct {
	line string
	err  error
}

type itemLine struct {
	valid     bool
	firstLine int
	numLines  int
	cy        int
	current   bool
	selected  bool
	label     string
	queryLen  int
	width     int
	hasBar    bool
	result    Result
	empty     bool
	other     bool
	hidden    bool
}

func (t *Terminal) inListWindow() bool {
	return t.window != t.inputWindow && t.window != t.headerWindow && t.window != t.headerLinesWindow && t.window != t.footerWindow
}

func (t *Terminal) markEmptyLine(line int) {
	if t.inListWindow() {
		t.prevLines[line] = itemLine{valid: true, firstLine: line, empty: true}
	}
}

func (t *Terminal) markOtherLine(line int) {
	if t.inListWindow() {
		t.prevLines[line] = itemLine{valid: true, firstLine: line, other: true}
	}
}

type fitpad struct {
	fit int
	pad int
}

type labelPrinter func(tui.Window, int)

type markerClass int

const (
	markerSingle markerClass = iota
	markerTop
	markerMiddle
	markerBottom
)

type StatusItem struct {
	Index     int    `json:"index"`
	Text      string `json:"text"`
	Positions []int  `json:"positions,omitempty"`
}

type Status struct {
	Reading    bool         `json:"reading"`
	Progress   int          `json:"progress"`
	Query      string       `json:"query"`
	Position   int          `json:"position"`
	Sort       bool         `json:"sort"`
	TotalCount int          `json:"totalCount"`
	MatchCount int          `json:"matchCount"`
	Current    *StatusItem  `json:"current"`
	Matches    []StatusItem `json:"matches"`
	Selected   []StatusItem `json:"selected"`
}

type versionedCallback struct {
	version  int64
	callback func()
}

type runningCmd struct {
	cmd       *exec.Cmd
	tempFiles []string
}

// Terminal represents terminal input/output
type Terminal struct {
	initDelay            time.Duration
	infoCommand          string
	infoStyle            infoStyle
	infoPrefix           string
	wrap                 bool
	wrapWord             bool
	wrapSign             string
	wrapSignWidth        int
	previewWrapSign      string
	previewWrapSignWidth int
	ghost                string
	separator            labelPrinter
	separatorLen         int
	separatorAuto        bool
	spinner              []string
	promptString         string
	prompt               func()
	promptLen            int
	borderLabel          labelPrinter
	borderLabelLen       int
	borderLabelOpts      labelOpts
	previewLabel         labelPrinter
	previewLabelLen      int
	previewLabelOpts     labelOpts
	inputLabel           labelPrinter
	inputLabelLen        int
	inputLabelOpts       labelOpts
	headerLabel          labelPrinter
	headerLabelLen       int
	headerLabelOpts      labelOpts
	footerLabel          labelPrinter
	footerLabelLen       int
	footerLabelOpts      labelOpts
	gutterReverse        bool
	gutterRawReverse     bool
	pointer              string
	pointerLen           int
	pointerEmpty         string
	pointerEmptyRaw      string
	marker               string
	markerLen            int
	markerEmpty          string
	markerMultiLine      [3]string
	queryLen             [2]int
	layout               layoutType
	fullscreen           bool
	keepRight            bool
	hscroll              bool
	hscrollOff           int
	scrollOff            int
	gap                  int
	gapLine              labelPrinter
	gapLineLen           int
	wordRubout           string
	wordNext             string
	subWordRubout        string
	subWordNext          string
	cx                   int
	cy                   int
	offset               int
	xoffset              int
	yanked               []rune
	input                []rune
	inputOverride        *[]rune
	pasting              *[]rune
	multi                int
	multiLine            bool
	sort                 bool
	toggleSort           bool
	track                trackOption
	idNth                []Range
	trackKey             string
	trackBlocked         bool
	trackSync            bool
	trackKeyCache        map[int32]bool
	wait                 waitState
	pendingSelections    map[string]selectedItem
	targetIndex          int32
	delimiter            Delimiter
	expect               map[tui.Event]string
	keymap               map[tui.Event][]*action
	keymapOrg            map[tui.Event][]*action
	pressed              string
	printQueue           []string
	printQuery           bool
	history              *History
	cycle                bool
	highlightLine        bool
	headerVisible        bool
	headerFirst          bool
	headerLines          int
	header               []Item
	header0              []string
	footer               []string
	ellipsis             string
	scrollbar            string
	previewScrollbar     string
	ansi                 bool
	freezeLeft           int
	freezeRight          int
	nthAttr              tui.Attr
	nth                  []Range
	nthCurrent           []Range
	withNthDefault       string
	withNthExpr          string
	withNthEnabled       bool
	acceptNth            func([]Token, int32) string
	tabstop              int
	margin               [4]sizeSpec
	padding              [4]sizeSpec
	unicode              bool
	listenAddr           *listenAddress
	listenPort           *int
	listener             net.Listener
	listenUnsafe         bool
	borderShape          tui.BorderShape
	listBorderShape      tui.BorderShape
	inputBorderShape     tui.BorderShape
	headerBorderShape    tui.BorderShape
	headerLinesShape     tui.BorderShape
	footerBorderShape    tui.BorderShape
	listLabel            labelPrinter
	listLabelLen         int
	listLabelOpts        labelOpts
	cleanExit            bool
	executor             *util.Executor
	paused               bool
	inputless            bool
	border               tui.Window
	window               tui.Window
	inputWindow          tui.Window
	inputBorder          tui.Window
	headerWindow         tui.Window
	headerBorder         tui.Window
	headerLinesWindow    tui.Window
	headerLinesBorder    tui.Window
	footerWindow         tui.Window
	footerBorder         tui.Window
	wborder              tui.Window
	pborder              tui.Window
	pwindow              tui.Window
	borderWidth          int
	count                int
	progress             int
	hasStartActions      bool
	hasResultActions     bool
	hasFocusActions      bool
	hasLoadActions       bool
	hasResizeActions     bool
	triggerLoad          bool
	pendingReqList       bool
	filterSelection      bool
	reading              bool
	running              *util.AtomicBool
	failed               *string
	jumping              jumpMode
	jumpLabels           string
	printer              func(string)
	printsep             string
	merger               *Merger
	passMerger           *Merger
	resultMerger         *Merger
	matchMap             map[int32]Result
	selected             map[int32]selectedItem
	version              int64
	revision             revision
	bgVersion            int64
	runningCmds          *util.ConcurrentSet[*runningCmd]
	reqBox               *util.EventBox
	initialPreviewOpts   previewOpts
	previewOpts          previewOpts
	activePreviewOpts    *previewOpts
	previewer            previewer
	previewed            previewed
	previewBox           *util.EventBox
	eventBox             *util.EventBox
	mutex                sync.Mutex
	uiMutex              sync.Mutex
	initFunc             func() error
	prevLines            []itemLine
	suppress             bool
	startChan            chan fitpad
	killChan             chan bool
	killedChan           chan bool
	serverInputChan      chan []*action
	callbackChan         chan versionedCallback
	bgQueue              map[action][]func(bool)
	bgSemaphore          chan struct{}
	bgSemaphores         map[action]chan struct{}
	keyChan              chan tui.Event
	eventChan            chan tui.Event
	timerChan            chan tui.Event
	slab                 *util.Slab
	theme                *tui.ColorTheme
	tui                  tui.Renderer
	ttyDefault           string
	ttyin                *os.File
	executing            *util.AtomicBool
	termSize             tui.TermSize
	lastAction           actionType
	lastKey              string
	lastFocus            focusedItem
	areaLines            int
	areaColumns          int
	forcePreview         bool
	clickHeaderLine      int
	clickHeaderColumn    int
	clickFooterLine      int
	clickFooterColumn    int
	proxyScript          string
	setNativeLabel       func(string)
	numLinesCache        map[int32]numLinesCacheValue
	raw                  bool
	lastActivity         time.Time
}

type numLinesCacheValue struct {
	atMost   int
	numLines int
}

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

type focusedItem struct {
	major int // revision.major; changes on reload
	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)
}

// EventTypes are listed in the order of their priority.
const (
	reqResize util.EventType = iota
	reqReinit
	reqFullRedraw
	reqRedraw

	reqJump
	reqPrompt
	reqInfo
	reqHeader
	reqFooter
	reqList
	reqRedrawInputLabel
	reqRedrawHeaderLabel
	reqRedrawFooterLabel
	reqRedrawListLabel
	reqRedrawBorderLabel
	reqRedrawPreviewLabel

	reqPreviewReady
	reqPreviewEnqueue
	reqPreviewDisplay
	reqPreviewRefresh
	reqPreviewDelayed

	reqActivate
	reqClose
	reqPrintQuery
	reqBecome
	reqQuit
	reqFatal
)

func isTerminalEvent(et util.EventType) bool {
	switch et {
	case reqClose, reqPrintQuery, reqBecome, reqQuit, reqFatal:
		return true
	}
	return false
}

type action struct {
	t actionType
	a string
}

//go:generate stringer -type=actionType
type actionType int

const (
	actIgnore actionType = iota
	actStart
	actClick
	actInvalid
	actBracketedPasteBegin
	actBracketedPasteEnd
	actChar
	actMouse
	actBeginningOfLine
	actAbort
	actAccept
	actAcceptNonEmpty
	actAcceptOrPrintQuery
	actBackwardChar
	actBackwardDeleteChar
	actBackwardDeleteCharEof
	actBackwardWord
	actBackwardSubWord
	actCancel

	actChangeBorderLabel
	actChangeGhost
	actChangeHeader
	actChangeHeaderLines
	actChangeFooter
	actChangeHeaderLabel
	actChangeFooterLabel
	actChangeInfoCommand
	actChangeInputLabel
	actChangeListLabel
	actChangeMulti
	actChangeNth
	actChangeWithNth
	actChangePointer
	actChangePreview
	actChangePreviewLabel
	actChangePreviewWindow
	actChangePrompt
	actChangeQuery

	actClearScreen
	actClearQuery
	actClearSelection
	actClose
	actDeleteChar
	actDeleteCharEof
	actEndOfLine
	actFatal
	actForwardChar
	actForwardWord
	actForwardSubWord
	actKillLine
	actKillWord
	actKillSubWord
	actUnixLineDiscard
	actUnixWordRubout
	actYank
	actBackwardKillWord
	actBackwardKillSubWord
	actSelectAll
	actDeselectAll
	actToggle
	actToggleSearch
	actToggleAll
	actToggleDown
	actToggleUp
	actToggleIn
	actToggleOut
	actToggleTrack
	actToggleTrackCurrent
	actToggleHeader
	actToggleWrap
	actToggleWrapWord
	actToggleMultiLine
	actToggleHscroll
	actToggleRaw
	actEnableRaw
	actDisableRaw
	actTrackCurrent
	actToggleInput
	actHideInput
	actShowInput
	actUntrackCurrent
	actDown
	actDownMatch
	actUp
	actUpMatch
	actPageUp
	actPageDown
	actPosition
	actHalfPageUp
	actHalfPageDown
	actOffsetUp
	actOffsetDown
	actOffsetMiddle
	actJump
	actJumpAccept // XXX Deprecated in favor of jump:accept binding
	actPrintQuery // XXX Deprecated (not very useful, just use --print-query)
	actRefreshPreview
	actReplaceQuery
	actToggleSort
	actShowPreview
	actHidePreview
	actTogglePreview
	actTogglePreviewWrap
	actTogglePreviewWrapWord

	actTransform
	actTransformBorderLabel
	actTransformGhost
	actTransformHeader
	actTransformHeaderLines
	actTransformFooter
	actTransformHeaderLabel
	actTransformFooterLabel
	actTransformInfoCommand
	actTransformInputLabel
	actTransformListLabel
	actTransformNth
	actTransformWithNth
	actTransformPointer
	actTransformPreviewLabel
	actTransformPrompt
	actTransformQuery
	actTransformSearch

	actTrigger

	actBgTransform
	actBgTransformBorderLabel
	actBgTransformGhost
	actBgTransformHeader
	actBgTransformHeaderLines
	actBgTransformFooter
	actBgTransformHeaderLabel
	actBgTransformFooterLabel
	actBgTransformInfoCommand
	actBgTransformInputLabel
	actBgTransformListLabel
	actBgTransformNth
	actBgTransformWithNth
	actBgTransformPointer
	actBgTransformPreviewLabel
	actBgTransformPrompt
	actBgTransformQuery
	actBgTransformSearch

	actBgCancel

	actSearch
	actPreview
	actPreviewTop
	actPreviewBottom
	actPreviewUp
	actPreviewDown
	actPreviewPageUp
	actPreviewPageDown
	actPreviewHalfPageUp
	actPreviewHalfPageDown
	actPrevHistory
	actPrevSelected
	actPrint
	actPut
	actNextHistory
	actNextSelected
	actExecute
	actExecuteSilent
	actExecuteMulti // Deprecated
	actSigStop
	actBest
	actFirst
	actLast
	actReload
	actReloadSync
	actDisableSearch
	actEnableSearch
	actSelect
	actDeselect
	actUnbind
	actRebind
	actToggleBind
	actBecome
	actShowHeader
	actHideHeader
	actBell
	actExclude
	actExcludeMulti
	actAsync
	actWait
)

func (a actionType) Name() string {
	return util.ToKebabCase(a.String()[3:])
}

func processExecution(action actionType) bool {
	switch action {
	case actTransform,
		actTransformBorderLabel,
		actTransformGhost,
		actTransformHeader,
		actTransformHeaderLines,
		actTransformFooter,
		actTransformHeaderLabel,
		actTransformFooterLabel,
		actTransformInfoCommand,
		actTransformInputLabel,
		actTransformListLabel,
		actTransformNth,
		actTransformWithNth,
		actTransformPointer,
		actTransformPreviewLabel,
		actTransformPrompt,
		actTransformQuery,
		actTransformSearch,
		actBgTransform,
		actBgTransformBorderLabel,
		actBgTransformGhost,
		actBgTransformHeader,
		actBgTransformHeaderLines,
		actBgTransformFooter,
		actBgTransformHeaderLabel,
		actBgTransformFooterLabel,
		actBgTransformInfoCommand,
		actBgTransformInputLabel,
		actBgTransformListLabel,
		actBgTransformNth,
		actBgTransformWithNth,
		actBgTransformPointer,
		actBgTransformPreviewLabel,
		actBgTransformPrompt,
		actBgTransformQuery,
		actBgTransformSearch,
		actPreview,
		actChangePreview,
		actChangeInfoCommand,
		actRefreshPreview,
		actExecute,
		actExecuteSilent,
		actExecuteMulti,
		actReload,
		actReloadSync,
		actBecome:
		return true
	}
	return false
}

type placeholderFlags struct {
	plus          bool
	asterisk      bool
	preserveSpace bool
	number        bool
	forceUpdate   bool
	file          bool
	raw           bool
}

type withNthSpec struct {
	fn func([]Token, int32) string // nil = clear (restore original)
}

type searchRequest struct {
	sort        bool
	sync        bool
	nth         *[]Range
	withNth     *withNthSpec
	headerLines *int
	command     *commandSpec
	environ     []string
	changed     bool
	denylist    []int32
	revision    revision
}

type previewRequest struct {
	template     string
	scrollOffset int
	list         [3][]*Item // current, select, and all matched items
	env          []string
	query        string
}

type previewResult struct {
	version int64
	lines   []string
	offset  int
	spinner string
}

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[tui.Event][]*action {
	keymap := make(map[tui.Event][]*action)
	add := func(e tui.EventType, a actionType) {
		keymap[e.AsEvent()] = toActions(a)
	}
	addEvent := func(e tui.Event, a actionType) {
		keymap[e] = toActions(a)
	}

	add(tui.Fatal, actFatal)
	add(tui.Invalid, actInvalid)
	add(tui.BracketedPasteBegin, actBracketedPasteBegin)
	add(tui.BracketedPasteEnd, actBracketedPasteEnd)
	add(tui.CtrlA, actBeginningOfLine)
	add(tui.CtrlB, actBackwardChar)
	add(tui.CtrlC, actAbort)
	add(tui.CtrlG, actAbort)
	add(tui.CtrlQ, actAbort)
	add(tui.Esc, actAbort)
	add(tui.CtrlD, actDeleteCharEof)
	add(tui.CtrlE, actEndOfLine)
	add(tui.CtrlF, actForwardChar)
	add(tui.Backspace, actBackwardDeleteChar)
	add(tui.CtrlBackspace, actBackwardDeleteChar)
	add(tui.Tab, actToggleDown)
	add(tui.ShiftTab, actToggleUp)
	add(tui.CtrlJ, actDown)
	add(tui.CtrlK, actUp)
	add(tui.CtrlL, actClearScreen)
	add(tui.Enter, actAccept)
	add(tui.CtrlN, actDownMatch)
	add(tui.CtrlP, actUpMatch)
	add(tui.AltDown, actDownMatch)
	add(tui.AltUp, actUpMatch)
	add(tui.CtrlU, actUnixLineDiscard)
	add(tui.CtrlW, actUnixWordRubout)
	add(tui.CtrlY, actYank)
	if !util.IsWindows() {
		add(tui.CtrlZ, actSigStop)
	}
	add(tui.CtrlSlash, actToggleWrapWord)
	addEvent(tui.AltKey('/'), actToggleWrapWord)

	addEvent(tui.AltKey('b'), actBackwardWord)
	add(tui.ShiftLeft, actBackwardWord)
	add(tui.AltLeft, actBackwardWord)
	addEvent(tui.AltKey('f'), actForwardWord)
	add(tui.ShiftRight, actForwardWord)
	add(tui.AltRight, actForwardWord)
	addEvent(tui.AltKey('d'), actKillWord)
	add(tui.AltBackspace, actBackwardKillWord)

	add(tui.Up, actUp)
	add(tui.Down, actDown)
	add(tui.Left, actBackwardChar)
	add(tui.Right, actForwardChar)

	add(tui.Home, actBeginningOfLine)
	add(tui.End, actEndOfLine)
	add(tui.Delete, actDeleteChar)
	add(tui.PageUp, actPageUp)
	add(tui.PageDown, actPageDown)

	add(tui.ShiftUp, actPreviewUp)
	add(tui.ShiftDown, actPreviewDown)

	add(tui.Mouse, actMouse)
	add(tui.LeftClick, actClick)
	add(tui.RightClick, actToggle)
	add(tui.SLeftClick, actToggle)
	add(tui.SRightClick, actToggle)

	add(tui.ScrollUp, actUp)
	add(tui.ScrollDown, actDown)
	keymap[tui.SScrollUp.AsEvent()] = toActions(actToggle, actUp)
	keymap[tui.SScrollDown.AsEvent()] = toActions(actToggle, actDown)

	add(tui.PreviewScrollUp, actPreviewUp)
	add(tui.PreviewScrollDown, actPreviewDown)
	return keymap
}

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

func mayTriggerPreview(opts *Options) bool {
	if opts.ListenAddr != nil {
		return true
	}
	for _, actions := range opts.Keymap {
		for _, action := range actions {
			switch action.t {
			case actPreview, actChangePreview, actTransform, actBgTransform:
				return true
			}
		}
	}
	return false
}

func makeSpinner(unicode bool) []string {
	if unicode {
		return []string{`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`}
	}
	return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
}

func evaluateHeight(opts *Options, termHeight int) int {
	size := opts.Height.size
	if opts.Height.percent {
		if opts.Height.inverse {
			size = 100 - size
		}
		return max(int(size*float64(termHeight)/100.0), opts.MinHeight)
	}
	if opts.Height.inverse {
		size = float64(termHeight) - size
	}
	return int(size)
}

// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
	input := trimQuery(opts.Query)
	var delay time.Duration
	if opts.Sync {
		delay = 0
	} else if opts.Tac {
		delay = initialDelayTac
	} else {
		delay = initialDelay
	}
	var previewBox *util.EventBox
	// We need to start the previewer even when --preview option is not specified
	// * if HTTP server is enabled
	// * if 'preview' or 'change-preview' action is bound to a key
	// * if 'transform' action is bound to a key
	if len(opts.Preview.command) > 0 || mayTriggerPreview(opts) {
		previewBox = util.NewEventBox()
	}
	var renderer tui.Renderer
	fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
	var err error
	// Reuse ttyin if available to avoid having multiple file descriptors open
	// when you run fzf multiple times in your Go program. Closing it is known to
	// cause problems with 'become' action and invalid terminal state after exit.
	if ttyin == nil {
		if ttyin, err = tui.TtyIn(opts.TtyDefault); err != nil {
			return nil, err
		}
	}
	if fullscreen {
		if tui.HasFullscreenRenderer() {
			renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop)
		} else {
			renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
				true, func(h int) int { return h })
		}
	} else {
		maxHeightFunc := func(termHeight int) int {
			// Minimum height required to render fzf excluding margin and padding
			effectiveMinHeight := minHeight
			if previewBox != nil && opts.Preview.aboveOrBelow() {
				effectiveMinHeight += 1 + borderLines(opts.Preview.Border(opts.Layout))
			}
			if opts.noSeparatorLine() {
				effectiveMinHeight--
			}
			effectiveMinHeight += borderLines(opts.BorderShape)
			return min(termHeight, max(evaluateHeight(opts, termHeight), effectiveMinHeight))
		}
		renderer, err = tui.NewLightRenderer(opts.TtyDefault, ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
	}
	if err != nil {
		return nil, err
	}
	if opts.Inputless {
		renderer.HideCursor()
	}
	wordRubout := "[^\\pL\\pN][\\pL\\pN]"
	wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
	subWordRubout := "[a-z][A-Z]|[^\\pL\\pN][\\pL\\pN]"
	subWordNext := "[a-z][A-Z]|[\\pL\\pN][^\\pL\\pN]|(.$)"
	if opts.FileWord {
		sep := regexp.QuoteMeta(string(os.PathSeparator))
		wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
		wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
	}
	keymapCopy := maps.Clone(opts.Keymap)

	em := EmptyMerger(revision{})
	t := Terminal{
		initDelay:          delay,
		infoCommand:        opts.InfoCommand,
		infoStyle:          opts.InfoStyle,
		infoPrefix:         opts.InfoPrefix,
		ghost:              opts.Ghost,
		separator:          nil,
		spinner:            makeSpinner(opts.Unicode),
		promptString:       opts.Prompt,
		queryLen:           [2]int{0, 0},
		layout:             opts.Layout,
		fullscreen:         fullscreen,
		keepRight:          opts.KeepRight,
		hscroll:            opts.Hscroll,
		hscrollOff:         opts.HscrollOff,
		scrollOff:          opts.ScrollOff,
		pointer:            *opts.Pointer,
		pointerLen:         uniseg.StringWidth(*opts.Pointer),
		marker:             *opts.Marker,
		markerLen:          uniseg.StringWidth(*opts.Marker),
		markerMultiLine:    *opts.MarkerMulti,
		wordRubout:         wordRubout,
		wordNext:           wordNext,
		subWordRubout:      subWordRubout,
		subWordNext:        subWordNext,
		cx:                 len(input),
		cy:                 0,
		offset:             0,
		xoffset:            0,
		yanked:             []rune{},
		input:              input,
		multi:              opts.Multi,
		multiLine:          opts.ReadZero && opts.MultiLine,
		wrap:               opts.Wrap,
		wrapWord:           opts.WrapWord,
		sort:               opts.Sort > 0,
		toggleSort:         opts.ToggleSort,
		track:              opts.Track,
		idNth:              opts.IdNth,
		targetIndex:        minItem.Index(),
		delimiter:          opts.Delimiter,
		expect:             opts.Expect,
		keymap:             opts.Keymap,
		keymapOrg:          keymapCopy,
		pressed:            "",
		printQuery:         opts.PrintQuery,
		history:            opts.History,
		margin:             opts.Margin,
		padding:            opts.Padding,
		unicode:            opts.Unicode,
		listenAddr:         opts.ListenAddr,
		listenUnsafe:       opts.Unsafe,
		borderShape:        opts.BorderShape,
		listBorderShape:    opts.ListBorderShape,
		inputBorderShape:   opts.InputBorderShape,
		headerBorderShape:  opts.HeaderBorderShape,
		headerLinesShape:   opts.HeaderLinesShape,
		footerBorderShape:  opts.FooterBorderShape,
		borderWidth:        1,
		listLabel:          nil,
		listLabelOpts:      opts.ListLabel,
		borderLabel:        nil,
		borderLabelOpts:    opts.BorderLabel,
		previewLabel:       nil,
		previewLabelOpts:   opts.PreviewLabel,
		inputLabel:         nil,
		inputLabelOpts:     opts.InputLabel,
		headerLabel:        nil,
		headerLabelOpts:    opts.HeaderLabel,
		footerLabel:        nil,
		footerLabelOpts:    opts.FooterLabel,
		cleanExit:          opts.ClearOnExit,
		executor:           executor,
		paused:             opts.Phony,
		inputless:          opts.Inputless,
		cycle:              opts.Cycle,
		highlightLine:      opts.CursorLine,
		headerVisible:      true,
		headerFirst:        opts.HeaderFirst,
		headerLines:        opts.HeaderLines,
		gap:                opts.Gap,
		header:             []Item{},
		footer:             opts.Footer,
		header0:            opts.Header,
		ansi:               opts.Ansi,
		freezeLeft:         opts.FreezeLeft,
		freezeRight:        opts.FreezeRight,
		nthAttr:            opts.Theme.Nth.Attr,
		nth:                opts.Nth,
		nthCurrent:         opts.Nth,
		withNthDefault:     opts.WithNthExpr,
		withNthExpr:        opts.WithNthExpr,
		withNthEnabled:     opts.WithNth != nil,
		tabstop:            opts.Tabstop,
		raw:                opts.Raw,
		hasStartActions:    false,
		hasResultActions:   false,
		hasFocusActions:    false,
		hasLoadActions:     false,
		triggerLoad:        false,
		reading:            true,
		running:            util.NewAtomicBool(true),
		failed:             nil,
		jumping:            jumpDisabled,
		jumpLabels:         opts.JumpLabels,
		printer:            opts.Printer,
		printsep:           opts.PrintSep,
		proxyScript:        opts.ProxyScript,
		setNativeLabel:     nativeLabelSetter(),
		merger:             em,
		passMerger:         em,
		resultMerger:       em,
		matchMap:           make(map[int32]Result),
		selected:           make(map[int32]selectedItem),
		runningCmds:        util.NewConcurrentSet[*runningCmd](),
		reqBox:             util.NewEventBox(),
		initialPreviewOpts: opts.Preview,
		previewOpts:        opts.Preview,
		activePreviewOpts:  &opts.Preview,
		previewer:          previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}, [2]int{0, 0}},
		previewed:          previewed{0, 0, 0, false, false, false, false},
		previewBox:         previewBox,
		eventBox:           eventBox,
		mutex:              sync.Mutex{},
		uiMutex:            sync.Mutex{},
		suppress:           true,
		slab:               util.MakeSlab(slab16Size, slab32Size),
		theme:              opts.Theme,
		startChan:          make(chan fitpad, 1),
		killChan:           make(chan bool),
		killedChan:         make(chan bool),
		serverInputChan:    make(chan []*action, 100),
		callbackChan:       make(chan versionedCallback, maxBgProcesses),
		bgQueue:            make(map[action][]func(bool)),
		bgSemaphore:        make(chan struct{}, maxBgProcesses),
		bgSemaphores:       make(map[action]chan struct{}),
		keyChan:            make(chan tui.Event),
		eventChan:          make(chan tui.Event, 7), // start | (load + result + result-final + zero|one) | (focus) | (resize)
		timerChan:          make(chan tui.Event),    // unbuffered: every() ticks coalesce when main loop is busy
		tui:                renderer,
		ttyDefault:         opts.TtyDefault,
		ttyin:              ttyin,
		initFunc:           func() error { return renderer.Init() },
		executing:          util.NewAtomicBool(false),
		lastAction:         actStart,
		lastFocus:          focusedItem{},
		lastActivity:       time.Now(),
		numLinesCache:      make(map[int32]numLinesCacheValue),
		// The initial load counts as a search in progress ('start:wait').
		// Set before the reader starts so the first final result clears it.
		wait: waitState{searching: true}}
	if opts.AcceptNth != nil {
		t.acceptNth = opts.AcceptNth(t.delimiter)
	}

	baseTheme := opts.BaseTheme
	if baseTheme == nil {
		baseTheme = renderer.DefaultTheme()
	}
	// If the list border can't host an inline separator, the runtime later coerces
	// BorderInline to BorderLine. Mirror that here so theme color inheritance matches
	// the final rendering rather than the user's requested shape.
	inlineListSupported := opts.ListBorderShape.HasTop() && opts.ListBorderShape.HasBottom()
	headerInline := inlineListSupported && (opts.HeaderBorderShape == tui.BorderInline || opts.HeaderLinesShape == tui.BorderInline)
	footerInline := inlineListSupported && opts.FooterBorderShape == tui.BorderInline
	hasHeader := opts.HeaderBorderShape.Visible() || opts.HeaderLinesShape.Visible()
	// This should be called before accessing tui.Color*
	tui.InitTheme(opts.Theme, baseTheme, opts.Bold, opts.Black, opts.InputBorderShape.Visible(), hasHeader, headerInline, footerInline)

	// Gutter character
	var gutterChar, gutterRawChar string
	if opts.Gutter != nil {
		gutterChar = *opts.Gutter
	} else if t.unicode {
		gutterChar = "▌"
	} else {
		gutterChar = " "
		t.gutterReverse = true
	}

	if opts.GutterRaw != nil {
		gutterRawChar = *opts.GutterRaw
	} else if t.unicode {
		gutterRawChar = "▖"
	} else {
		gutterRawChar = ":"
		t.gutterRawReverse = false
	}

	t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
	// Pre-calculated empty pointer and marker signs
	if t.pointerLen == 0 {
		t.pointerEmpty = ""
		t.pointerEmptyRaw = ""
	} else {
		t.pointerEmpty = gutterChar + strings.Repeat(" ", max(0, t.pointerLen-1))
		t.pointerEmptyRaw = gutterRawChar + strings.Repeat(" ", max(0, t.pointerLen-1))
	}
	t.markerEmpty = strings.Repeat(" ", t.markerLen)

	// Labels
	t.listLabel, t.listLabelLen = t.ansiLabelPrinter(opts.ListLabel.label, &tui.ColListLabel, false)
	t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false)
	t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColPreviewLabel, false)
	t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(opts.InputLabel.label, &tui.ColInputLabel, false)
	t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(opts.HeaderLabel.label, &tui.ColHeaderLabel, false)
	t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(opts.FooterLabel.label, &tui.ColFooterLabel, false)

	// Determine border shape
	if t.borderShape == tui.BorderLine {
		if t.fullscreen {
			t.borderShape = tui.BorderNone
		} else {
			t.borderShape = tui.BorderTop
		}
	}

	// Determine input border shape
	t.inputBorderShape = resolveInputBorderShape(t.layout, t.inputBorderShape)

	// Inline borders are embedded between the list's top and bottom horizontals.
	// Shapes missing either one (none/phantom/line/single-sided) fall back to a plain
	// horizontal separator (same as BorderLine).
	inlineSupported := t.listBorderShape.HasTop() && t.listBorderShape.HasBottom()
	if !inlineSupported {
		if t.headerBorderShape == tui.BorderInline {
			t.headerBorderShape = tui.BorderLine
		}
		if t.headerLinesShape == tui.BorderInline {
			t.headerLinesShape = tui.BorderLine
		}
		if t.footerBorderShape == tui.BorderInline {
			t.footerBorderShape = tui.BorderLine
		}
	}

	// Determine header border shape
	if t.headerBorderShape == tui.BorderLine {
		if t.layout == layoutReverse {
			t.headerBorderShape = tui.BorderBottom
		} else {
			t.headerBorderShape = tui.BorderTop
		}
	}

	// Determine header lines border shape
	if t.headerLinesShape == tui.BorderLine {
		if t.layout == layoutDefault {
			t.headerLinesShape = tui.BorderTop
		} else {
			t.headerLinesShape = tui.BorderBottom
		}
	}

	// Determine footer border shape
	if t.footerBorderShape == tui.BorderLine {
		if t.layout == layoutReverse {
			t.footerBorderShape = tui.BorderTop
		} else {
			t.footerBorderShape = tui.BorderBottom
		}
	}

	// The default separator can be suppressed at runtime depending on the
	// window layout (see separatorLength)
	t.separatorAuto = opts.Separator == nil
	bar := "─"
	if opts.Separator != nil {
		bar = *opts.Separator
	} else if !t.unicode {
		bar = "-"
	}
	if len(bar) > 0 {
		t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
	}

	// Gap line
	if t.gap > 0 && len(*opts.GapLine) > 0 {
		t.gapLine, t.gapLineLen = t.ansiLabelPrinter(*opts.GapLine, &tui.ColGapLine, true)
	}

	if opts.Ellipsis != nil {
		t.ellipsis = *opts.Ellipsis
	} else if t.unicode {
		t.ellipsis = "··"
	} else {
		t.ellipsis = ".."
	}

	if t.unicode {
		t.wrapSign = "↳ "
		t.borderWidth = uniseg.StringWidth("│")
	} else {
		t.wrapSign = "> "
	}
	if opts.WrapSign != nil {
		t.wrapSign = *opts.WrapSign
	}
	t.wrapSign, t.wrapSignWidth = t.processTabsStr(t.wrapSign, 0)
	t.previewWrapSign = t.wrapSign
	t.previewWrapSignWidth = t.wrapSignWidth
	if opts.PreviewWrapSign != nil {
		t.previewWrapSign, t.previewWrapSignWidth = t.processTabsStr(*opts.PreviewWrapSign, 0)
	}
	if opts.Scrollbar == nil {
		if t.unicode && t.borderWidth == 1 {
			t.scrollbar = "│"
		} else {
			t.scrollbar = "|"
		}
		t.previewScrollbar = t.scrollbar
	} else {
		runes := []rune(*opts.Scrollbar)
		if len(runes) > 0 {
			t.scrollbar = string(runes[0])
			t.previewScrollbar = t.scrollbar
			if len(runes) > 1 {
				t.previewScrollbar = string(runes[1])
			}
		}
	}

	var resizeActions []*action
	resizeActions, t.hasResizeActions = t.keymap[tui.Resize.AsEvent()]
	if t.tui.ShouldEmitResizeEvent() {
		t.keymap[tui.Resize.AsEvent()] = append(toActions(actClearScreen), resizeActions...)
	}
	_, t.hasStartActions = t.keymap[tui.Start.AsEvent()]
	_, t.hasResultActions = t.keymap[tui.Result.AsEvent()]
	if _, prs := t.keymap[tui.ResultFinal.AsEvent()]; prs {
		t.hasResultActions = true
	}
	_, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()]
	_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]

	if t.listenAddr != nil {
		listener, port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.dumpStatus)
		if err != nil {
			return nil, err
		}
		t.listener = listener
		if port > 0 {
			t.listenPort = &port
		}
	}

	if t.hasStartActions {
		t.eventChan <- tui.Start.AsEvent()
	}

	return &t, nil
}

func (t *Terminal) deferActivation() bool {
	return t.initDelay == 0 && (t.hasStartActions || t.hasLoadActions || t.hasResultActions || t.hasFocusActions)
}

func (t *Terminal) environ() []string {
	return t.environImpl(false)
}

func (t *Terminal) environForPreview() []string {
	return t.environImpl(true)
}

func (t *Terminal) environImpl(forPreview bool) []string {
	env := os.Environ()
	if t.listenAddr != nil && len(t.listenAddr.sock) > 0 {
		env = append(env, "FZF_SOCK="+t.listenAddr.sock)
	}
	if t.listenPort != nil {
		env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
	}
	env = append(env, "FZF_QUERY="+string(t.input))
	env = append(env, "FZF_ACTION="+t.lastAction.Name())
	env = append(env, "FZF_KEY="+t.lastKey)
	idleMs := time.Since(t.lastActivity).Milliseconds()
	env = append(env, fmt.Sprintf("FZF_IDLE_TIME=%d", idleMs/1000))
	env = append(env, fmt.Sprintf("FZF_IDLE_TIME_MS=%d", idleMs))
	env = append(env, "FZF_PROMPT="+string(t.promptString))
	env = append(env, "FZF_GHOST="+string(t.ghost))
	env = append(env, "FZF_POINTER="+string(t.pointer))
	env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
	env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
	env = append(env, "FZF_LIST_LABEL="+t.listLabelOpts.label)
	env = append(env, "FZF_INPUT_LABEL="+t.inputLabelOpts.label)
	env = append(env, "FZF_HEADER_LABEL="+t.headerLabelOpts.label)
	direction := "down"
	if t.layout == layoutDefault {
		direction = "up"
	}
	env = append(env, "FZF_DIRECTION="+direction)
	if len(t.nthCurrent) > 0 {
		env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
	}
	if len(t.withNthExpr) > 0 {
		env = append(env, "FZF_WITH_NTH="+t.withNthExpr)
	}
	if t.raw {
		val := "0"
		if t.isCurrentItemMatch() {
			val = "1"
		}
		env = append(env, "FZF_RAW="+val)
	}
	inputState := "enabled"
	if t.inputless {
		inputState = "hidden"
	} else if t.paused {
		inputState = "disabled"
	}
	if t.wrap {
		if t.wrapWord {
			env = append(env, "FZF_WRAP=word")
		} else {
			env = append(env, "FZF_WRAP=char")
		}
	}
	env = append(env, "FZF_INPUT_STATE="+inputState)
	env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
	env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length()))
	env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
	env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
	env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
	env = append(env, fmt.Sprintf("FZF_POS=%d", min(t.merger.Length(), t.cy+1)))
	if item := t.currentItem(); item != nil {
		// Skip if the value contains a NUL byte (exec(2) would reject the env)
		// or is too large (a huge item can overflow ARG_MAX and break exec
		// entirely for preview and other child commands).
		if s := item.AsString(t.ansi); !strings.ContainsRune(s, 0) && len(s) <= maxCurrentItemEnvSize {
			env = append(env, "FZF_CURRENT_ITEM="+s)
		}
	}
	env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_LINE=%d", t.clickHeaderLine))
	env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_COLUMN=%d", t.clickHeaderColumn))
	env = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_LINE=%d", t.clickFooterLine))
	env = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_COLUMN=%d", t.clickFooterColumn))
	env = t.addClickHeaderWord(env)
	env = t.addClickFooterWord(env)

	// Add preview environment variables if preview is enabled
	pwindowSize := t.pwindowSize()
	if pwindowSize.Lines > 0 {
		lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
		columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
		if forPreview {
			env = append(env, lines)
			env = append(env, columns)
		}
		env = append(env, "FZF_PREVIEW_"+lines)
		env = append(env, "FZF_PREVIEW_"+columns)
		env = append(env, fmt.Sprintf("FZF_PREVIEW_TOP=%d", t.tui.Top()+t.pwindow.Top()))
		env = append(env, fmt.Sprintf("FZF_PREVIEW_LEFT=%d", t.pwindow.Left()))
	}

	return env
}

func borderLines(shape tui.BorderShape) int {
	lines := 0
	if shape.HasTop() {
		lines++
	}
	if shape.HasBottom() {
		lines++
	}
	return lines
}

func borderColumns(shape tui.BorderShape, borderWidth int) int {
	columns := 0
	if shape.HasLeft() {
		columns += 1 + borderWidth
	}
	if shape.HasRight() {
		columns += 1 + borderWidth
	}
	return columns
}

func (t *Terminal) visibleHeaderLines() int {
	if !t.headerVisible {
		return 0
	}
	return len(t.header0) + t.headerLines
}

func (t *Terminal) visibleHeaderLinesInList() int {
	if t.headerWindow != nil || t.headerLinesWindow != nil {
		return 0
	}
	return t.visibleHeaderLines()
}

func (t *Terminal) visibleInputLinesInList() int {
	if t.inputWindow != nil || t.inputless {
		return 0
	}
	if t.noSeparatorLine() {
		return 1
	}
	return 2
}

// Extra number of lines needed to display fzf
func (t *Terminal) extraLines() int {
	// borderLines() reports zero for BorderInline, but addInline() still
	// reserves a divider line for it
	sectionLines := func(shape tui.BorderShape) int {
		if shape == tui.BorderInline {
			return 1
		}
		return borderLines(shape)
	}
	extra := 0
	if !t.inputless {
		extra++
		if !t.noSeparatorLine() {
			extra++
		}
		if t.inputBorderShape.Visible() {
			extra += borderLines(t.inputBorderShape)
		}
	}
	if t.listBorderShape.Visible() {
		extra += borderLines(t.listBorderShape)
	}
	if t.headerVisible {
		if t.hasHeaderWindow() {
			extra += sectionLines(t.headerBorderShape)
		}
		extra += len(t.header0)
		if w, shape := t.determineHeaderLinesShape(); w {
			extra += sectionLines(shape)
		}
		extra += t.headerLines
	}
	if len(t.footer) > 0 {
		extra += sectionLines(t.footerBorderShape)
		extra += len(t.footer)
	}
	return extra
}

func (t *Terminal) MaxFitAndPad() (int, int) {
	_, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
	padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
	fit := screenHeight - padHeight - t.extraLines()
	return fit, padHeight
}

func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) (labelPrinter, int) {
	// Nothing to do
	if len(str) == 0 {
		return nil, 0
	}

	// Extract ANSI color codes
	str = firstLine(str)
	text, colors, _ := extractColor(str, nil, nil)
	runes := []rune(text)

	// Simpler printer for strings without ANSI colors or tab characters
	if colors == nil && !strings.ContainsRune(text, '\t') {
		length := util.StringWidth(text)
		if length == 0 {
			return nil, 0
		}
		printFn := func(window tui.Window, limit int) {
			ellipsis := []rune{}
			ellipsisWidth := 0
			if !fill {
				ellipsis, ellipsisWidth = util.Truncate(t.ellipsis, limit)
			}
			if length > limit {
				trimmedRunes, _ := t.trimRight(runes, limit-ellipsisWidth)
				window.CPrint(*color, string(trimmedRunes)+string(ellipsis))
			} else if fill {
				window.CPrint(*color, util.RepeatToFill(text, length, limit))
			} else {
				window.CPrint(*color, text)
			}
		}
		return printFn, length
	}

	// Printer that correctly handles ANSI color codes and tab characters
	item := &Item{text: util.RunesToChars(runes), colors: colors}
	length := t.displayWidth(runes)
	if length == 0 {
		return nil, 0
	}
	result := Result{item: item}
	var offsets []colorOffset
	printFn := func(window tui.Window, limit int) {
		if offsets == nil {
			// tui.Col* are not initialized until renderer.Init()
			offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, 0, false)
		}
		for limit > 0 {
			if length > limit {
				trimmedRunes, _ := t.trimRight(runes, limit)
				t.printColoredString(window, trimmedRunes, offsets, *color)
				break
			} else if fill {
				t.printColoredString(window, runes, offsets, *color)
				limit -= length
			} else {
				t.printColoredString(window, runes, offsets, *color)
				break
			}
		}
	}
	return printFn, length
}

// Temporarily switch 'window' so that we can use the existing windows with
// a different window
func (t *Terminal) withWindow(w tui.Window, f func()) {
	prevWindow := t.window
	if w != nil {
		t.window = w
	}
	f()
	t.window = prevWindow
}

func (t *Terminal) parsePrompt(prompt string) (func(), int) {
	var state *ansiState
	prompt = firstLine(prompt)
	trimmed, colors, _ := extractColor(prompt, state, nil)
	item := &Item{text: util.ToChars([]byte(trimmed)), colors: colors}

	// "Prompt>  "
	//  -------    // Do not apply ANSI attributes to the trailing whitespaces
	//             // unless the part has a non-default ANSI state
	loc := whiteSuffix.FindStringIndex(trimmed)
	if loc != nil {
		blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{tui.ColPrompt.Fg(), tui.ColPrompt.Bg(), -1, tui.AttrClear, -1, nil}}
		if item.colors != nil {
			lastColor := (*item.colors)[len(*item.colors)-1]
			if lastColor.offset[1] < int32(loc[1]) {
				blankState.offset[0] = lastColor.offset[1]
				colors := append(*item.colors, blankState)
				item.colors = &colors
			}
		} else {
			colors := []ansiOffset{blankState}
			item.colors = &colors
		}
	}
	output := func() {
		wrap := t.wrap
		t.wrap = false
		t.withWindow(t.inputWindow, func() {
			preTask := func(markerClass) int {
				return 1
			}
			t.printHighlighted(
				Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, 0, 0, true, preTask, nil, 0)
		})
		t.wrap = wrap
	}
	_, promptLen := t.processTabsStr(trimmed, 0)

	return output, promptLen
}

func (t *Terminal) noSeparatorLine() bool {
	return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLength() > 0)
}

// Effective length of the horizontal separator. The default separator is
// suppressed when the input section is already visually separated from the
// list section by a border line.
func (t *Terminal) separatorLength() int {
	if t.separatorAuto && t.separatedByBorder() {
		return 0
	}
	return t.separatorLen
}

// BorderLine for the input border resolves to a single line on the side
// facing the list section
func resolveInputBorderShape(layout layoutType, shape tui.BorderShape) tui.BorderShape {
	if shape != tui.BorderLine {
		return shape
	}
	if layout == layoutReverse {
		return tui.BorderBottom
	}
	return tui.BorderTop
}

// Whether the input border draws a line on the side facing the list section
func inputBorderFacesList(layout layoutType, shape tui.BorderShape) bool {
	shape = resolveInputBorderShape(layout, shape)
	if layout == layoutReverse {
		return shape.HasBottom()
	}
	return shape.HasTop()
}

// Whether a border line is already drawn between the info line and the
// section next to it on the list side, making the default horizontal
// separator redundant
func (t *Terminal) separatedByBorder() bool {
	// The input border itself draws a line on the side facing the list section
	if inputBorderFacesList(t.layout, t.inputBorderShape) {
		return true
	}
	// A preview window at 'next' position sits right next to the input
	// section, in front of the sections handled below
	anyNext := false
	for po := &t.previewOpts; po != nil; po = po.alternative {
		if po.position == posNext {
			anyNext = true
			break
		}
	}
	if anyNext && t.hasPreviewer() {
		// The separator is hidden only when every spec in the threshold chain
		// displays a preview window at 'next' position with a border line
		// facing the input section. We cannot single out the active spec;
		// which one is active depends on the terminal size, and the
		// resolution (computePreviewSize in resizeWindows) itself consults
		// noSeparatorLine. When not every spec qualifies, err on the side of
		// showing the separator.
		if len(t.previewOpts.command) == 0 {
			// No preview command, so no preview window is normally shown, but
			// the preview(...) action can still display one at any time
			return false
		}
		// NOTE: resizeWindows can clear 'hidden' mid-layout when the
		// preview(...) action forces the window, so the frame being laid out
		// can briefly disagree with this decision until the next relayout
		for po := &t.previewOpts; po != nil; po = po.alternative {
			if po.position != posNext || po.hidden || po.size.size == 0 || !t.borderFacingInput(po.Border(t.layout)) {
				return false
			}
		}
		return true
	}
	hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
	hasHeaderWindow := t.hasHeaderWindow()
	// Mirror of hasInputWindow in resizeWindows; a 'next' preview position
	// gives the input section its own window even when no preview window is
	// shown. A 'next' spec here implies there is no previewer (the block
	// above returns otherwise), and without a previewer the position is
	// resolved from the first spec alone, so a 'next' in a threshold
	// alternative does not count.
	hasInputWindow := t.previewOpts.position == posNext || t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow
	if !hasInputWindow {
		// The input section is embedded in the list window and no border
		// separates the two
		return false
	}

	// The input section has its own window. Determine which section it is
	// facing, and check if the border of that section draws a line toward it.
	// NOTE: hasHeaderWindow can be true with no header content when the input
	// border is visible (see Terminal.hasHeaderWindow). An empty header window
	// takes no space, so it cannot be the facing section.
	hasHeaderSection := hasHeaderWindow && t.visibleHeaderLines() > 0
	if hasHeaderSection && !t.headerFirst && t.headerBorderShape != tui.BorderInline {
		// Header section is facing the input section. An inline header is
		// excluded; it is drawn inside the list border and is covered by the
		// branches below.
		return t.borderFacingInput(t.headerBorderShape)
	}
	// Header lines section is facing the input section, except in
	// reverse-list layout where it is on the other side of the list section,
	// or with --header-first and no header section where it is moved past
	// the input section
	if hasHeaderLinesWindow && t.layout != layoutReverseList && (hasHeaderWindow || !t.headerFirst) {
		return t.borderFacingInput(headerLinesShape)
	}
	// The input section sits right next to the list section
	return t.borderFacingInput(t.listBorderShape)
}

// Whether the border of the section next to the input section on the list
// side draws a horizontal line facing the info line
func (t *Terminal) borderFacingInput(shape tui.BorderShape) bool {
	if shape == tui.BorderInline {
		// Embedded between the horizontal lines of the list border
		return true
	}
	if t.layout == layoutReverse {
		return shape.HasTop()
	}
	return shape.HasBottom()
}

func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
	if total == 0 || total*perLine <= height {
		return 0, 0
	}
	barLength := max(1, height*height/(total*perLine))
	var barStart int
	if total == height {
		barStart = 0
	} else {
		barStart = min(height-barLength, (height*perLine-barLength)*offset/(total*perLine-height))
	}
	return barLength, barStart
}

func (t *Terminal) barCol() int {
	if len(t.scrollbar) == 0 && !t.listBorderShape.HasRight() && !t.borderShape.HasRight() && !t.hasPreviewWindowOnRight() {
		return 0
	}
	return 1
}

func (t *Terminal) wrapCols() int {
	if !t.wrap {
		return 0 // No wrap
	}
	return max(t.window.Width()-(t.pointerLen+t.markerLen+t.barCol()), 1)
}

func (t *Terminal) clearNumLinesCache() {
	t.numLinesCache = make(map[int32]numLinesCacheValue)
}

// Number of lines the item takes including the gap
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
	var numLines int
	if !t.wrap && !t.multiLine {
		numLines = 1 + t.gap
		return numLines, numLines > atMost
	}
	if cached, prs := t.numLinesCache[item.Index()]; prs {
		// Can we use this cache? Let's be conservative.
		if cached.atMost <= atMost {
			return cached.numLines, false
		}
	}
	var overflow bool
	if !t.wrap && t.multiLine {
		numLines, overflow = item.text.NumLines(atMost)
	} else {
		var lines [][]rune
		lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop, t.wrapWord)
		numLines = len(lines)
	}
	numLines += t.gap
	if !overflow {
		t.numLinesCache[item.Index()] = numLinesCacheValue{atMost, numLines}
	}
	return numLines, overflow || numLines > atMost
}

func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
	if !t.wrap && !t.multiLine {
		text := make([]rune, item.text.Length())
		copy(text, item.text.ToRunes())
		return [][]rune{text}, false
	}
	return item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop, t.wrapWord)
}

// Estimate the average number of lines per item. Instead of going through all
// items, we only check a few items around the current cursor position.
func (t *Terminal) avgNumLines() int {
	if !t.wrap && !t.multiLine {
		return 1
	}

	maxItems := t.maxItems()
	numLines := 0
	count := 0
	total := t.merger.Length()
	offset := max(0, min(t.offset, total-maxItems-1))
	for idx := 0; idx < maxItems && idx+offset < total; idx++ {
		result := t.merger.Get(idx + offset)
		lines, _ := t.numItemLines(result.item, maxItems)
		numLines += lines
		count++
	}
	if count == 0 {
		return 1
	}
	return numLines / count
}

func (t *Terminal) getScrollbar() (int, int) {
	return getScrollbar(t.avgNumLines(), t.merger.Length(), t.maxItems(), t.offset)
}

// Input returns current query string
func (t *Terminal) Input() (bool, []rune) {
	t.mutex.Lock()
	defer t.mutex.Unlock()
	paused := t.paused
	src := t.input
	if t.inputOverride != nil {
		paused = false
		src = *t.inputOverride
	}
	return paused, copySlice(src)
}

// PauseRendering blocks the terminal from reading items.
// Must be paired with ResumeRendering.
func (t *Terminal) PauseRendering() {
	t.mutex.Lock()
}

// ResumeRendering releases the lock acquired by PauseRendering.
func (t *Terminal) ResumeRendering() {
	t.mutex.Unlock()
}

// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
	t.mutex.Lock()
	t.count = cnt
	if t.hasLoadActions && t.reading && final {
		t.triggerLoad = true
	}
	t.reading = !final
	t.failed = failedCommand
	suppressed := t.suppress
	t.mutex.Unlock()
	t.reqBox.Set(reqInfo, nil)

	// We want to defer activating the interface when --sync is used and any of
	// start, load, or result events are bound
	if suppressed && final && !t.deferActivation() {
		t.reqBox.Set(reqActivate, nil)
	}
}

func (t *Terminal) changeHeader(header string) bool {
	var lines []string
	if len(header) > 0 {
		lines = strings.Split(strings.TrimSuffix(header, "\n"), "\n")
	}
	needFullRedraw := len(t.header0) != len(lines)
	t.header0 = lines
	t.clickHeaderLine = 0
	t.clickHeaderColumn = 0
	return needFullRedraw
}

func (t *Terminal) changeFooter(footer string) bool {
	var lines []string
	if len(footer) > 0 {
		lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
	}
	needFullRedraw := len(t.footer) != len(lines)
	t.footer = lines
	t.clickFooterLine = 0
	t.clickFooterColumn = 0
	return needFullRedraw
}

// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []Item) {
	t.mutex.Lock()
	// Pad to t.headerLines so that click coordinate mapping works correctly
	if len(header) < t.headerLines {
		padded := make([]Item, t.headerLines)
		copy(padded, header)
		header = padded
	}
	t.header = 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(result MatchResult) {
	merger := result.merger
	t.mutex.Lock()
	waitWasBlocked := t.wait.blocked
	wakeUp := false
	if result.final() {
		t.wait.searching = false
		// If waiting, unblock so main loop can execute pending actions.
		// Note: any final result unblocks the wait, not just the one for the
		// search that armed it. Back-to-back searches (--listen, bg-transform
		// callbacks, reload+wait) can therefore unblock early and run the
		// pending actions on the previous result set. Accepted; a per-search
		// generation token through the matcher isn't worth the complexity.
		if t.wait.blocked {
			t.unblockWait()
			wakeUp = len(t.wait.pending) > 0
		}
	}
	prevIndex := minItem.Index()
	newRevision := merger.Revision()
	if t.revision.compatible(newRevision) && t.track != trackDisabled {
		if t.merger.Length() > 0 {
			prevIndex = t.currentIndex()
		} else if merger.Length() > 0 {
			prevIndex = merger.First().item.Index()
		}
	}
	if t.targetIndex != minItem.Index() {
		prevIndex = t.targetIndex
		t.targetIndex = minItem.Index()
	}
	if result.final() {
		t.progress = 100
	}
	t.merger = merger
	t.resultMerger = merger
	t.passMerger = result.passMerger
	if t.raw {
		t.merger = result.passMerger
		t.matchMap = t.resultMerger.ToMap()
	} else {
		t.merger = result.merger
		t.matchMap = make(map[int32]Result)
	}
	if t.revision != newRevision {
		if !t.revision.compatible(newRevision) {
			// Reloaded: capture selection keys for restoration, then clear (reload-sync only)
			if t.trackSync && len(t.idNth) > 0 && t.multi > 0 && len(t.selected) > 0 {
				t.pendingSelections = make(map[string]selectedItem, len(t.selected))
				for _, sel := range t.selected {
					key := t.trackKeyFor(sel.item, t.idNth)
					t.pendingSelections[key] = sel
				}
			}
			t.selected = make(map[int32]selectedItem)
			t.clearNumLinesCache()
		} else {
			// Trimmed by --tail: filter selection by index
			filtered := make(map[int32]selectedItem)
			minIndex := merger.minIndex
			maxIndex := merger.maxIndex
			for k, v := range t.selected {
				var included bool
				if maxIndex > minIndex {
					included = k >= minIndex && k < maxIndex
				} else if maxIndex < minIndex { // int32 overflow [==>   <==]
					included = k >= minIndex || k < maxIndex
				}
				if included {
					filtered[k] = v
				}
			}
			t.selected = filtered
		}
		t.revision = newRevision
		t.version++

		// Filter out selections that no longer match after with-nth change.
		// Must be inside the revision check so we don't consume the flag
		// on a stale EvtSearchFin from a previous search.
		if t.filterSelection && t.multi > 0 && len(t.selected) > 0 {
			matchMap := t.resultMerger.ToMap()
			filtered := make(map[int32]selectedItem)
			for k, v := range t.selected {
				if _, matched := matchMap[k]; matched {
					filtered[k] = v
				}
			}
			t.selected = filtered
		}
		t.filterSelection = false
	}
	if t.triggerLoad {
		t.triggerLoad = false
		t.pendingReqList = true
		t.eventChan <- tui.Load.AsEvent()
	}
	// Search for the tracked item by nth key
	// - reload (async): search eagerly, unblock as soon as match is found
	// - reload-sync: wait until stream is complete before searching
	trackWasBlocked := t.trackBlocked
	if len(t.trackKey) > 0 && (!t.trackSync || !t.reading) {
		found := false
		for i := 0; i < t.merger.Length(); i++ {
			item := t.merger.Get(i).item
			idx := item.Index()
			match, ok := t.trackKeyCache[idx]
			if !ok {
				match = t.trackKeyFor(item, t.idNth) == t.trackKey
				t.trackKeyCache[idx] = match
			}
			if match {
				t.cy = i
				if t.track.Current() {
					t.track.index = idx
				}
				found = true
				break
			}
		}
		if found || !t.reading {
			t.unblockTrack()
		}
	} else if prevIndex >= 0 {
		pos := t.cy - t.offset
		count := t.merger.Length()
		i := t.merger.FindIndex(prevIndex)
		if i >= 0 {
			t.cy = i
			t.offset = t.cy - pos
		} else if t.track.Current() {
			t.track = trackDisabled
			t.cy = pos
			t.offset = 0
		} else if t.cy > count {
			// Try to keep the vertical position when the list shrinks
			t.cy = count - min(count, t.maxItems()) + pos
		}
	}
	// Restore selections by id-nth key after reload completes
	if !t.reading && t.pendingSelections != nil {
		for i := 0; i < t.merger.Length() && len(t.pendingSelections) > 0; i++ {
			item := t.merger.Get(i).item
			key := t.trackKeyFor(item, t.idNth)
			if sel, found := t.pendingSelections[key]; found {
				t.selected[item.Index()] = selectedItem{sel.at, item}
				delete(t.pendingSelections, key)
			}
		}
		t.pendingSelections = nil
	}
	needActivation := false
	if !t.reading {
		switch t.resultMerger.Length() {
		case 0:
			zero := tui.Zero.AsEvent()
			if _, prs := t.keymap[zero]; prs {
				t.pendingReqList = true
				t.eventChan <- zero
			}
			// --sync, only 'focus' is bound, but no items to focus
			needActivation = t.suppress && !t.hasResultActions && !t.hasLoadActions && t.hasFocusActions
		case 1:
			one := tui.One.AsEvent()
			if _, prs := t.keymap[one]; prs {
				t.pendingReqList = true
				t.eventChan <- one
			}
		}
	}
	if t.hasResultActions {
		result := tui.Result.AsEvent()
		if _, prs := t.keymap[result]; prs {
			t.pendingReqList = true
			t.eventChan <- result
		}
		if !t.reading {
			resultFinal := tui.ResultFinal.AsEvent()
			if _, prs := t.keymap[resultFinal]; prs {
				t.pendingReqList = true
				t.eventChan <- resultFinal
			}
		}
	}
	updateList := !t.trackBlocked && !t.pendingReqList
	updatePrompt := (trackWasBlocked && !t.trackBlocked) || (waitWasBlocked && !t.wait.blocked)
	t.mutex.Unlock()

	// Wake up the main loop to execute pending actions after wait unblocks.
	// Send from a goroutine; UpdateList runs inside the event box callback,
	// and an inline send on a full channel would deadlock with the main loop
	// blocking on eventBox.Set while trying to drain the channel.
	if wakeUp {
		go func() {
			t.serverInputChan <- []*action{{t: actIgnore}}
		}()
	}

	t.reqBox.Set(reqInfo, nil)
	if updateList {
		t.reqBox.Set(reqList, nil)
	}
	if updatePrompt {
		t.reqBox.Set(reqPrompt, nil)
	}
	if needActivation {
		t.reqBox.Set(reqActivate, nil)
	}
}

func (t *Terminal) output() bool {
	if t.printQuery {
		t.printer(string(t.input))
	}
	if len(t.expect) > 0 {
		t.printer(t.pressed)
	}
	for _, s := range t.printQueue {
		t.printer(s)
	}
	transform := func(item *Item) string {
		return item.AsString(t.ansi)
	}
	if t.acceptNth != nil {
		transform = func(item *Item) string {
			return item.acceptNth(t.ansi, t.delimiter, t.acceptNth)
		}
	}
	found := len(t.selected) > 0
	if !found {
		current := t.currentItem()
		if current != nil {
			t.printer(transform(current))
			found = true
		}
	} else {
		for _, sel := range t.sortSelected() {
			t.printer(transform(sel.item))
		}
	}
	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 {
	width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
	return width
}

func (t *Terminal) displayWidthWithPrefix(str string, prefixWidth int) int {
	width, _ := util.RunesWidth([]rune(str), prefixWidth, t.tabstop, math.MaxInt32)
	return width
}

// displayWidthWithoutEscapes is displayWidthWithPrefix for a string that may
// still carry pass-throughs and ANSI codes, neither of which take any column.
func (t *Terminal) displayWidthWithoutEscapes(str string, prefixWidth int) int {
	_, text := extractPassThroughs(str)
	stripped, _, _ := extractColor(text, nil, nil)
	return t.displayWidthWithPrefix(stripped, prefixWidth)
}

const (
	minWidth  = 4
	minHeight = 3
)

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

func (t *Terminal) minPreviewSize(opts *previewOpts) (int, int) {
	border := opts.Border(t.layout)
	minPreviewWidth := 1 + borderColumns(border, t.borderWidth)
	minPreviewHeight := 1 + borderLines(border)

	switch opts.position {
	case posLeft, posRight:
		if len(t.scrollbar) > 0 && !border.HasRight() {
			// Need a column to show scrollbar
			minPreviewWidth++
		}
	}

	return minPreviewWidth, minPreviewHeight
}

func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
	screenWidth := t.tui.MaxX()
	screenHeight := t.tui.MaxY()
	marginInt := [4]int{}  // TRBL
	paddingInt := [4]int{} // TRBL
	sizeSpecToInt := func(index int, spec sizeSpec) int {
		if spec.percent {
			var max float64
			if index%2 == 0 {
				max = float64(screenHeight)
			} else {
				max = float64(screenWidth)
			}
			return int(max * spec.size * 0.01)
		}
		return int(spec.size)
	}
	for idx, sizeSpec := range t.padding {
		paddingInt[idx] = sizeSpecToInt(idx, sizeSpec)
	}

	bw := t.borderWidth
	extraMargin := [4]int{} // TRBL
	for idx, sizeSpec := range t.margin {
		switch t.borderShape {
		case tui.BorderHorizontal:
			extraMargin[idx] += 1 - idx%2
		case tui.BorderVertical:
			extraMargin[idx] += (1 + bw) * (idx % 2)
		case tui.BorderTop:
			if idx == 0 {
				extraMargin[idx]++
			}
		case tui.BorderRight:
			if idx == 1 {
				extraMargin[idx] += 1 + bw
			}
		case tui.BorderBottom:
			if idx == 2 {
				extraMargin[idx]++
			}
		case tui.BorderLeft:
			if idx == 3 {
				extraMargin[idx] += 1 + bw
			}
		case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble, tui.BorderDashed:
			extraMargin[idx] += 1 + bw*(idx%2)
		}
		marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
	}

	adjust := func(idx1 int, idx2 int, maximum int, minimum int) {
		if minimum > maximum {
			minimum = maximum
		}
		margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2]
		if maximum-margin < minimum {
			desired := maximum - minimum
			paddingInt[idx1] = desired * paddingInt[idx1] / margin
			paddingInt[idx2] = desired * paddingInt[idx2] / margin
			marginInt[idx1] = max(extraMargin[idx1], desired*marginInt[idx1]/margin)
			marginInt[idx2] = max(extraMargin[idx2], desired*marginInt[idx2]/margin)
		}
	}

	minAreaWidth := minWidth
	minAreaHeight := minHeight
	if t.inputless {
		minAreaHeight--
	}
	if t.noSeparatorLine() {
		minAreaHeight--
	}
	if t.needPreviewWindow() {
		minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts)
		switch t.activePreviewOpts.position {
		case posUp, posDown, posNext:
			minAreaHeight += minPreviewHeight
			minAreaWidth = max(minPreviewWidth, minAreaWidth)
		case posLeft, posRight:
			minAreaWidth += minPreviewWidth
			minAreaHeight = max(minPreviewHeight, minAreaHeight)
		}
	}
	adjust(1, 3, screenWidth, minAreaWidth)
	adjust(0, 2, screenHeight, minAreaHeight)

	return screenWidth, screenHeight, marginInt, paddingInt
}

func (t *Terminal) forceRerenderList() {
	t.prevLines = make([]itemLine, len(t.prevLines))
}

func (t *Terminal) hasHeaderWindow() bool {
	if !t.headerVisible {
		return false
	}
	if t.hasHeaderLinesWindow() {
		return len(t.header0) > 0
	}
	if t.headerBorderShape.Visible() || t.headerFirst {
		return len(t.header0)+t.headerLines > 0
	}
	return t.inputBorderShape.Visible()
}

func (t *Terminal) hasHeaderLinesWindow() bool {
	w, _ := t.determineHeaderLinesShape()
	return w
}

func (t *Terminal) determineHeaderLinesShape() (bool, tui.BorderShape) {
	if !t.headerVisible || t.headerLines == 0 {
		return false, tui.BorderNone
	}

	// --header-lines-border is set
	if t.headerLinesShape != tui.BorderUndefined {
		return true, t.headerLinesShape
	}

	// --header-lines-border is not set, determine if we should use
	// the style of --header-border
	shape := tui.BorderNone
	if len(t.header0) == 0 {
		shape = t.headerBorderShape
	}
	if shape == tui.BorderNone {
		shape = tui.BorderPhantom
	}

	// --layout reverse-list is set
	if t.layout == layoutReverseList {
		return true, shape
	}

	// Use header window instead
	if len(t.header0) == 0 {
		if t.headerFirst && shape == tui.BorderPhantom {
			return true, shape
		}
		return false, t.headerBorderShape
	}

	// We have both types of headers, and we want to separate the two
	if t.headerFirst {
		return true, shape
	}

	return false, tui.BorderNone
}

// Inline sections live inside wborder rather than consuming shift/shrink/availableLines.
type inlineRole int

const (
	inlineRoleHeader inlineRole = iota
	inlineRoleHeaderLines
	inlineRoleFooter
)

type inlineSlot struct {
	role         inlineRole
	windowType   tui.WindowType
	contentLines int // 0 when out of budget: a ghost placeholder is created so
	// t.headerWindow / t.footerWindow stay non-nil but no frame is painted.
	label     labelPrinter
	labelOpts labelOpts
	labelLen  int
}

// inlineMetaFor returns (onTop: top stack vs bottom; windowType; isInner:
// adjacent to list content) for the given inline role, derived from the layout.
// Header is outer when paired with header-lines; header-lines is always inner;
// footer is inner except in reverseList where it sits outside header-lines.
func (t *Terminal) inlineMetaFor(role inlineRole) (onTop bool, windowType tui.WindowType, isInner bool) {
	switch role {
	case inlineRoleHeader:
		return t.layout == layoutReverse, tui.WindowHeader, false
	case inlineRoleHeaderLines:
		return t.layout != layoutDefault, tui.WindowHeader, true
	case inlineRoleFooter:
		return t.layout != layoutReverse, tui.WindowFooter, t.layout != layoutReverseList
	}
	return false, tui.WindowBase, false
}

func (t *Terminal) placeInlineSection(win tui.Window, role inlineRole) {
	switch role {
	case inlineRoleHeader:
		t.headerWindow = win
	case inlineRoleHeaderLines:
		t.headerLinesWindow = win
	case inlineRoleFooter:
		t.footerWindow = win
	}
}

// placeInlineStack walks `slots` in outer-to-inner order. Only the outermost
// slot (index 0) claims the adjacent wborder edge; later slots paint side-only
// frames so the T-junction separator lands between sections.
func (t *Terminal) placeInlineStack(slots []inlineSlot, startRow int, onTop bool) {
	firstEdge := tui.SectionEdgeTop
	if !onTop {
		firstEdge = tui.SectionEdgeBottom
	}
	noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
	subLeft := t.window.Left()
	subWidth := t.window.Width()
	cursor := startRow
	for i, s := range slots {
		var windowTop, sepRow int
		if onTop {
			windowTop = cursor
			sepRow = cursor + s.contentLines - t.wborder.Top()
		} else {
			windowTop = cursor - s.contentLines + 1
			sepRow = windowTop - 1 - t.wborder.Top()
		}
		// 0-height placeholder keeps t.headerWindow / t.footerWindow non-nil.
		win := t.tui.NewWindow(windowTop, subLeft, subWidth, s.contentLines, s.windowType, noBorder, true)
		t.placeInlineSection(win, s.role)
		if s.contentLines == 0 {
			continue
		}
		secTop := windowTop - t.wborder.Top()
		secBottom := secTop + s.contentLines - 1
		edge := tui.SectionEdgeNone
		if i == 0 {
			edge = firstEdge
		}
		t.wborder.PaintSectionFrame(secTop, secBottom, s.windowType, edge)
		// useBottom=onTop so the separator always hugs the list (inner side).
		// Matters for thinblock/block where top != bottom char.
		t.wborder.DrawHSeparator(sepRow, s.windowType, onTop)
		t.printLabelAt(t.wborder, s.label, s.labelOpts, s.labelLen, sepRow)
		if onTop {
			cursor += s.contentLines + 1
		} else {
			cursor -= s.contentLines + 1
		}
	}
}

func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
	t.clearNumLinesCache()
	t.forcePreview = forcePreview
	screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
	width := screenWidth - marginInt[1] - marginInt[3]
	height := screenHeight - marginInt[0] - marginInt[2]

	t.prevLines = make([]itemLine, max(1, screenHeight))
	if t.border != nil && redrawBorder {
		t.border = nil
	}
	if t.window != nil {
		t.window = nil
	}
	if t.wborder != nil {
		t.wborder = nil
	}
	if t.headerWindow != nil {
		t.headerWindow = nil
	}
	if t.headerBorder != nil {
		t.headerBorder = nil
	}
	if t.footerWindow != nil {
		t.footerWindow = nil
	}
	if t.footerBorder != nil {
		t.footerBorder = nil
	}
	if t.headerLinesWindow != nil {
		t.headerLinesWindow = nil
	}
	if t.headerLinesBorder != nil {
		t.headerLinesBorder = nil
	}
	if t.inputWindow != nil {
		t.inputWindow = nil
	}
	if t.inputBorder != nil {
		t.inputBorder = nil
	}
	if t.pborder != nil {
		t.pborder = nil
	}
	hadPreviewWindow := t.hasPreviewWindow()
	if hadPreviewWindow {
		t.pwindow = nil
	}
	// Reset preview version so that full redraw occurs
	t.previewed.version = 0

	bw := t.borderWidth
	offsets := [4]int{} // TRWH
	if t.borderShape.HasTop() {
		offsets[0] -= 1
		offsets[3] += 1
	}
	if t.borderShape.HasRight() {
		offsets[2] += 1 + bw
	}
	if t.borderShape.HasBottom() {
		offsets[3] += 1
	}
	if t.borderShape.HasLeft() {
		offsets[1] -= 1 + bw
		offsets[2] += 1 + bw
	}
	if t.border == nil && t.borderShape.Visible() {
		t.border = t.tui.NewWindow(
			marginInt[0]+offsets[0], marginInt[3]+offsets[1], width+offsets[2], height+offsets[3],
			tui.WindowBase, tui.MakeBorderStyle(t.borderShape, t.unicode), true)
	}

	// Add padding to margin
	for idx, val := range paddingInt {
		marginInt[idx] += val
	}
	width -= paddingInt[1] + paddingInt[3]
	height -= paddingInt[0] + paddingInt[2]

	// Adjust position and size of the list window if input border is set
	inputBorderHeight := 0
	availableLines := height

	shift := 0
	shrink := 0
	hasHeaderWindow := t.hasHeaderWindow()
	hasFooterWindow := len(t.footer) > 0
	hasHeaderLinesWindow, headerLinesShape := t.determineHeaderLinesShape()
	// computePreviewSize returns the size resizePreviewWindows will compute
	// for opts and the minimum size for that axis: height/minPreviewHeight
	// for vertical positions, width/minPreviewWidth for horizontal.
	computePreviewSize := func(opts *previewOpts) (int, int) {
		minPreviewWidth, minPreviewHeight := t.minPreviewSize(opts)
		switch opts.position {
		case posUp, posDown, posNext:
			minWindowHeight := minHeight
			if t.inputless {
				minWindowHeight--
			}
			if t.noSeparatorLine() {
				minWindowHeight--
			}
			return calculateSize(height, opts.size, minWindowHeight, minPreviewHeight), minPreviewHeight
		case posLeft, posRight:
			minListWidth := minWidth
			if t.listBorderShape.HasLeft() {
				minListWidth += 2
			}
			if t.listBorderShape.HasRight() {
				minListWidth++
			}
			return calculateSize(width, opts.size, minListWidth, minPreviewWidth), minPreviewWidth
		}
		return 0, 0
	}
	// Walk the threshold chain to determine the previewOpts that
	// resizePreviewWindows will actually settle on. We need this here
	// because hasInputWindow and the availableLines adjustment below run
	// before resizePreviewWindows, and t.activePreviewOpts still holds the
	// previous frame's resolution.
	effectivePreviewOpts := &t.previewOpts
	if t.needPreviewWindow() {
		opts := &t.previewOpts
		for {
			if opts.size.size == 0 || opts.threshold <= 0 || opts.alternative == nil {
				break
			}
			if actual, _ := computePreviewSize(opts); actual >= opts.threshold {
				break
			}
			opts = opts.alternative
			if opts.hidden {
				break
			}
		}
		effectivePreviewOpts = opts
	}
	hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow || effectivePreviewOpts.position == posNext)
	inputWindowHeight := 2
	if t.noSeparatorLine() {
		inputWindowHeight--
	}
	if hasInputWindow {
		inputBorderHeight = util.Constrain(borderLines(t.inputBorderShape)+inputWindowHeight, 0, availableLines)
		if t.layout == layoutReverse {
			shift = inputBorderHeight
			shrink = inputBorderHeight
		} else {
			shrink = inputBorderHeight
		}
		availableLines -= inputBorderHeight
	} else if !t.inputless {
		availableLines -= inputWindowHeight
	}

	// FIXME: Needed?
	if t.needPreviewWindow() {
		switch effectivePreviewOpts.position {
		case posUp, posDown, posNext:
			_, minPreviewHeight := t.minPreviewSize(effectivePreviewOpts)
			availableLines -= minPreviewHeight
		}
	}

	// Slices are ordered outer-to-inner: index 0 touches wborder's edge.
	var inlineTop []inlineSlot
	var inlineBottom []inlineSlot

	// Caps contentLines against remaining wborder space. Oversized requests (e.g. a
	// huge --header-lines) become 0-height placeholders rather than pushing the list
	// window to negative height.
	addInline := func(contentLines int, role inlineRole) {
		onTop, windowType, isInner := t.inlineMetaFor(role)
		used := 0
		for _, s := range inlineTop {
			if s.contentLines > 0 {
				used += s.contentLines + 1
			}
		}
		for _, s := range inlineBottom {
			if s.contentLines > 0 {
				used += s.contentLines + 1
			}
		}
		remaining := availableLines - borderLines(t.listBorderShape) - used - 1
		if remaining < 2 {
			contentLines = 0
		} else {
			contentLines = util.Constrain(contentLines, 1, remaining-1)
		}
		slot := inlineSlot{role: role, windowType: windowType, contentLines: contentLines}
		switch role {
		case inlineRoleHeader:
			slot.label, slot.labelOpts, slot.labelLen = t.headerLabel, t.headerLabelOpts, t.headerLabelLen
		case inlineRoleFooter:
			slot.label, slot.labelOpts, slot.labelLen = t.footerLabel, t.footerLabelOpts, t.footerLabelLen
		}
		target := &inlineTop
		if !onTop {
			target = &inlineBottom
		}
		if isInner {
			*target = append(*target, slot)
		} else {
			*target = append([]inlineSlot{slot}, *target...)
		}
	}

	// Adjust position and size of the list window if header border is set
	headerBorderHeight := 0
	if hasHeaderWindow {
		headerWindowHeight := t.visibleHeaderLines()
		if hasHeaderLinesWindow {
			headerWindowHeight -= t.headerLines
		}
		if t.headerBorderShape == tui.BorderInline {
			addInline(headerWindowHeight, inlineRoleHeader)
		} else {
			headerBorderHeight = util.Constrain(borderLines(t.headerBorderShape)+headerWindowHeight, 0, availableLines)
			if t.layout == layoutReverse {
				shift += headerBorderHeight
				shrink += headerBorderHeight
			} else {
				shrink += headerBorderHeight
			}
			availableLines -= headerBorderHeight
		}
	}

	headerLinesHeight := 0
	if hasHeaderLinesWindow {
		if headerLinesShape == tui.BorderInline {
			addInline(t.headerLines, inlineRoleHeaderLines)
		} else {
			headerLinesHeight = util.Constrain(borderLines(headerLinesShape)+t.headerLines, 0, availableLines)
			if t.layout != layoutDefault {
				shift += headerLinesHeight
				shrink += headerLinesHeight
			} else {
				shrink += headerLinesHeight
			}
			availableLines -= headerLinesHeight
		}
	}

	footerBorderHeight := 0
	if hasFooterWindow {
		if t.footerBorderShape == tui.BorderInline {
			addInline(len(t.footer), inlineRoleFooter)
		} else {
			// Footer lines should not take all available lines
			footerBorderHeight = util.Constrain(borderLines(t.footerBorderShape)+len(t.footer), 0, availableLines)
			shrink += footerBorderHeight
			if t.layout != layoutReverse {
				shift += footerBorderHeight
			}
			availableLines -= footerBorderHeight
		}
	}

	inlineTopLines := 0
	for _, s := range inlineTop {
		if s.contentLines > 0 {
			inlineTopLines += s.contentLines + 1
		}
	}
	inlineBottomLines := 0
	for _, s := range inlineBottom {
		if s.contentLines > 0 {
			inlineBottomLines += s.contentLines + 1
		}
	}

	// Set up list border
	hasListBorder := t.listBorderShape.Visible()
	innerWidth := width
	innerHeight := height
	innerMarginInt := marginInt
	innerBorderFn := func(top int, left int, width int, height int) {
		if hasListBorder {
			t.wborder = t.tui.NewWindow(
				top+shift, left, width, height-shrink, tui.WindowList, tui.MakeBorderStyle(t.listBorderShape, t.unicode), false)
		}
	}
	if hasListBorder {
		if t.listBorderShape.HasTop() {
			innerHeight--
			innerMarginInt[0]++
		}
		if t.listBorderShape.HasBottom() {
			innerHeight--
		}
		if t.listBorderShape.HasLeft() {
			innerWidth -= 2
			innerMarginInt[3] += 2
		}
		if t.listBorderShape.HasRight() {
			innerWidth--
		}
	}

	t.areaLines = height
	t.areaColumns = width

	// If none of the inner borders has the right side, but the outer border does, increase the list width by 1 column
	listStickToRight := t.borderShape.HasRight() && !t.listBorderShape.HasRight() && !t.inputBorderShape.HasRight() &&
		(!t.headerVisible || !t.headerBorderShape.HasRight() || t.visibleHeaderLines() == 0)

	// Set up preview window
	noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
	cleanLeft := []int{}
	// previewNextSize is pheight when the preview is placed adjacent to
	// the input (position == "next"); inputBorderTop() reads it through the
	// closure to push input past the preview band.
	previewNextSize := 0
	// inputBorderTop returns the canvas Y at which the input border window
	// should be placed. It depends on wborder/t.window (set by the preview
	// case), the layout, --header-first, and previewNextSize (set when
	// posNext is active). Used both for placing the preview adjacent to
	// input and later for placing the input window itself.
	inputBorderTop := func() int {
		w := t.wborder
		if w == nil {
			w = t.window
		}
		hasSeparateHeader := hasHeaderWindow && t.headerBorderShape != tui.BorderInline
		hasSeparateHeaderLines := hasHeaderLinesWindow && headerLinesShape != tui.BorderInline
		if (hasSeparateHeader || hasSeparateHeaderLines) && t.headerFirst {
			switch t.layout {
			case layoutDefault:
				btop := w.Top() + w.Height() + previewNextSize
				if hasHeaderWindow && hasHeaderLinesWindow {
					btop += headerLinesHeight
				}
				return btop
			case layoutReverse:
				btop := w.Top() - inputBorderHeight - previewNextSize
				if hasHeaderWindow && hasHeaderLinesWindow {
					btop -= headerLinesHeight
				}
				return btop
			case layoutReverseList:
				return w.Top() + w.Height() + previewNextSize
			}
		}
		switch t.layout {
		case layoutDefault:
			return w.Top() + w.Height() + headerBorderHeight + headerLinesHeight + previewNextSize
		case layoutReverse:
			return w.Top() - shrink + footerBorderHeight - previewNextSize
		case layoutReverseList:
			return w.Top() + w.Height() + headerBorderHeight + previewNextSize
		}
		return 0
	}
	if forcePreview || t.needPreviewWindow() {
		var resizePreviewWindows func(previewOpts *previewOpts)
		resizePreviewWindows = func(previewOpts *previewOpts) {
			t.activePreviewOpts = previewOpts
			if previewOpts.size.size == 0 {
				return
			}
			hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil
			createPreviewWindow := func(y int, x int, w int, h int) {
				pwidth := w
				pheight := h
				shape := previewOpts.Border(t.layout)
				previewBorder := tui.MakeBorderStyle(shape, t.unicode)
				t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false)
				pwidth -= borderColumns(shape, bw)
				pheight -= borderLines(shape)
				if shape.HasLeft() {
					x += 1 + bw
				}
				if shape.HasTop() {
					y += 1
				}
				if len(t.scrollbar) > 0 && !shape.HasRight() {
					// Need a column to show scrollbar
					pwidth -= 1
				}
				t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
				t.pwindow.SetWrapSign(t.previewWrapSign, t.previewWrapSignWidth)
				if !hadPreviewWindow {
					t.pwindow.Erase()
				}
			}
			// Shared boilerplate for vertical positions (posUp/posDown/posNext):
			// compute pheight, apply the threshold alternative, honor hidden,
			// and update listStickToRight. Returns (pheight, true) when the
			// caller should return early.
			computeVerticalSize := func() (int, bool) {
				pheight, minPreviewHeight := computePreviewSize(previewOpts)
				if hasThreshold && pheight < previewOpts.threshold {
					t.activePreviewOpts = previewOpts.alternative
					if forcePreview {
						previewOpts.alternative.hidden = false
					}
					if !previewOpts.alternative.hidden {
						resizePreviewWindows(previewOpts.alternative)
					}
					return 0, true
				}
				if forcePreview {
					previewOpts.hidden = false
				}
				if previewOpts.hidden {
					return 0, true
				}
				listStickToRight = listStickToRight && !previewOpts.Border(t.layout).HasRight()
				if listStickToRight {
					innerWidth++
					width++
				}
				return util.Constrain(pheight, minPreviewHeight, availableLines), false
			}
			switch previewOpts.position {
			case posUp, posDown:
				pheight, done := computeVerticalSize()
				if done {
					return
				}

				if previewOpts.position == posUp {
					innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
					createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
				} else {
					innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
					createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
				}
			case posNext:
				pheight, done := computeVerticalSize()
				if done {
					return
				}
				previewNextSize = pheight

				if t.layout == layoutReverse {
					// [(header)][input][preview]([header])[list]: reuse posUp's
					// wborder/list math; input is pulled back up by pheight in
					// its positioning. Preview sits directly below input.
					innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+pheight+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
					createPreviewWindow(inputBorderTop()+inputBorderHeight, marginInt[3], width, pheight)
				} else {
					// [list]([header])[preview][input][(header)]: reuse posDown's
					// wborder/list math; input is pushed down by pheight in its
					// positioning. Preview sits directly above input.
					innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth, innerHeight-pheight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
					createPreviewWindow(inputBorderTop()-pheight, marginInt[3], width, pheight)
				}
			case posLeft, posRight:
				pwidth, _ := computePreviewSize(previewOpts)
				if hasThreshold && pwidth < previewOpts.threshold {
					t.activePreviewOpts = previewOpts.alternative
					if forcePreview {
						previewOpts.alternative.hidden = false
					}
					if !previewOpts.alternative.hidden {
						resizePreviewWindows(previewOpts.alternative)
					}
					return
				}
				if forcePreview {
					previewOpts.hidden = false
				}
				if previewOpts.hidden {
					return
				}
				if previewOpts.position == posLeft {
					// Put scrollbar closer to the right border for consistent look
					if t.borderShape.HasRight() && !hasListBorder {
						innerWidth++
					}
					// Add a 1-column margin between the preview window and the main window
					m := 0
					if !hasListBorder {
						m = 1
					}
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)

					// Clear characters on the margin
					// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1,border-none --footer-border --footer f --header h --header-border
					if !previewOpts.Border(t.layout).HasRight() {
						cleanLeft = append(cleanLeft, -2)
					}
					// fzf --bind 'space:toggle-preview' --preview ':' --preview-window left,1 --footer-border --footer f --header h --header-border
					if !hasListBorder {
						cleanLeft = append(cleanLeft, -1)
					}

					innerBorderFn(marginInt[0], marginInt[3]+pwidth, width-pwidth, height)
					createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
				} else {
					// NOTE: Relaxed condition for the following cases
					//  fzf --preview 'seq 500' --preview-window border-left --border
					//  fzf --preview 'seq 500' --preview-window border-left --border --list-border
					//  fzf --preview 'seq 500' --preview-window border-left --border --input-border
					listStickToRight = t.borderShape.HasRight() && !previewOpts.Border(t.layout).HasRight()
					if listStickToRight {
						innerWidth++
						width++
					}
					innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
					t.window = t.tui.NewWindow(
						innerMarginInt[0]+shift+inlineTopLines, innerMarginInt[3], innerWidth-pwidth, innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
					x := marginInt[3] + width - pwidth
					createPreviewWindow(marginInt[0], x, pwidth, height)
				}
			}
		}
		resizePreviewWindows(&t.previewOpts)

		if t.borderShape.HasRight() && !listStickToRight {
			// Need to clear the extra margin between the borders
			// fzf --preview 'seq 1000' --preview-window border-left --bind space:change-preview-window:border-rounded --border vertical
			// fzf --preview 'seq 1000' --preview-window up,hidden --bind space:toggle-preview --border vertical
			y := 0
			if t.borderShape.HasTop() {
				y++
			}
			maxY := t.border.Height()
			if t.borderShape.HasBottom() {
				maxY--
			}
			for ; y < maxY; y++ {
				t.border.Move(y, t.border.Width()-2)
				t.border.Print(" ")
			}
		}
	} else {
		t.activePreviewOpts = &t.previewOpts
	}

	// Without preview window
	if t.window == nil {
		if listStickToRight {
			// Put scrollbar closer to the right border for consistent look
			innerWidth++
			width++
		}
		innerBorderFn(marginInt[0], marginInt[3], width, height)
		t.window = t.tui.NewWindow(
			innerMarginInt[0]+shift+inlineTopLines,
			innerMarginInt[3],
			innerWidth,
			innerHeight-shrink-inlineTopLines-inlineBottomLines, tui.WindowList, noBorder, true)
	}

	if len(inlineTop)+len(inlineBottom) > 0 && t.wborder != nil {
		t.placeInlineStack(inlineTop, t.window.Top()-inlineTopLines, true)
		t.placeInlineStack(inlineBottom, t.window.Top()+t.window.Height()+inlineBottomLines-1, false)
	}

	if len(t.scrollbar) == 0 {
		for y := 0; y < t.window.Height(); y++ {
			t.window.Move(y, t.window.Width()-1)
			t.window.Print(" ")
		}
	}

	createInnerWindow := func(b tui.Window, shape tui.BorderShape, windowType tui.WindowType, shift int) tui.Window {
		top := b.Top()
		left := b.Left() + shift
		if shape.HasTop() {
			top++
		}
		if shape.HasLeft() {
			left += t.borderWidth + 1
		}
		width := b.Width() - borderColumns(shape, t.borderWidth) - shift
		if shape.HasRight() {
			width++
		}
		// Make sure that the width does not exceed the list width
		width = min(t.window.Width()+t.headerIndentImpl(0, shape), width)
		height := b.Height() - borderLines(shape)
		return t.tui.NewWindow(top, left, width, height, windowType, noBorder, true)
	}

	// Set up input border
	w := t.wborder
	if t.wborder == nil {
		w = t.window
	}

	if hasInputWindow {
		btop := inputBorderTop()
		shift := 0
		if !t.inputBorderShape.HasLeft() && t.listBorderShape.HasLeft() {
			shift += t.borderWidth + 1
		}
		t.inputBorder = t.tui.NewWindow(
			btop,
			w.Left(),
			w.Width(),
			inputBorderHeight, tui.WindowInput, tui.MakeBorderStyle(t.inputBorderShape, t.unicode), true)
		if shift > 0 && !t.inputBorderShape.Visible() {
			// Small box on the left to erase the residue
			// e.g.
			//  fzf --list-border --header-border --bind 'space:change-header(hello),enter:change-header()'
			t.tui.NewWindow(btop, w.Left(), shift, inputBorderHeight, tui.WindowInput, noBorder, false).Erase()
		}
		t.inputWindow = createInnerWindow(t.inputBorder, t.inputBorderShape, tui.WindowInput, shift)
	}

	// Set up header border
	if hasHeaderWindow && t.headerBorderShape != tui.BorderInline {
		var btop int
		if hasInputWindow && t.headerFirst {
			if t.layout == layoutReverse {
				btop = w.Top() - shrink + footerBorderHeight - previewNextSize
			} else if t.layout == layoutReverseList {
				btop = w.Top() + w.Height() + inputBorderHeight + previewNextSize
			} else {
				btop = w.Top() + w.Height() + inputBorderHeight + headerLinesHeight + previewNextSize
			}
		} else {
			if t.layout == layoutReverse {
				btop = w.Top() - headerBorderHeight - headerLinesHeight
			} else if t.layout == layoutReverseList {
				btop = w.Top() + w.Height()
			} else {
				btop = w.Top() + w.Height() + headerLinesHeight
			}
		}
		t.headerBorder = t.tui.NewWindow(
			btop,
			w.Left(),
			w.Width(),
			headerBorderHeight, tui.WindowHeader, tui.MakeBorderStyle(t.headerBorderShape, t.unicode), true)
		t.headerWindow = createInnerWindow(t.headerBorder, t.headerBorderShape, tui.WindowHeader, 0)
	}

	// Set up header lines border
	if hasHeaderLinesWindow && headerLinesShape != tui.BorderInline {
		var btop int
		// NOTE: We still have to handle --header-first here in case
		// --header-lines-border is set. Can't we just use header window instead
		// with the style? So we can display header label.
		//   fzf --header-lines 3 --header-label hello --header-border
		//   fzf --header-lines 3 --header-label hello --header-lines-border
		headerFirst := t.headerFirst && len(t.header0) == 0

		if headerFirst {
			if t.layout == layoutDefault {
				btop = w.Top() + w.Height() + inputBorderHeight + previewNextSize
			} else if t.layout == layoutReverse {
				btop = w.Top() - headerLinesHeight - inputBorderHeight
			} else {
				btop = w.Top() - headerLinesHeight
			}
		} else {
			if t.layout != layoutDefault {
				btop = w.Top() - headerLinesHeight
			} else {
				btop = w.Top() + w.Height()
			}
		}
		t.headerLinesBorder = t.tui.NewWindow(
			btop,
			w.Left(),
			w.Width(),
			headerLinesHeight, tui.WindowHeader, tui.MakeBorderStyle(headerLinesShape, t.unicode), true)
		t.headerLinesWindow = createInnerWindow(t.headerLinesBorder, headerLinesShape, tui.WindowHeader, 0)
	}

	// Set up footer
	if hasFooterWindow && t.footerBorderShape != tui.BorderInline {
		var btop int
		if t.layout == layoutReverse {
			btop = w.Top() + w.Height()
		} else if t.layout == layoutReverseList {
			btop = w.Top() - footerBorderHeight - headerLinesHeight
		} else {
			btop = w.Top() - footerBorderHeight
		}
		t.footerBorder = t.tui.NewWindow(
			btop,
			w.Left(),
			w.Width(),
			footerBorderHeight, tui.WindowFooter, tui.MakeBorderStyle(t.footerBorderShape, t.unicode), true)
		t.footerWindow = createInnerWindow(t.footerBorder, t.footerBorderShape, tui.WindowFooter, 0)
	}

	for _, w := range []tui.Window{t.window, t.headerBorder, t.headerLinesBorder, t.footerBorder, t.inputBorder} {
		if w == nil {
			continue
		}

		for y := 0; y < w.Height(); y++ {
			for _, left := range cleanLeft {
				w.Move(y, left)
				w.Print(" ")
			}
		}
	}

	// When the list label lands on an edge owned by an inline section, swap its bg
	// so the label reads as part of that section's frame. Fg stays at list-label.
	listLabel, listLabelLen := t.listLabel, t.listLabelLen
	var adjacentSection *inlineSlot
	if t.listLabelOpts.bottom && len(inlineBottom) > 0 {
		adjacentSection = &inlineBottom[0]
	} else if !t.listLabelOpts.bottom && len(inlineTop) > 0 {
		adjacentSection = &inlineTop[0]
	}
	if adjacentSection != nil {
		bg := tui.BorderColor(adjacentSection.windowType).Bg()
		custom := tui.ColListLabel.WithBg(tui.ColorAttr{Color: bg, Attr: tui.AttrUndefined})
		listLabel, listLabelLen = t.ansiLabelPrinter(t.listLabelOpts.label, &custom, false)
	}
	t.printLabel(t.wborder, listLabel, t.listLabelOpts, listLabelLen, t.listBorderShape, false)
	t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
	t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(t.layout), false)
	t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, false)
	t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, false)
	t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, false)
}

// printLabelAt positions and renders a label at the given row of `window`. Shared by
// printLabel (which computes row from the border shape) and the inline-section label
// code (which uses an explicit separator row).
func (t *Terminal) printLabelAt(window tui.Window, render labelPrinter, opts labelOpts, length int, row int) {
	if window == nil || render == nil || window.Height() == 0 {
		return
	}
	var col int
	if opts.column == 0 {
		col = max(0, (window.Width()-length)/2)
	} else if opts.column < 0 {
		col = max(0, window.Width()+opts.column+1-length)
	} else {
		col = min(opts.column-1, window.Width()-length)
	}
	window.Move(row, col)
	render(window, window.Width())
}

func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
	if window == nil || window.Height() == 0 {
		return
	}
	switch borderShape {
	case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble, tui.BorderDashed:
		if redrawBorder {
			window.DrawHBorder()
		}
		row := 0
		if borderShape == tui.BorderBottom || opts.bottom {
			row = window.Height() - 1
		}
		t.printLabelAt(window, render, opts, length, row)
	}
}

func (t *Terminal) move(y int, x int, clear bool) {
	h := t.window.Height()

	switch t.layout {
	case layoutDefault:
		y = h - y - 1
	case layoutReverseList:
		if !t.inListWindow() && t.window != t.headerLinesWindow {
			// From bottom to top
			y = h - y - 1
		} else {
			/*
			 * List 1
			 * List 2
			 * Header 1
			 * Header 2
			 * Input 2
			 * Input 1
			 */
			i := t.visibleInputLinesInList()
			n := t.visibleHeaderLinesInList()
			if i > 0 && y < i {
				y = h - y - 1
			} else if n > 0 && y < i+n {
				y = h - y - 1
			} else {
				// Top to bottom
				y -= n + i
			}
		}
	}

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

func (t *Terminal) truncateQuery() {
	// We're limiting the length of the query not to make fzf unresponsive when
	// the user accidentally pastes a huge chunk of text. Therefore, we're not
	// interested in the exact display width of the query. We just limit the
	// number of runes.
	t.input = t.input[:min(len(t.input), maxPatternLength)]
	t.cx = util.Constrain(t.cx, 0, len(t.input))
}

func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
	w := t.window
	if t.inputWindow != nil {
		w = t.inputWindow
	}
	maxWidth := max(1, w.Width()-t.promptLen-1)

	_, overflow := t.trimLeft(t.input[:t.cx], maxWidth, 0)
	minOffset := int(overflow)
	maxOffset := minOffset + (maxWidth-max(0, maxWidth-t.cx))/2
	t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
	before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth, 0)
	beforeLen := t.displayWidth(before)
	after, _ := t.trimRight(t.input[t.cx:], maxWidth-beforeLen)
	afterLen := t.displayWidth(after)
	t.queryLen = [2]int{beforeLen, afterLen}
	return before, after
}

func (t *Terminal) placeCursor() {
	if t.inputless {
		return
	}
	x := t.promptLen + t.queryLen[0]
	if t.inputWindow != nil {
		y := t.inputWindow.Height() - 1
		if t.layout == layoutReverse {
			y = 0
		}
		x = min(x, t.inputWindow.Width()-1)
		t.inputWindow.Move(y, x)
		return
	}
	x = min(x, t.window.Width()-1)
	t.move(0, x, false)
}

func (t *Terminal) printPrompt() {
	if t.inputless {
		return
	}
	w := t.window
	if t.inputWindow != nil {
		w = t.inputWindow
	}
	if w.Height() == 0 {
		return
	}
	t.prompt()

	before, after := t.updatePromptOffset()
	if len(before) == 0 && len(after) == 0 && len(t.ghost) > 0 {
		maxWidth := max(1, w.Width()-t.promptLen-1)
		runes, _ := t.trimRight([]rune(t.ghost), maxWidth)
		w.CPrint(tui.ColGhost, string(runes))
		return
	}

	color := tui.ColInput
	if t.paused {
		color = tui.ColDisabled
	} else if t.trackBlocked || t.waitFeedback() {
		color = color.WithAttr(tui.Dim)
	}
	w.CPrint(color, string(before))
	w.CPrint(color, string(after))
}

func (t *Terminal) trimMessage(message string, maxWidth int) string {
	if len(message) <= maxWidth {
		return message
	}
	runes, _ := t.trimRight([]rune(message), maxWidth-2)
	return string(runes) + strings.Repeat(".", util.Constrain(maxWidth, 0, 2))
}

func (t *Terminal) printInfo() {
	if t.inputless {
		return
	}
	t.withWindow(t.inputWindow, func() { t.printInfoImpl() })
}

func (t *Terminal) printInfoImpl() {
	if t.window.Width() <= 1 || t.window.Height() == 0 {
		return
	}
	pos := 0
	line := 0
	separatorLen := t.separatorLength()
	maxHeight := t.window.Height()
	move := func(y int, x int, clear bool) bool {
		if y < 0 || y >= maxHeight {
			return false
		}
		t.move(y, x, clear)
		t.markOtherLine(y)
		return true
	}
	printSpinner := func() {
		if t.reading {
			duration := int64(spinnerDuration)
			idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
			t.window.CPrint(tui.ColSpinner, t.spinner[idx])
		} else {
			t.window.Print(" ") // Clear spinner
		}
	}
	printInfoPrefix := func() {
		str := t.infoPrefix
		maxWidth := t.window.Width() - pos
		width := util.StringWidth(str)
		if width > maxWidth {
			trimmed, _ := t.trimRight([]rune(str), maxWidth)
			str = string(trimmed)
			width = maxWidth
		}
		move(line, pos, separatorLen == 0)
		if t.reading {
			t.window.CPrint(tui.ColSpinner, str)
		} else {
			t.window.CPrint(tui.ColPrompt, str)
		}
		pos += width
	}
	printSeparator := func(fillLength int, pad bool) {
		if separatorLen > 0 {
			t.separator(t.window, fillLength)
			t.window.Print(" ")
		} else if pad {
			t.window.Print(strings.Repeat(" ", fillLength+1))
		}
	}

	if t.infoStyle == infoHidden {
		if separatorLen > 0 {
			if !move(line+1, 0, false) {
				return
			}
			printSeparator(t.window.Width()-1, false)
		}
		return
	}

	found := t.resultMerger.Length()
	total := max(found, t.count)
	output := fmt.Sprintf("%d/%d", found, total)
	if t.multi > 0 {
		if t.multi == maxMulti {
			output += fmt.Sprintf(" (%d)", len(t.selected))
		} else {
			output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
		}
	}
	if t.toggleSort {
		if t.sort {
			output += " +S"
		} else {
			output += " -S"
		}
	}
	if t.track.Global() {
		if t.trackBlocked {
			output += " +T*"
		} else {
			output += " +T"
		}
	} else if t.track.Current() {
		if t.trackBlocked {
			output += " +t*"
		} else {
			output += " +t"
		}
	}
	if t.waitFeedback() {
		output += " (..)"
	}
	// Keep the search progress at the end so the other indicators don't shift
	// as it appears and disappears.
	if t.progress > 0 && t.progress < 100 {
		output += fmt.Sprintf(" (%d%%)", t.progress)
	}
	if t.failed != nil && t.count == 0 {
		output = fmt.Sprintf("[Command failed: %s]", *t.failed)
	}
	var outputPrinter labelPrinter
	outputLen := len(output)
	if t.infoCommand != "" {
		output = t.executeCommand(t.infoCommand, false, true, true, true, output)
		outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false)
	}

	shiftLen := t.queryLen[0] + t.queryLen[1] + 1
	if shiftLen == 1 && len(t.ghost) > 0 {
		shiftLen = util.StringWidth(t.ghost)
	}
	switch t.infoStyle {
	case infoDefault:
		if !move(line+1, 0, separatorLen == 0) {
			return
		}
		printSpinner()
		t.window.Print(" ") // Margin
		pos = 2
	case infoRight:
		if !move(line+1, 0, false) {
			return
		}
	case infoInlineRight:
		pos = t.promptLen + shiftLen
	case infoInline:
		pos = t.promptLen + shiftLen
		printInfoPrefix()
	}

	if t.infoStyle == infoRight {
		maxWidth := t.window.Width() - 1
		if t.reading {
			// Need space for spinner and a margin column
			maxWidth -= 2
		}
		var fillLength int
		if outputPrinter == nil {
			output = t.trimMessage(output, maxWidth)
			fillLength = t.window.Width() - len(output) - 2
		} else {
			fillLength = t.window.Width() - outputLen - 2
		}
		if t.reading {
			if fillLength >= 2 {
				printSeparator(fillLength-2, true)
			}
			printSpinner()
			t.window.Print(" ")
		} else if fillLength >= 0 {
			printSeparator(fillLength, true)
		}
		if outputPrinter == nil {
			t.window.CPrint(tui.ColInfo, output)
		} else {
			outputPrinter(t.window, maxWidth-1)
		}
		if fillLength >= 0 {
			t.window.Print(" ") // Margin
		}
		return
	}

	if t.infoStyle == infoInlineRight {
		if len(t.infoPrefix) == 0 {
			move(line, pos, false)
			newPos := max(pos, t.window.Width()-outputLen-3)
			t.window.Print(strings.Repeat(" ", newPos-pos))
			pos = newPos
			if pos < t.window.Width() {
				printSpinner()
				pos++
			}
			if pos < t.window.Width()-1 {
				t.window.Print(" ")
				pos++
			}
		} else {
			pos = max(pos, t.window.Width()-outputLen-util.StringWidth(t.infoPrefix)-1)
			printInfoPrefix()
		}
	}

	maxWidth := t.window.Width() - pos - 1
	if outputPrinter == nil {
		output = t.trimMessage(output, maxWidth)
		t.window.CPrint(tui.ColInfo, output)
	} else {
		outputPrinter(t.window, maxWidth)
	}
	if t.infoStyle == infoInline && outputLen < maxWidth-1 && t.reading {
		t.window.Print(" ")
		printSpinner()
		outputLen += 2
	}

	if t.infoStyle == infoInlineRight {
		if separatorLen > 0 {
			if !move(line+1, 0, false) {
				return
			}
			printSeparator(t.window.Width()-1, false)
		}
		return
	}

	fillLength := maxWidth - outputLen - 1
	if fillLength > 0 {
		t.window.CPrint(tui.ColSeparator, " ")
		printSeparator(fillLength, false)
	}
}

func (t *Terminal) resizeIfNeeded() bool {
	// Check if input border is used and input has changed
	if t.inputBorderShape.Visible() && t.inputWindow == nil && !t.inputless || t.inputWindow != nil && t.inputless {
		t.printAll()
		return true
	}

	// Inline sections are budget-capped inside wborder, so window.Height() may be
	// smaller than the requested content length. Treat "capped" (height < want) as
	// a no-op to avoid triggering a full redraw on every info/header/footer event
	// when the user has requested more content than fits.
	mismatch := func(shape tui.BorderShape, height, want int) bool {
		if shape == tui.BorderInline && height < want {
			return false
		}
		return height != want
	}

	// Check footer window
	if len(t.footer) > 0 && (t.footerWindow == nil || mismatch(t.footerBorderShape, t.footerWindow.Height(), len(t.footer))) ||
		len(t.footer) == 0 && t.footerWindow != nil {
		t.printAll()
		return true
	}

	// Check if the header borders are used and header has changed
	allHeaderLines := t.visibleHeaderLines()
	primaryHeaderLines := allHeaderLines
	needHeaderWindow := t.hasHeaderWindow()
	needHeaderLinesWindow := t.hasHeaderLinesWindow()
	if needHeaderLinesWindow {
		primaryHeaderLines -= t.headerLines
	}
	if (needHeaderWindow && t.headerWindow == nil) ||
		(!needHeaderWindow && t.headerWindow != nil) ||
		(needHeaderWindow && t.headerWindow != nil && mismatch(t.headerBorderShape, t.headerWindow.Height(), primaryHeaderLines)) ||
		(needHeaderLinesWindow && t.headerLinesWindow == nil) ||
		(!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
		(needHeaderLinesWindow && t.headerLinesWindow != nil && mismatch(t.headerLinesShape, t.headerLinesWindow.Height(), t.headerLines)) {
		t.printAll()
		return true
	}
	return false
}

func (t *Terminal) printHeader() {
	if !t.headerVisible {
		return
	}

	// headerWindow is nil when hasHeaderWindow() returned false at resize time,
	// e.g. --header-border=inline combined with empty header content. Don't
	// delegate to printHeaderImpl because its nil-window branch folds the header
	// into the list window, which isn't valid for inline. A nil window is only
	// legitimate when the shape is NOT inline (e.g. header combined with the
	// list when --no-list-border is in effect).
	if !(t.headerBorderShape == tui.BorderInline && t.headerWindow == nil) {
		t.withWindow(t.headerWindow, func() {
			var headerItems []Item
			if !t.hasHeaderLinesWindow() {
				headerItems = t.header
			}
			t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
		})
	}
	if w, shape := t.determineHeaderLinesShape(); w &&
		!(shape == tui.BorderInline && t.headerLinesWindow == nil) {
		t.withWindow(t.headerLinesWindow, func() {
			t.printHeaderImpl(t.headerLinesWindow, shape, nil, t.header)
		})
	}
}

func (t *Terminal) printFooter() {
	if len(t.footer) == 0 {
		return
	}
	indentSize := t.headerIndent(t.footerBorderShape)
	indent := strings.Repeat(" ", indentSize)
	max := min(len(t.footer), t.footerWindow.Height())

	// Wrapping is not supported for footer
	wrap := t.wrap
	t.wrap = false
	t.withWindow(t.footerWindow, func() {
		var state *ansiState
		for idx, lineStr := range t.footer[:max] {
			line := idx
			if t.layout != layoutReverse {
				line = max - idx - 1
			}
			trimmed, colors, newState := extractColor(lineStr, state, nil)
			state = newState
			item := &Item{
				text:   util.ToChars([]byte(trimmed)),
				colors: colors}

			t.printHighlighted(Result{item: item},
				tui.ColFooter, tui.ColFooter, false, false, false, line, line, true,
				func(markerClass) int {
					t.footerWindow.Print(indent)
					return indentSize
				}, nil, 0)
		}
	})
	t.wrap = wrap
}

func (t *Terminal) headerIndent(borderShape tui.BorderShape) int {
	return t.headerIndentImpl(t.pointerLen+t.markerLen, borderShape)
}

func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
	indentSize := base
	if t.listBorderShape.HasLeft() {
		indentSize += 1 + t.borderWidth
	}
	// Section borders with their own left side skip past the list border's left column.
	// Inline sections also skip it, but only when the list border actually has a left,
	// since otherwise the inline window starts flush with the list window.
	if borderShape.HasLeft() || (borderShape == tui.BorderInline && t.listBorderShape.HasLeft()) {
		indentSize -= 1 + t.borderWidth
		if indentSize < 0 {
			indentSize = 0
		}
	}
	return indentSize
}

func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) {
	max := t.window.Height()
	var state *ansiState
	needReverse := false
	switch t.layout {
	case layoutDefault, layoutReverseList:
		needReverse = true
	}
	// Wrapping is not supported for header
	wrap := t.wrap

	// Align header with the list
	//   fzf --header-lines 3 --style full --no-list-border
	//   fzf --header-lines 3 --style full --no-header-border
	//   fzf --header-lines 3 --style full --no-header-border --no-input-border
	indentSize := t.pointerLen + t.markerLen
	if window != nil {
		indentSize = t.headerIndent(borderShape)
	}
	indent := strings.Repeat(" ", indentSize)
	t.wrap = false
	totalLines := len(lines1) + len(lines2)
	for idx := 0; idx < totalLines; idx++ {
		line := idx
		if needReverse && idx < len(lines1) {
			line = len(lines1) - idx - 1
		}
		if !t.inputless && t.inputWindow == nil && window == nil && !t.headerFirst {
			line++
			if !t.noSeparatorLine() {
				line++
			}
		}
		if line >= max {
			continue
		}

		var item *Item
		if idx < len(lines1) {
			trimmed, colors, newState := extractColor(lines1[idx], state, nil)
			state = newState
			item = &Item{
				text:   util.ToChars([]byte(trimmed)),
				colors: colors}
		} else {
			headerItem := lines2[idx-len(lines1)]
			item = &headerItem
		}

		t.printHighlighted(Result{item: item},
			tui.ColHeader, tui.ColHeader, false, false, false, line, line, true,
			func(markerClass) int {
				t.window.Print(indent)
				return indentSize
			}, nil, 0)
	}
	t.wrap = wrap
}

func (t *Terminal) canSpanMultiLines() bool {
	return (t.multiLine || t.wrap || t.gap > 0) && t.inListWindow()
}

func (t *Terminal) renderBar(line int, barRange [2]int) {
	// If the screen is not filled with the list in non-multi-line mode,
	// scrollbar is not visible at all. But in multi-line mode, we may need
	// to redraw the scrollbar character at the end.
	if t.canSpanMultiLines() {
		t.prevLines[line].hasBar = t.printBar(line, true, barRange)
	}
}

func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
	t.move(line, 0, true)
	t.markEmptyLine(line)
	t.renderBar(line, barRange)
}

func (t *Terminal) gutter(current bool, alt bool) {
	var color tui.ColorPair
	if current {
		color = tui.ColCurrentPointerEmpty
	} else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse {
		if alt {
			color = tui.ColAltPointerEmpty
		} else {
			color = tui.ColPointerEmpty
		}
	} else {
		if alt {
			color = tui.ColAltPointerEmptyChar
		} else {
			color = tui.ColPointerEmptyChar
		}
	}
	gutter := t.pointerEmpty
	if t.raw {
		gutter = t.pointerEmptyRaw
	}
	t.window.CPrint(color, gutter)
}

func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) {
	t.move(line, 0, false)
	t.gutter(false, false)
	t.window.Print(t.markerEmpty)
	x := t.pointerLen + t.markerLen

	width := t.window.Width() - x - 1
	if drawLine && t.gapLine != nil {
		t.gapLine(t.window, width)
	} else {
		t.move(line, x, true)
	}
	t.markOtherLine(line)
	t.renderBar(line, barRange)
	t.prevLines[line].width = width
}

func (t *Terminal) printList() {
	t.constrain()
	barLength, barStart := t.getScrollbar()

	maxy := t.maxItems() - 1
	count := t.merger.Length() - t.offset

	// Start line
	startLine := t.promptLines() + t.visibleHeaderLinesInList()
	maxy += startLine

	barRange := [2]int{startLine + barStart, startLine + barStart + barLength}
	for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 {
		if itemCount < count {
			item := t.merger.Get(itemCount + t.offset)
			current := itemCount == t.cy-t.offset
			line = t.printItem(item, line, maxy, itemCount, current, barRange)
		} else if !t.prevLines[line].empty {
			t.renderEmptyLine(line, barRange)
		}
	}
}

func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool {
	hasBar := lineNum >= barRange[0] && lineNum < barRange[1]
	if (hasBar != t.prevLines[lineNum].hasBar || forceRedraw) && t.window.Width() > 0 {
		if len(t.scrollbar) > 0 {
			t.move(lineNum, t.window.Width()-1, true)
			if hasBar {
				t.window.CPrint(tui.ColScrollbar, t.scrollbar)
			}
		}
	}
	return hasBar
}

func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int {
	item := result.item
	matched := true
	var matchResult Result
	if t.raw {
		if matchResult, matched = t.matchMap[item.Index()]; matched {
			result = matchResult
		}
	}

	_, selected := t.selected[item.Index()]
	label := ""
	extraWidth := 0
	alt := false
	altBg := t.theme.AltBg
	selectedBg := selected && t.theme.SelectedBg != t.theme.ListBg
	if t.jumping != jumpDisabled {
		if index < len(t.jumpLabels) {
			// Striped
			if !altBg.IsColorDefined() {
				altBg = t.theme.DarkBg
				alt = index%2 == 0
			} else {
				alt = index%2 == 1
			}
			label = t.jumpLabels[index:index+1] + strings.Repeat(" ", max(0, t.pointerLen-1))
			if t.pointerLen == 0 {
				extraWidth = 1
			}
		}
	} else {
		if current {
			label = t.pointer
		}
		alt = !selectedBg && altBg.IsColorDefined() && index%2 == 1
	}

	// Avoid unnecessary redraw
	numLines, _ := t.numItemLines(item, maxLine-line+1)
	newLine := itemLine{valid: true, firstLine: line, numLines: numLines, cy: index + t.offset, current: current, selected: selected, label: label,
		result: result, queryLen: len(t.input), width: 0, hasBar: line >= barRange[0] && line < barRange[1], hidden: !matched}
	prevLine := t.prevLines[line]
	forceRedraw := !prevLine.valid || prevLine.other || prevLine.firstLine != newLine.firstLine
	printBar := func(lineNum int, forceRedraw bool) bool {
		return t.printBar(lineNum, forceRedraw, barRange)
	}

	if !forceRedraw &&
		prevLine.hidden == newLine.hidden &&
		prevLine.numLines == newLine.numLines &&
		prevLine.current == newLine.current &&
		prevLine.selected == newLine.selected &&
		prevLine.label == newLine.label &&
		prevLine.queryLen == newLine.queryLen &&
		prevLine.result == newLine.result {
		t.prevLines[line].hasBar = printBar(line, false)
		return line + numLines - 1
	}

	maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + t.barCol())
	postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool, lbg tui.ColorPair) {
		width += extraWidth
		if (current || selected || alt) && t.highlightLine || lbg.IsFullBgMarker() {
			color := tui.ColSelected
			if lbg.IsFullBgMarker() {
				color = lbg
			} else if current {
				color = tui.ColCurrent
			} else if alt {
				color = color.WithBg(altBg)
			}
			fillSpaces := maxWidth - width
			if wrapped {
				fillSpaces -= t.wrapSignWidth
			}
			if fillSpaces > 0 {
				t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
			}
			newLine.width = maxWidth
		} else {
			var fillSpaces int
			if forceRedraw {
				fillSpaces = maxWidth - width
			} else {
				fillSpaces = t.prevLines[lineNum].width - width
			}
			if wrapped {
				fillSpaces -= t.wrapSignWidth
			}
			if fillSpaces > 0 {
				t.window.Print(strings.Repeat(" ", fillSpaces))
			}
			newLine.width = width
			if wrapped {
				newLine.width += t.wrapSignWidth
			}
		}
		// When width is 0, line is completely cleared. We need to redraw scrollbar
		newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
		t.prevLines[lineNum] = newLine
	}

	var finalLineNum int
	markerFor := func(markerClass markerClass) string {
		marker := t.marker
		switch markerClass {
		case markerTop:
			marker = t.markerMultiLine[0]
		case markerMiddle:
			marker = t.markerMultiLine[1]
		case markerBottom:
			marker = t.markerMultiLine[2]
		}
		return marker
	}
	indentSize := t.pointerLen + t.markerLen
	if current {
		preTask := func(marker markerClass) int {
			w := t.window.Width() - t.pointerLen
			if w < 0 {
				return indentSize
			}
			if len(label) == 0 {
				t.gutter(true, false)
			} else {
				t.window.CPrint(tui.ColCurrentPointer, label)
			}
			if w-t.markerLen < 0 {
				return indentSize
			}
			if selected {
				t.window.CPrint(tui.ColCurrentMarker, markerFor(marker))
			} else {
				t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
			}
			return indentSize
		}
		colCurrent := tui.ColCurrent
		nthOverlay := t.theme.NthCurrentAttr
		if selected {
			nthOverlay = t.theme.NthSelectedAttr.Merge(t.theme.NthCurrentAttr)
			baseAttr := tui.ColNormal.Attr().Merge(t.theme.NthSelectedAttr).Merge(t.theme.NthCurrentAttr)
			colCurrent = colCurrent.WithNewAttr(baseAttr)
		}
		finalLineNum = t.printHighlighted(result, colCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
	} else {
		preTask := func(marker markerClass) int {
			w := t.window.Width() - t.pointerLen
			if w < 0 {
				return indentSize
			}
			if len(label) == 0 {
				t.gutter(false, index%2 == 1)
			} else {
				t.window.CPrint(tui.ColPointer, label)
			}
			if w-t.markerLen < 0 {
				return indentSize
			}
			if selected {
				t.window.CPrint(tui.ColMarker, markerFor(marker))
			} else {
				t.window.Print(t.markerEmpty)
			}
			return indentSize
		}
		var base, match tui.ColorPair
		if selected {
			base = tui.ColSelected
			match = tui.ColSelectedMatch
		} else {
			base = tui.ColNormal
			match = tui.ColMatch
		}
		if alt {
			base = base.WithBg(altBg)
			match = match.WithBg(altBg)
		}
		var nthOverlay tui.Attr
		if selected {
			nthOverlay = t.theme.NthSelectedAttr
		}
		finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
	}
	for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
		finalLineNum++
		t.renderGapLine(finalLineNum, barRange, i == t.gap-1)
	}
	return finalLineNum
}

func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
	// We start from the beginning to handle tab characters
	_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
	if overflowIdx >= 0 {
		return runes[:overflowIdx], true
	}
	return runes, false
}

func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
	width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
	return width
}

func (t *Terminal) trimLeft(runes []rune, width int, ellipsisWidth int) ([]rune, int32) {
	width = max(0, width)
	var trimmed int32

	str := string(runes)
	runningSum := 0
	runningSumAdjusted := 0
	// We can't just subtract the width on each segment because there might be
	// a tab character afterwards. For example, with the tabstop = 8:
	//   1234____5678
	//   234_____5678
	//   34______5678
	//   4_______5678
	//   ________5678
	//   5678
	//   678
	//   78
	//   8
	// We need to look ahead, but not to the end to avoid performance hit.
	type queuedSegment struct {
		rs []rune
		w  int
	}
	allQueue := []queuedSegment{}
	queuedWidth := 0
	limit := width - ellipsisWidth
	processQueue := func() {
		for idx, item := range allQueue {
			if runningSumAdjusted <= limit {
				allQueue = allQueue[idx:]
				return
			}
			runningSumAdjusted -= item.w
			runes = runes[len(item.rs):]
			trimmed += int32(len(item.rs))
		}
		allQueue = []queuedSegment{}
	}

	gr := uniseg.NewGraphemes(str)
	queue := []queuedSegment{}
	for gr.Next() {
		s := gr.Str()
		rs := gr.Runes()

		var w int
		if len(rs) == 1 && rs[0] == '\t' {
			w = t.tabstop - runningSum%t.tabstop
		} else {
			w = util.StringWidth(string(rs))
		}
		runningSum += w
		runningSumAdjusted += w
		queue = append(queue, queuedSegment{rs: rs, w: w})
		queuedWidth += w
		if queuedWidth >= t.tabstop || s == "\t" {
			queuedWidth = 0

			if s == "\t" {
				queue[len(queue)-1].w = t.tabstop
				for idx := range queue[:len(queue)-1] {
					queue[idx].w = 0
				}
			}
			allQueue = append(allQueue, queue...)
			queue = []queuedSegment{}
			processQueue()
		}
	}
	allQueue = append(allQueue, queue...)
	processQueue()
	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, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, hidden bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair), nthOverlay tui.Attr) int {
	var displayWidth int
	item := result.item
	matchOffsets := []Offset{}
	var pos *[]int
	if match && t.resultMerger.pattern != nil {
		_, matchOffsets, pos = t.resultMerger.pattern.MatchItem(item, true, t.slab)
	}
	charOffsets := matchOffsets
	if pos != nil {
		runes := item.text.ToRunes()
		charOffsets = make([]Offset, len(*pos))
		for idx, p := range *pos {
			gr := uniseg.NewGraphemes(string(runes[p:]))
			w := 1
			for gr.Next() {
				w = len(gr.Runes())
				break
			}
			offset := Offset{int32(p), int32(p + w)}
			charOffsets[idx] = offset
		}
		slices.SortFunc(charOffsets, compareOffsets)
	}

	// When postTask is nil, we're printing header lines. No need to care about nth.
	var nthOffsets []Offset
	if postTask != nil {
		wholeCovered := len(t.nthCurrent) == 0
		for _, nth := range t.nthCurrent {
			// Do we still want to apply a different style when the current nth
			// covers the whole string? Probably not. And we can simplify the logic.
			if nth.IsFull() {
				wholeCovered = true
				break
			}
		}
		if wholeCovered && t.nthAttr&tui.AttrRegular > 0 {
			// But if 'nth' is set to 'regular', it's a sign that you're applying
			// a different style to the rest of the string. e.g. 'nth:regular,fg:dim'
			// In this case, we still need to apply it to clear the style.
			fgAttr := tui.ColNormal.Attr()
			nthAttrFinal := fgAttr.Merge(t.nthAttr).Merge(nthOverlay)
			colBase = colBase.WithNewAttr(nthAttrFinal)
		}
		if !wholeCovered && t.nthAttr > 0 {
			var tokens []Token
			if item.transformed != nil && item.transformed.revision == t.resultMerger.revision {
				tokens = item.transformed.tokens
			} else {
				tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
			}
			nthOffsets = make([]Offset, len(tokens))
			for i, token := range tokens {
				start := token.prefixLength
				length := token.text.Length() - token.text.TrailingWhitespaces()
				end := start + int32(length)
				nthOffsets[i] = Offset{int32(start), int32(end)}
			}
			slices.SortFunc(nthOffsets, compareOffsets)
		}
	}
	allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)

	// Determine split offset for horizontal scrolling with freeze
	splitOffset1 := -1
	splitOffset2 := -1
	if t.hscroll && !t.wrap {
		var tokens []Token
		if t.freezeLeft > 0 || t.freezeRight > 0 {
			tokens = Tokenize(item.text.ToString(), t.delimiter)
		}

		// 0 | 1 | 2 | 3 | 4 | 5
		// ------>       <------
		if t.freezeLeft > 0 {
			if len(tokens) > 0 {
				token := tokens[min(t.freezeLeft, len(tokens))-1]
				splitOffset1 = int(token.prefixLength) + token.text.Length() - token.text.TrailingWhitespaces()
			}
		}
		if t.freezeRight > 0 {
			index := max(t.freezeLeft-1, len(tokens)-t.freezeRight-1)
			if index < 0 {
				splitOffset2 = 0
			} else if index >= t.freezeLeft {
				token := tokens[index]
				delimiter := strings.TrimLeftFunc(GetLastDelimiter(token.text.ToString(), t.delimiter), unicode.IsSpace)
				splitOffset2 = int(token.prefixLength) + token.text.Length() - len([]rune(delimiter))
			}
			splitOffset2 = max(splitOffset2, splitOffset1)
		}
	}

	maxLines := 1
	if t.canSpanMultiLines() {
		maxLines = maxLineNum - lineNum + 1
	}
	lines, overflow := t.itemLines(item, maxLines)
	numItemLines := len(lines)

	finalLineNum := lineNum
	topCutoff := false
	skipLines := 0
	wrapped := false
	if t.canSpanMultiLines() {
		// Cut off the upper lines in the 'default' layout
		if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
			lines, _ = t.itemLines(item, math.MaxInt)

			// To see if the first visible line is wrapped, we need to check the last cut-off line
			prevLine := lines[len(lines)-maxLines-1]
			if len(prevLine) == 0 || prevLine[len(prevLine)-1] != '\n' {
				wrapped = true
			}

			skipLines = len(lines) - maxLines
			topCutoff = true
		}
	}
	from := 0
	for lineOffset := 0; lineOffset < len(lines) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
		line := lines[lineOffset]
		finalLineNum = lineNum
		offsets := []colorOffset{}
		lbg := tui.NoColorPair()
		for idx, offset := range allOffsets {
			lineEnd := int32(from + len(line))
			if offset.offset[0] >= lineEnd {
				if offset.IsFullBgMarker(lineEnd) {
					lbg = offset.color
				}
				allOffsets = allOffsets[idx:]
				break
			}

			if offset.offset[0] < int32(from) {
				continue
			}

			if offset.offset[1] < lineEnd {
				offset.offset[0] -= int32(from)
				offset.offset[1] -= int32(from)
				offsets = append(offsets, offset)
			} else {
				if idx < len(allOffsets)-1 {
					next := allOffsets[idx+1]
					if next.IsFullBgMarker(lineEnd) {
						lbg = next.color
						idx++
					}
				}
				dupe := offset
				dupe.offset[0] = lineEnd

				offset.offset[0] -= int32(from)
				offset.offset[1] = lineEnd
				offsets = append(offsets, offset)

				allOffsets = append([]colorOffset{dupe}, allOffsets[idx+1:]...)
				break
			}
		}
		splitOffsetLeft := 0
		if splitOffset1 >= 0 && splitOffset1 > from && splitOffset1 < from+len(line) {
			splitOffsetLeft = splitOffset1 - from
		}
		splitOffsetRight := -1
		if splitOffset2 >= 0 && splitOffset2 >= from && splitOffset2 < from+len(line) {
			splitOffsetRight = splitOffset2 - from
		}
		from += len(line)
		if lineOffset < skipLines {
			continue
		}
		actualLineOffset := lineOffset - skipLines

		var maxEnd int
		for _, offset := range offsets {
			if offset.match {
				maxEnd = max(maxEnd, int(offset.offset[1]))
			}
		}

		actualLineNum := lineNum
		if t.layout == layoutDefault {
			actualLineNum = (lineNum - actualLineOffset) + (numItemLines - actualLineOffset) - 1
		}
		t.move(actualLineNum, 0, forceRedraw && postTask == nil)

		indentSize := t.pointerLen + t.markerLen
		if preTask != nil {
			var marker markerClass
			if numItemLines == 1 {
				if !overflow {
					marker = markerSingle
				} else if topCutoff {
					marker = markerBottom
				} else {
					marker = markerTop
				}
			} else {
				if actualLineOffset == 0 { // First line
					if topCutoff {
						marker = markerMiddle
					} else {
						marker = markerTop
					}
				} else if actualLineOffset == numItemLines-1 { // Last line
					if topCutoff || !overflow {
						marker = markerBottom
					} else {
						marker = markerMiddle
					}
				} else {
					marker = markerMiddle
				}
			}

			indentSize = preTask(marker)
		}

		maxWidth := t.window.Width() - (indentSize + t.barCol())
		wasWrapped := false
		if wrapped {
			wrapSign := t.wrapSign
			if maxWidth < t.wrapSignWidth {
				runes, _ := util.Truncate(wrapSign, maxWidth)
				wrapSign = string(runes)
				maxWidth = 0
			} else {
				maxWidth -= t.wrapSignWidth
			}
			t.window.CPrint(colBase.WithAttr(tui.Dim), wrapSign)
			wrapped = false
			wasWrapped = true
		}

		if len(line) > 0 && line[len(line)-1] == '\n' && lineOffset < len(lines)-1 {
			line = line[:len(line)-1]
		} else {
			wrapped = true
		}

		frozenLeft := line[:splitOffsetLeft]
		middle := line[splitOffsetLeft:]
		frozenRight := []rune{}
		if splitOffsetRight >= splitOffsetLeft {
			middle = line[splitOffsetLeft:splitOffsetRight]
			frozenRight = line[splitOffsetRight:]
		}
		displayWidthSum := 0
		displayWidthLeft := 0
		todo := [3]func(){}
		for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} {
			if len(runes) == 0 {
				continue
			}
			shift := 0
			maxe := maxEnd
			offs := make([]colorOffset, len(offsets))
			for idx := range offsets {
				offs[idx] = offsets[idx]
				if fidx == 1 && splitOffsetRight > 0 {
					shift = splitOffsetRight
				} else if fidx == 2 && splitOffsetLeft > 0 {
					shift = splitOffsetLeft
				}
				offs[idx].offset[0] -= int32(shift)
				offs[idx].offset[1] -= int32(shift)
			}
			maxe -= shift
			ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth)
			adjustedMaxWidth := maxWidth
			if fidx < 2 {
				// For frozen parts, reserve space for the ellipsis in the middle part
				adjustedMaxWidth -= ellipsisWidth
			}
			var prefixWidth int
			if fidx == 2 {
				prefixWidth = displayWidthLeft
			}
			displayWidth = t.displayWidthWithLimit(runes, prefixWidth, adjustedMaxWidth)
			if !t.wrap && displayWidth > adjustedMaxWidth {
				maxe = util.Constrain(maxe+min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
				transformOffsets := func(diff int32) {
					for idx := range offs {
						offs[idx].offset[0] -= diff
						offs[idx].offset[1] -= diff
					}
				}
				if t.hscroll {
					if fidx == 1 || fidx == 2 && t.keepRight && pos == nil {
						trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth)
						transformOffsets(diff - int32(len(ellipsis)))
						runes = append(ellipsis, trimmed...)
					} else if fidx == 0 || !t.overflow(runes[:maxe], maxWidth-ellipsisWidth) {
						// Stri..
						runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
						runes = append(runes, ellipsis...)
					} else {
						// Stri..
						if t.overflow(runes[maxe:], ellipsisWidth) {
							runes = append(runes[:maxe], ellipsis...)
						}
						// ..ri..
						var diff int32
						runes, diff = t.trimLeft(runes, maxWidth, ellipsisWidth)

						// Transform offsets
						transformOffsets(diff - int32(len(ellipsis)))
						runes = append(ellipsis, runes...)
					}
				} else {
					runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
					runes = append(runes, ellipsis...)
				}
				displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
			}
			displayWidthSum += displayWidth
			if fidx == 0 {
				displayWidthLeft = displayWidth
			}

			if maxWidth > 0 {
				color := colBase
				if hidden {
					color = color.WithFg(t.theme.Nomatch)
				}
				todo[fidx] = func() {
					t.printColoredString(t.window, runes, offs, color, prefixWidth)
				}
			} else {
				break
			}
			maxWidth -= displayWidth
		}
		if todo[0] != nil {
			todo[0]()
		}
		if todo[2] != nil {
			todo[2]()
		}
		if todo[1] != nil {
			todo[1]()
		}
		if postTask != nil {
			postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg)
		} else {
			t.markOtherLine(actualLineNum)
		}
		lineNum += 1
	}

	return finalLineNum
}

func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair, initialPrefixWidth ...int) {
	var index int32
	var substr string
	var prefixWidth int
	if len(initialPrefixWidth) > 0 {
		prefixWidth = initialPrefixWidth[0]
	}
	maxOffset := int32(len(text))
	var url *url
	for _, offset := range offsets {
		b := util.Constrain(offset.offset[0], index, maxOffset)
		e := util.Constrain(offset.offset[1], index, maxOffset)
		if url != nil && offset.url != url {
			url = nil
			window.LinkEnd()
		}

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

		if b < e {
			substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
			if url == nil && offset.url != nil {
				url = offset.url
				window.LinkBegin(url.uri, url.params)
			}
			window.CPrint(offset.color, substr)
		}

		index = e
		if index >= maxOffset {
			break
		}
	}
	if url != nil {
		window.LinkEnd()
	}
	if index < maxOffset {
		substr, _ = t.processTabs(text[index:], prefixWidth)
		window.CPrint(colBase, substr)
	}
}

func (t *Terminal) renderPreviewSpinner() {
	numLines := len(t.previewer.lines)
	spin := t.previewer.spinner
	if len(spin) > 0 || t.previewer.scrollable {
		maxWidth := t.pwindow.Width()
		if !t.previewer.scrollable || !t.activePreviewOpts.info {
			if maxWidth > 0 {
				t.pwindow.Move(0, maxWidth-1)
				t.pwindow.CPrint(tui.ColPreviewSpinner, spin)
			}
		} else {
			offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines)
			if len(spin) > 0 {
				spin += " "
				maxWidth -= 2
			}
			offsetRunes, _ := t.trimRight([]rune(offsetString), maxWidth)
			pos := maxWidth - t.displayWidth(offsetRunes)
			t.pwindow.Move(0, pos)
			if maxWidth > 0 {
				t.pwindow.CPrint(tui.ColPreviewSpinner, spin)
				t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), string(offsetRunes))
			}
		}
	}
}

func (t *Terminal) renderPreviewArea(unchanged bool) {
	if t.previewed.wipe && t.previewed.version != t.previewer.version {
		t.previewed.wipe = false
		t.pwindow.Erase()
	} else if unchanged {
		t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
	} else {
		t.previewed.filled = false
		// We don't erase the window here to avoid flickering during scroll.
		// However, tcell renderer uses double-buffering technique and there's no
		// flickering. So we just erase the window and make the rest of the code
		// simpler.
		if !t.pwindow.EraseMaybe() {
			t.pwindow.DrawBorder()
			t.pwindow.Move(0, 0)
		}
	}

	height := t.pwindow.Height()
	body := t.previewer.lines
	headerLines := t.activePreviewOpts.headerLines
	lineNo := -t.previewer.offset + headerLines
	// Scrollbar is sized from the body alone, split off or not
	scrollLines := len(body)
	// Do not enable preview header lines if it's value is too large
	if headerLines > 0 && headerLines < min(len(body), height) {
		scrollLines -= headerLines
		header := t.previewer.lines[0:headerLines]
		// A separate header pass would resume the body inside an image, which
		// takes up more rows than the line it arrives on
		if !containsImage(header) {
			// Always redraw header
			t.renderPreviewText(height, header, 0, false)
			t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
			body = t.previewer.lines[headerLines:]
		}
	}
	t.renderPreviewText(height, body, lineNo, unchanged)

	if !unchanged {
		t.pwindow.FinishFill()
	}

	if len(t.scrollbar) == 0 {
		return
	}

	effectiveHeight := height - headerLines
	barLength, barStart := getScrollbar(1, scrollLines, effectiveHeight, min(scrollLines-effectiveHeight, t.previewer.offset-headerLines))
	t.renderPreviewScrollbar(headerLines, barLength, barStart)
}

func (t *Terminal) makeImageBorder(width int, top bool) string {
	tl := "┌"
	tr := "┐"
	v := "╎"
	h := "╌"
	if !t.unicode {
		tl = "+"
		tr = "+"
		h = "-"
		v = "|"
	}
	repeat := max(0, width-2)
	if top {
		return tl + strings.Repeat(h, repeat) + tr
	}
	return v + strings.Repeat(" ", repeat) + v
}

func findPassThrough(line string) []int {
	loc := passThroughBeginRegex.FindStringIndex(line)
	if loc == nil {
		return nil
	}

	rest := line[loc[0]:]
	after := line[loc[1]:]
	if strings.HasPrefix(rest, "\x1bPtmux") { // Tmux
		eloc := passThroughEndTmuxRegex.FindStringIndex(after)
		if eloc == nil {
			return nil
		}
		return []int{loc[0], loc[1] + eloc[1]}
	} else if strings.HasPrefix(rest, "\x1b]1337;") { // iTerm2
		index := loc[1]
		for {
			after := line[index:]
			pos := strings.IndexAny(after, "\x1b\a")
			if pos < 0 {
				return nil
			}
			if after[pos] == '\a' {
				return []int{loc[0], index + pos + 1}
			}
			if pos < len(after)-1 && after[pos+1] == '\\' {
				return []int{loc[0], index + pos + 2}
			}
			index += pos + 1
		}
	}
	// Kitty
	pos := strings.Index(after, "\x1b\\")
	if pos < 0 {
		return nil
	}
	if pos < len(after)-2 && after[pos+2] == '\r' {
		return []int{loc[0], loc[1] + pos + 3}
	}
	return []int{loc[0], loc[1] + pos + 2}
}

// tmux takes a bare APC as a request to set the pane title, so a Kitty
// graphics command never reaches the terminal and clobbers the title on the
// way. 'kitten icat --clear' emits one unwrapped. Sixel is left alone.
// https://github.com/junegunn/fzf/issues/4870
func wrapPassThrough(passThrough string, tmux bool) string {
	if !tmux || !strings.HasPrefix(passThrough, "\x1b_G") {
		return passThrough
	}
	// Only the sequence is passed through, not the trailing CR
	suffix := ""
	if strings.HasSuffix(passThrough, "\r") {
		passThrough, suffix = passThrough[:len(passThrough)-1], "\r"
	}
	return "\x1bPtmux;" + strings.ReplaceAll(passThrough, "\x1b", "\x1b\x1b") + "\x1b\\" + suffix
}

// Whether the sequence draws an image. Kitty commands that only transmit or
// delete do not
func isImagePassThrough(passThrough string) bool {
	// Unwrap the tmux passthrough sequence, in which every ESC is doubled
	if after, ok := strings.CutPrefix(passThrough, "\x1bPtmux;"); ok {
		passThrough = strings.ReplaceAll(after, "\x1b\x1b", "\x1b")
	}
	if after, ok := strings.CutPrefix(passThrough, "\x1b_G"); ok {
		// Control data ends at the payload delimiter or at the terminator
		keys := after
		if index := strings.IndexAny(keys, ";\x1b"); index >= 0 {
			keys = keys[:index]
		}
		for _, key := range strings.Split(keys, ",") {
			// Transmit and display, or put an image already transmitted
			if key == "a=T" || key == "a=p" {
				return true
			}
		}
		return false
	}
	if after, ok := strings.CutPrefix(passThrough, "\x1b]1337;"); ok {
		return strings.HasPrefix(after, "File=") || strings.HasPrefix(after, "MultipartFile=")
	}
	return sixelBeginRegex.MatchString(passThrough)
}

// Whether any line carries an image
func containsImage(lines []string) bool {
	for _, line := range lines {
		for {
			loc := findPassThrough(line)
			if loc == nil {
				break
			}
			if isImagePassThrough(line[loc[0]:loc[1]]) {
				return true
			}
			line = line[loc[1]:]
		}
	}
	return false
}

func extractPassThroughs(line string) ([]string, string) {
	passThroughs := []string{}
	transformed := ""
	index := 0
	for {
		rest := line[index:]
		loc := findPassThrough(rest)
		if loc == nil {
			transformed += rest
			break
		}
		passThroughs = append(passThroughs, rest[loc[0]:loc[1]])
		transformed += line[index : index+loc[0]]
		index += loc[1]
	}

	return passThroughs, transformed
}

// splitOnIND breaks a preview line on IND (ESC D), which moves the cursor
// down one line, keeping the column. A program drawing at a column offset ends
// its rows with IND instead of a newline, because ONLCR would rewrite a newline
// as CR NL and snap the cursor to column 0. chafa does this for Kitty Unicode
// placeholders, and without the break the whole image collapses into a single
// line.
//
// The column is tracked and re-created with padding so that an indented image
// keeps its indent, and a CUB right before the IND is subtracted, which is how
// chafa returns to the column its rows start on.
func (t *Terminal) splitOnIND(line string) []string {
	chunks := strings.Split(line, "\x1bD")
	if len(chunks) == 1 {
		return nil
	}

	lines := make([]string, 0, len(chunks))
	col := 0
	for _, chunk := range chunks[:len(chunks)-1] {
		lines = append(lines, strings.Repeat(" ", col)+chunk+"\n")
		col += t.displayWidthWithoutEscapes(chunk, col)
		if match := cursorBackRegex.FindStringSubmatch(chunk); match != nil {
			back := 1
			if len(match[1]) > 0 {
				back, _ = strconv.Atoi(match[1])
			}
			col = max(0, col-back)
		}
	}
	return append(lines, strings.Repeat(" ", col)+chunks[len(chunks)-1])
}

// followOffset computes the correct content-line offset for follow mode,
// accounting for line wrapping in the preview window.
func (t *Terminal) followOffset() int {
	lines := t.previewer.lines
	headerLines := t.activePreviewOpts.headerLines
	height := t.pwindow.Height() - headerLines
	if height <= 0 || len(lines) <= headerLines {
		return headerLines
	}

	body := lines[headerLines:]
	if !t.activePreviewOpts.wrap {
		return max(t.previewer.offset, headerLines+len(body)-height)
	}

	maxWidth := t.pwindow.Width()
	visualLines := 0
	for i := len(body) - 1; i >= 0; i-- {
		h := t.previewLineHeight(body[i], maxWidth)
		if visualLines+h > height {
			return min(len(lines)-1, headerLines+i+1)
		}
		visualLines += h
	}
	return headerLines
}

// previewLineHeight estimates the number of visual lines a preview content line
// occupies when wrapping is enabled.
func (t *Terminal) previewLineHeight(line string, maxWidth int) int {
	if maxWidth <= 0 {
		return 1
	}

	// For word-wrap mode, count the sub-lines produced by word wrapping.
	// Each sub-line may still char-wrap if it contains a word longer than the width.
	if t.activePreviewOpts.wrapWord {
		subLines := t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
		total := 0
		for i, sub := range subLines {
			prefixWidth := 0
			cols := maxWidth
			if i > 0 {
				prefixWidth = t.previewWrapSignWidth
				cols -= t.previewWrapSignWidth
			}
			w := t.ansiLineWidth(sub, prefixWidth)
			if cols <= 0 {
				cols = 1
			}
			total += max(1, (w+cols-1)/cols)
		}
		return total
	}

	// For char-wrap, compute visible width and divide by available width.
	w := t.ansiLineWidth(line, 0)
	if w <= maxWidth {
		return 1
	}
	remaining := w - maxWidth
	contWidth := max(1, maxWidth-t.previewWrapSignWidth)
	return 1 + (remaining+contWidth-1)/contWidth
}

// ansiLineWidth computes the display width of a string, skipping ANSI escape sequences.
// prefixWidth is the visual offset where the content starts (e.g. wrap sign width for
// continuation lines), used for correct tab stop alignment.
func (t *Terminal) ansiLineWidth(line string, prefixWidth int) int {
	line = strings.TrimSuffix(line, "\n")
	trimmed, _, _ := extractColor(line, nil, nil)
	_, width := t.processTabsStr(trimmed, prefixWidth)
	return width - prefixWidth
}

func (t *Terminal) wordWrapAnsiLine(line string, maxWidth int, wrapSignWidth int) []string {
	if maxWidth <= 0 {
		return []string{line}
	}

	var result []string
	lineStart := 0
	width := 0
	lastSpaceStart := -1
	lastSpaceEnd := -1
	widthBeforeLastSpace := 0
	lastSpaceWidth := 0
	max := maxWidth
	pos := 0

	for pos < len(line) {
		// Find next ANSI escape sequence
		start, end := nextAnsiEscapeSequence(line[pos:])

		// Determine the end of printable text before the next escape
		var printableEnd int
		if start < 0 {
			printableEnd = len(line)
		} else {
			printableEnd = pos + start
		}

		// Process printable characters using grapheme clusters
		gr := uniseg.NewGraphemes(line[pos:printableEnd])
		for gr.Next() {
			gStart, gEnd := gr.Positions()
			w := gr.Width()
			str := gr.Str()

			if str == "\t" {
				w = t.tabstop - (width % t.tabstop)
			}

			if str == " " || str == "\t" {
				lastSpaceStart = pos + gStart
				lastSpaceEnd = pos + gEnd
				widthBeforeLastSpace = width
				lastSpaceWidth = w
			}

			width += w

			if width > max && lastSpaceEnd > lineStart {
				result = append(result, line[lineStart:lastSpaceStart])
				lineStart = lastSpaceEnd
				width -= widthBeforeLastSpace + lastSpaceWidth
				lastSpaceStart = -1
				lastSpaceEnd = -1
				widthBeforeLastSpace = 0
				max = maxWidth - wrapSignWidth
			}
		}
		pos = printableEnd

		// Skip the ANSI escape sequence
		if start >= 0 {
			pos += end - start
		}
	}

	result = append(result, line[lineStart:])
	return result
}

func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
	maxWidth := t.pwindow.Width()
	var ansi *ansiState
	spinnerRedraw := t.pwindow.Y() == 0
	wiped := false
	image := false
	wireframe := false
	var index int
	var line string
Loop:
	for index, line = range lines {
		var lbg tui.Color = -1
		if ansi != nil {
			ansi.lbg = -1
		}

		passThroughs, line := extractPassThroughs(line)
		line = strings.TrimLeft(strings.TrimRight(line, "\r\n"), "\r")

		if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
			t.previewed.filled = true
			t.previewer.scrollable = true
			break
		} else if lineNo >= 0 {
			x := t.pwindow.X()
			y := t.pwindow.Y()
			if spinnerRedraw && lineNo > 0 {
				spinnerRedraw = false
				t.renderPreviewSpinner()
				t.pwindow.Move(y, x)
			}
			for idx, passThrough := range passThroughs {
				// Handling Sixel/iTerm image
				requiredLines := 0
				isSixel := strings.HasPrefix(passThrough, "\x1bP")
				isItermImage := strings.HasPrefix(passThrough, "\x1b]1337;")
				isImage := isSixel || isItermImage
				if isImage {
					t.previewed.wipe = true
					// NOTE: We don't have a good way to get the height of an iTerm image,
					// so we assume that it requires the full height of the preview
					// window.
					requiredLines = height

					if isSixel && t.termSize.PxHeight > 0 {
						rows := strings.Count(passThrough, "-")
						requiredLines = int(math.Ceil(float64(rows*6*t.termSize.Lines) / float64(t.termSize.PxHeight)))
					}
				}

				// Render wireframe when the image cannot be displayed entirely
				if requiredLines > 0 && y+requiredLines > height {
					top := true
					for ; y < height; y++ {
						t.pwindow.MoveAndClear(y, 0)
						t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, tui.AttrRegular, t.makeImageBorder(maxWidth, top))
						top = false
					}
					wireframe = true
					t.previewed.filled = true
					t.previewer.scrollable = true
					break Loop
				}

				// Clear previous wireframe or any other text
				if (t.previewed.wireframe || isImage && !t.previewed.image) && !wiped {
					wiped = true
					for i := y + 1; i < height; i++ {
						t.pwindow.MoveAndClear(i, 0)
					}
				}
				image = image || isImage
				if idx == 0 {
					t.pwindow.MoveAndClear(y, x)
				} else {
					t.pwindow.Move(y, x)
				}
				t.tui.PassThrough(wrapPassThrough(passThrough, inTmux))

				if requiredLines > 0 {
					if y+requiredLines == height {
						t.pwindow.Move(height-1, maxWidth-1)
						t.previewed.filled = true
						break Loop
					}
					t.pwindow.MoveAndClear(y+requiredLines, 0)
				}
			}

			if len(passThroughs) > 0 && len(line) == 0 {
				continue
			}

			// Pre-split line into sub-lines for word wrapping
			var subLines []string
			if t.activePreviewOpts.wrapWord {
				subLines = t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
			} else {
				subLines = []string{line}
			}

			var fillRet tui.FillReturn
			wrap := t.activePreviewOpts.wrap
			printWrapSign := func() {
				if t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, tui.Dim, t.previewWrapSign) == tui.FillNextLine {
					t.pwindow.Move(t.pwindow.Y()-1, t.pwindow.Width())
				}
				fillRet = tui.FillContinue
			}
			for subIdx, subLine := range subLines {
				// Render wrap sign for continuation sub-lines
				if subIdx > 0 {
					if fillRet == tui.FillContinue {
						fillRet = t.pwindow.Fill("\n")
						if fillRet == tui.FillSuspend {
							t.previewed.filled = true
							break Loop
						}
					}
					printWrapSign()
				}

				prefixWidth := t.pwindow.X()
				var url *url
				_, _, ansi = extractColor(subLine, ansi, func(str string, ansi *ansiState) bool {
					if len(str) > 0 && fillRet == tui.FillNextLine {
						printWrapSign()
						prefixWidth = t.pwindow.X()
					}
					trimmed := []rune(str)
					isTrimmed := false
					if !wrap {
						trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
					}
					if url == nil && ansi != nil && ansi.url != nil {
						url = ansi.url
						t.pwindow.LinkBegin(url.uri, url.params)
					}
					if url != nil && (ansi == nil || ansi.url == nil) {
						url = nil
						t.pwindow.LinkEnd()
					}
					if ansi != nil {
						lbg = ansi.lbg
					} else {
						lbg = -1
					}
					str, width := t.processTabs(trimmed, prefixWidth)
					if width > prefixWidth {
						prefixWidth = width
						colored := ansi != nil && ansi.colored()
						if t.theme.Colored && colored {
							fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.ul, ansi.attr, str)
						} else {
							attr := tui.AttrRegular
							if colored {
								attr = ansi.attr
							}
							fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, attr, str)
						}
					}
					return !isTrimmed &&
						(fillRet == tui.FillContinue || wrap && fillRet == tui.FillNextLine)
				})
				if url != nil {
					t.pwindow.LinkEnd()
				}

				if fillRet == tui.FillSuspend {
					t.previewed.filled = true
					break Loop
				}
			}

			t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() || t.previewed.filled
			if fillRet == tui.FillNextLine {
				continue
			} else if fillRet == tui.FillSuspend {
				t.previewed.filled = true
				break
			}
			if unchanged && lineNo == 0 {
				break
			}
			if t.theme.Colored && lbg >= 0 {
				fillRet = t.pwindow.CFill(-1, lbg, -1, tui.AttrRegular,
					strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
			} else {
				fillRet = t.pwindow.Fill("\n")
			}
			if fillRet == tui.FillSuspend {
				t.previewed.filled = true
				break
			}
		}
		lineNo++
	}
	t.previewer.scrollable = t.previewer.scrollable || t.previewed.filled || index < len(lines)-1
	t.previewed.image = image
	t.previewed.wireframe = wireframe
}

func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
	height := t.pwindow.Height()
	w := t.pborder.Width()
	xw := [2]int{t.pwindow.Left(), t.pwindow.Width()}
	redraw := false
	if len(t.previewer.bar) != height || t.previewer.xw != xw || t.previewed.version != t.previewer.version {
		redraw = true
		t.previewer.bar = make([]bool, height)
		t.previewer.xw = xw
	}
	xshift := -1 - t.borderWidth
	if !t.activePreviewOpts.Border(t.layout).HasRight() {
		xshift = -1
	}
	yshift := 1
	if !t.activePreviewOpts.Border(t.layout).HasTop() {
		yshift = 0
	}
	for i := yoff; i < height; i++ {
		x := w + xshift
		y := i + yshift

		// Avoid unnecessary redraws
		bar := i >= yoff+barStart && i < yoff+barStart+barLength
		if !redraw && bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() {
			continue
		}

		t.previewer.bar[i] = bar
		t.pborder.Move(y, x)
		if i >= yoff+barStart && i < yoff+barStart+barLength {
			t.pborder.CPrint(tui.ColPreviewScrollbar, t.previewScrollbar)
		} else {
			t.pborder.CPrint(tui.ColPreviewScrollbar, " ")
		}
	}
}

func (t *Terminal) printPreview() {
	if !t.hasPreviewWindow() || t.pwindow.Height() == 0 {
		return
	}
	numLines := len(t.previewer.lines)
	height := t.pwindow.Height()
	unchanged := (t.previewed.filled || numLines == t.previewed.numLines) &&
		t.previewer.version == t.previewed.version &&
		t.previewer.offset == t.previewed.offset
	t.previewer.scrollable = t.previewer.offset > t.activePreviewOpts.headerLines || numLines > height
	t.renderPreviewArea(unchanged)
	t.renderPreviewSpinner()
	t.previewed.numLines = numLines
	t.previewed.version = t.previewer.version
	t.previewed.offset = t.previewer.offset
}

func (t *Terminal) printPreviewDelayed() {
	if !t.hasPreviewWindow() || len(t.previewer.lines) > 0 && t.previewed.version == t.previewer.version {
		return
	}

	t.previewer.scrollable = false
	t.renderPreviewArea(true)

	message := t.trimMessage("Loading ..", t.pwindow.Width())
	pos := t.pwindow.Width() - len(message)
	t.pwindow.Move(0, pos)
	t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), message)
}

func (t *Terminal) processTabsStr(input string, prefixWidth int) (string, int) {
	var strbuf strings.Builder
	l := prefixWidth
	gr := uniseg.NewGraphemes(input)
	for gr.Next() {
		rs := gr.Runes()
		str := string(rs)
		var w int
		if len(rs) == 1 && rs[0] == '\t' {
			w = t.tabstop - l%t.tabstop
			strbuf.WriteString(strings.Repeat(" ", w))
		} else {
			w = util.StringWidth(str)
			strbuf.WriteString(str)
		}
		l += w
	}
	return strbuf.String(), l
}

func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
	return t.processTabsStr(string(runes), prefixWidth)
}

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

func (t *Terminal) flush() {
	t.placeCursor()
	if !t.suppress {
		windows := make([]tui.Window, 0, 9)
		if t.border != nil {
			windows = append(windows, t.border)
		}
		if t.pborder != nil {
			windows = append(windows, t.pborder)
		}
		if t.pwindow != nil {
			windows = append(windows, t.pwindow)
		}
		if t.wborder != nil {
			windows = append(windows, t.wborder)
		}
		if t.window != nil {
			windows = append(windows, t.window)
		}
		if t.headerBorder != nil {
			windows = append(windows, t.headerBorder)
		}
		if t.headerWindow != nil {
			windows = append(windows, t.headerWindow)
		}
		if t.headerLinesBorder != nil {
			windows = append(windows, t.headerLinesBorder)
		}
		if t.headerLinesWindow != nil {
			windows = append(windows, t.headerLinesWindow)
		}
		if t.inputBorder != nil {
			windows = append(windows, t.inputBorder)
		}
		if t.inputWindow != nil {
			windows = append(windows, t.inputWindow)
		}
		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
	}
	prefix := []rune(str[:locs[len(locs)-1][0]])
	return len(prefix)
}

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
	}
	prefix := []rune(str[:loc[0]])
	return len(prefix)
}

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 tui.Event, event tui.Event) bool {
	return event.Type == key.Type && event.Char == key.Char ||
		key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
}

func parsePlaceholder(match string) (bool, string, placeholderFlags) {
	flags := placeholderFlags{}

	if match[0] == '\\' {
		// Escaped placeholder pattern
		return true, match[1:], flags
	}

	if strings.HasPrefix(match, "{fzf:") {
		// {fzf:*} are not determined by the current item
		flags.forceUpdate = true
		return false, match, flags
	}

	trimmed := ""
	for _, char := range match[1:] {
		switch char {
		case '*':
			flags.asterisk = true
		case '+':
			flags.plus = true
		case 's':
			flags.preserveSpace = true
		case 'n':
			flags.number = true
		case 'f':
			flags.file = true
		case 'r':
			flags.raw = true
		case 'q':
			flags.forceUpdate = true
			trimmed += string(char)
		default:
			trimmed += string(char)
		}
	}

	matchWithoutFlags := "{" + trimmed

	return false, matchWithoutFlags, flags
}

func hasPreviewFlags(template string) (slot bool, plus bool, asterisk bool, forceUpdate bool) {
	for _, match := range placeholder.FindAllString(template, -1) {
		escaped, _, flags := parsePlaceholder(match)
		if escaped {
			continue
		}
		slot = true
		plus = plus || flags.plus
		asterisk = asterisk || flags.asterisk
		forceUpdate = forceUpdate || flags.forceUpdate
	}
	return
}

type replacePlaceholderParams struct {
	template   string
	stripAnsi  bool
	delimiter  Delimiter
	printsep   string
	forcePlus  bool
	query      string
	allItems   [3][]*Item // current, select, and all matched items
	lastAction actionType
	prompt     string
	executor   *util.Executor
}

func (t *Terminal) replacePlaceholderInInitialCommand(template string) (string, []string) {
	return t.replacePlaceholder(template, false, string(t.input), [3][]*Item{nil, nil, nil})
}

func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list [3][]*Item) (string, []string) {
	return replacePlaceholder(replacePlaceholderParams{
		template:   template,
		stripAnsi:  t.ansi,
		delimiter:  t.delimiter,
		printsep:   t.printsep,
		forcePlus:  forcePlus,
		query:      input,
		allItems:   list,
		lastAction: t.lastAction,
		prompt:     t.promptString,
		executor:   t.executor,
	})
}

func (t *Terminal) evaluateScrollOffset() int {
	if t.pwindow == nil {
		return 0
	}

	// We only need the current item to calculate the scroll offset
	current := []*Item{t.currentItem()}
	if current[0] == nil {
		current = nil
	}
	replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", [3][]*Item{current, nil, nil})
	removeFiles(tempFiles)
	offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "")

	atoi := func(s string) int {
		n, e := strconv.Atoi(s)
		if e != nil {
			return 0
		}
		return n
	}

	base := -1
	height := max(0, t.pwindow.Height()-t.activePreviewOpts.headerLines)
	for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
		if strings.HasPrefix(component, "-/") {
			component = component[1:]
		}
		if component[0] == '/' {
			denom := atoi(component[1:])
			if denom != 0 {
				base -= height / denom
			}
			break
		}
		base += atoi(component)
	}
	return max(0, base)
}

func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
	tempFiles := []string{}
	current := params.allItems[0]
	selected := params.allItems[1]
	matched := params.allItems[2]

	// replace placeholders one by one
	replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
		escaped, match, flags := parsePlaceholder(match)

		// this function implements the effects a placeholder has on items
		var replace func(*Item) string

		// placeholder types (escaped, query type, item type, token type)
		switch {
		case escaped:
			return match
		case match == "{q}" || match == "{fzf:query}":
			return params.executor.QuoteEntry(params.query)
		case strings.HasPrefix(match, "{q:"):
			if nth, err := splitNth(match[3 : len(match)-1]); err == nil {
				elems, prefixLength := awkTokenizer(params.query)
				tokens := withPrefixLengths(elems, prefixLength)
				trans := Transform(tokens, nth)
				result := JoinTokens(trans)
				if !flags.preserveSpace {
					result = strings.TrimSpace(result)
				}
				return params.executor.QuoteEntry(result)
			}

			return match
		case match == "{}":
			replace = func(item *Item) string {
				switch {
				case flags.number:
					n := item.text.Index
					if n == minItem.Index() {
						// NOTE: Item index should normally be positive, but if there's no
						// match, it will be set to math.MinInt32, and we don't want to
						// show that value. However, int32 can overflow, especially when
						// `--tail` is used with an endless input stream, and the index of
						// an item actually can be math.MinInt32. In that case, you're
						// getting an incorrect value, but we're going to ignore that for
						// now.
						return "''"
					}
					return strconv.Itoa(int(n))
				case flags.file || flags.raw:
					return item.AsString(params.stripAnsi)
				default:
					return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
				}
			}
		case match == "{fzf:action}":
			return params.lastAction.Name()
		case match == "{fzf:prompt}":
			return params.executor.QuoteEntry(params.prompt)
		default:
			// token type and also failover (below)
			rangeExpressions := strings.Split(match[1:len(match)-1], ",")
			ranges := make([]Range, len(rangeExpressions))
			for idx, s := range rangeExpressions {
				r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
				if !ok {
					// Invalid expression, just return the original string in the template
					return match
				}
				ranges[idx] = r
			}

			replace = func(item *Item) string {
				tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
				trans := Transform(tokens, ranges)
				str := JoinTokens(trans)

				// trim the last delimiter
				if params.delimiter.str != nil {
					str = strings.TrimSuffix(str, *params.delimiter.str)
				} else if params.delimiter.regex != nil {
					delims := params.delimiter.regex.FindAllStringIndex(str, -1)
					// make sure the delimiter is at the very end of the string
					if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
						str = str[:delims[len(delims)-1][0]]
					}
				}

				if !flags.preserveSpace {
					str = strings.TrimSpace(str)
				}
				if !flags.file && !flags.raw {
					str = params.executor.QuoteEntry(str)
				}
				return str
			}
		}

		// apply 'replace' function over proper set of items and return result

		items := current
		if flags.asterisk {
			items = matched
		} else if flags.plus || params.forcePlus {
			items = selected
		}
		replacements := make([]string, len(items))

		for idx, item := range items {
			replacements[idx] = replace(item)
		}

		if flags.file {
			file := WriteTemporaryFile(replacements, params.printsep)
			tempFiles = append(tempFiles, file)
			return file
		}
		return strings.Join(replacements, " ")
	})

	return replaced, tempFiles
}

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

func (t *Terminal) captureLine(template string) string {
	return t.executeCommand(template, false, true, true, true, "")
}

func (t *Terminal) captureLines(template string) string {
	return t.executeCommand(template, false, true, true, false, "")
}

func (t *Terminal) captureAsync(a action, firstLineOnly bool, callback func(string)) {
	_, list := t.buildPlusList(a.a, false)
	command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list)
	version := t.bgVersion
	cmd := t.executor.ExecCommand(command, true)
	cmd.Env = t.environ()
	item := func(proceed bool) {
		if proceed {
			out, _ := cmd.StdoutPipe()
			reader := bufio.NewReader(out)
			var output string
			if err := cmd.Start(); err == nil {
				runningCmd := runningCmd{cmd, tempFiles}
				t.runningCmds.Add(&runningCmd)
				if firstLineOnly {
					output, _ = reader.ReadString('\n')
					output = strings.TrimRight(output, "\r\n")
				} else {
					bytes, _ := io.ReadAll(reader)
					output = string(bytes)
				}
				cmd.Wait()
				t.runningCmds.Remove(&runningCmd)
			}
			t.callbackChan <- versionedCallback{version, func() { callback(output) }}
		}
		removeFiles(tempFiles)

	}
	queue, prs := t.bgQueue[a]
	if !prs {
		queue = []func(bool){}
	}
	queue = append(queue, item)
	t.bgQueue[a] = queue
}

func (t *Terminal) dispatchAsync() {
Loop:
	for a, queue := range t.bgQueue {
		delete(t.bgQueue, a)
		if len(queue) == 0 {
			continue
		}

		semaphore, prs := t.bgSemaphores[a]
		if !prs {
			semaphore = make(chan struct{}, maxBgProcessesPerAction)
			t.bgSemaphores[a] = semaphore
		}
		for _, item := range queue {
			select {
			// Acquire local semaphore
			case semaphore <- struct{}{}:
			default:
				// Failed to acquire local semaphore, putting only the last one back to the queue
				for _, item := range queue[:len(queue)-1] {
					item(false)
				}
				t.bgQueue[a] = queue[len(queue)-1:]
				continue Loop
			}
			todo := item
			go func() {
				// Acquire global semaphore
				t.bgSemaphore <- struct{}{}

				todo(true)
				// Release local semaphore
				<-semaphore
				// Release global semaphore
				<-t.bgSemaphore
			}()
		}
	}
}

func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string {
	line := ""
	valid, list := t.buildPlusList(template, forcePlus)
	// 'capture' is used for transform-* and we don't want to
	// return an empty string in those cases
	if !valid && !capture {
		return line
	}
	command, tempFiles := t.replacePlaceholder(template, forcePlus, string(t.input), list)
	cmd := t.executor.ExecCommand(command, false)
	cmd.Env = t.environ()
	if len(info) > 0 {
		cmd.Env = append(cmd.Env, "FZF_INFO="+info)
	}
	t.executing.Set(true)
	if !background {
		// Open a separate handle for tty input
		if in, _ := tui.TtyIn(t.ttyDefault); in != nil {
			cmd.Stdin = in
			if in != os.Stdin {
				defer in.Close()
			}
		}

		cmd.Stdout = os.Stdout
		if !util.IsTty(os.Stdout) {
			if out, _ := tui.TtyOut(t.ttyDefault); out != nil {
				cmd.Stdout = out
				defer out.Close()
			}
		}

		cmd.Stderr = os.Stderr
		if !util.IsTty(os.Stderr) {
			if out, _ := tui.TtyOut(t.ttyDefault); out != nil {
				cmd.Stderr = out
				defer out.Close()
			}
		}

		t.mutex.Unlock()
		if len(info) == 0 {
			t.uiMutex.Lock()
		}
		t.tui.Pause(true)
		cmd.Run()
		t.tui.Resume(true, false)
		t.mutex.Lock()
		// NOTE: Using t.reqBox.Set(reqFullRedraw...) instead can cause a deadlock
		t.fullRedraw()
		t.flush()
	} else {
		t.mutex.Unlock()
		if len(info) == 0 {
			t.uiMutex.Lock()
		}
		paused := atomic.Int32{}
		ctx, cancel := context.WithCancel(context.Background())
		go func() {
			select {
			case <-ctx.Done():
				return
			case <-time.After(blockDuration):
				if paused.CompareAndSwap(0, 1) {
					t.tui.Pause(false)
				}
			}
		}()
		if capture {
			out, _ := cmd.StdoutPipe()
			reader := bufio.NewReader(out)
			cmd.Start()
			if firstLineOnly {
				line, _ = reader.ReadString('\n')
				line = strings.TrimRight(line, "\r\n")
			} else {
				bytes, _ := io.ReadAll(reader)
				line = string(bytes)
			}
			cmd.Wait()
		} else {
			cmd.Run()
		}
		cancel()
		if paused.CompareAndSwap(1, 2) {
			t.tui.Resume(false, false)
		}
		t.mutex.Lock()

		// Redraw prompt in case the user has typed something after blockDuration
		if paused.Load() > 0 {
			// NOTE: Using t.reqBox.Set(reqXXX...) instead can cause a deadlock
			t.printPrompt()
			if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
				t.printInfo()
			}
		}
	}
	if len(info) == 0 {
		t.uiMutex.Unlock()
	}
	t.executing.Set(false)
	removeFiles(tempFiles)
	return line
}

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

func (t *Terminal) needPreviewWindow() bool {
	return t.hasPreviewer() && len(t.previewOpts.command) > 0 && t.activePreviewOpts.Visible()
}

// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
func (t *Terminal) canPreview() bool {
	return t.hasPreviewer() && (!t.activePreviewOpts.Visible() && !t.activePreviewOpts.hidden || t.hasPreviewWindow())
}

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

func (t *Terminal) hasPreviewWindowOnRight() bool {
	return t.hasPreviewWindow() && t.activePreviewOpts.position == posRight
}

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

func (t *Terminal) isCurrentItemMatch() bool {
	cnt := t.merger.Length()
	if t.cy >= 0 && cnt > 0 && cnt > t.cy {
		if !t.raw {
			return true
		}
		item := t.merger.Get(t.cy).item
		return t.isItemMatch(item)
	}
	return false
}

func (t *Terminal) isItemMatch(item *Item) bool {
	_, matched := t.matchMap[item.Index()]
	return matched
}

func (t *Terminal) filterSelected() {
	filtered := make(map[int32]selectedItem)
	for k, v := range t.selected {
		if t.isItemMatch(v.item) {
			filtered[k] = v
		}
	}
	t.selected = filtered
}

func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) {
	current := t.currentItem()
	slot, plus, asterisk, forceUpdate := hasPreviewFlags(template)
	if !(!slot || forceUpdate || asterisk || (forcePlus || plus) && len(t.selected) > 0) {
		if current == nil {
			// Invalid
			return false, [3][]*Item{nil, nil, nil}
		}
		return true, [3][]*Item{{current}, {current}, nil}
	}

	// We would still want to update preview window even if there is no match if
	//   1. the command template contains {q}
	//   2. or it contains {+} and we have more than one item already selected.
	// To do so, we pass an empty Item instead of nil to trigger an update.
	if current == nil {
		current = &minItem
	}

	var all []*Item
	if asterisk {
		cnt := t.merger.Length()
		all = make([]*Item, cnt)
		for i := range cnt {
			all[i] = t.merger.Get(i).item
		}
	}

	var sels []*Item
	if len(t.selected) == 0 {
		sels = []*Item{current}
	} else if len(t.selected) > 0 {
		sels = make([]*Item, len(t.selected))
		for i, sel := range t.sortSelected() {
			sels[i] = sel.item
		}
	}
	return true, [3][]*Item{{current}, sels, all}
}

func (t *Terminal) selectItem(item *Item) bool {
	if len(t.selected) >= t.multi {
		return false
	}
	if _, found := t.selected[item.Index()]; found {
		return true
	}

	t.selected[item.Index()] = selectedItem{time.Now(), item}
	t.version++

	return true
}

func (t *Terminal) selectItemChanged(item *Item) bool {
	if _, found := t.selected[item.Index()]; found {
		return false
	}
	return t.selectItem(item)
}

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

func (t *Terminal) deselectItemChanged(item *Item) bool {
	if _, found := t.selected[item.Index()]; found {
		t.deselectItem(item)
		return true
	}
	return false
}

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

func (t *Terminal) killPreview() {
	select {
	case t.killChan <- true:
		<-t.killedChan
	default:
	}
}

func (t *Terminal) cancelPreview() {
	select {
	case t.killChan <- false:
	default:
	}
}

func (t *Terminal) pwindowSize() tui.TermSize {
	if t.pwindow == nil {
		return tui.TermSize{}
	}
	size := tui.TermSize{Lines: t.pwindow.Height(), Columns: t.pwindow.Width()}

	if t.termSize.PxWidth > 0 {
		size.PxWidth = size.Columns * t.termSize.PxWidth / t.termSize.Columns
		size.PxHeight = size.Lines * t.termSize.PxHeight / t.termSize.Lines
	}
	return size
}

func (t *Terminal) currentIndex() int32 {
	if currentItem := t.currentItem(); currentItem != nil {
		return currentItem.Index()
	}
	return minItem.Index()
}

func (t *Terminal) currentFocus() focusedItem {
	return focusedItem{t.revision.major, t.currentItem()}
}

// Records the current focus and returns whether it differs from the last one.
// Indexes restart after a reload, so items are compared by --id-nth key, if
// set. Without it, any focused item after a reload is a new one.
func (t *Terminal) updateFocus() bool {
	curr := t.currentFocus()
	last := t.lastFocus
	t.lastFocus = curr
	if curr.item == nil || last.item == nil {
		return curr.item != last.item
	}
	if curr.major == last.major {
		return curr.item.Index() != last.item.Index()
	}
	return len(t.idNth) == 0 || t.trackKeyFor(curr.item, t.idNth) != t.trackKeyFor(last.item, t.idNth)
}

func (t *Terminal) trackKeyFor(item *Item, nth []Range) string {
	tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
	return StripLastDelimiter(JoinTokens(Transform(tokens, nth)), t.delimiter)
}

func (t *Terminal) unblockTrack() {
	if t.trackBlocked {
		t.trackBlocked = false
		t.trackKey = ""
		t.trackKeyCache = nil
		// Keep the cursor hidden if the wait feedback is still showing it
		if !t.inputless && !t.waitFeedback() {
			t.tui.ShowCursor()
		}
	}
}

// The wait state machine. Invariant: arming captures every action after
// 'wait' at any nesting level into wait.pending; while blocked, actions are
// dropped unless they are results of work started before the block
// (bg-transform callbacks, bracketed paste bookkeeping); abort/cancel
// discards everything.

// blockWait blocks action execution and defers the given actions until the
// current search completes (see UpdateList)
func (t *Terminal) blockWait(pending []*action) {
	t.wait.blocked = true
	t.wait.blockedAt = time.Now()
	// Clone so that later joins don't append into the backing array of the
	// bound action list
	t.wait.pending = slices.Clone(pending)
	// Show the waiting feedback only if the search takes long enough,
	// so that quick searches don't cause flickering
	go func() {
		timer := time.NewTimer(progressMinDuration)
		<-timer.C
		t.mutex.Lock()
		blocked := t.wait.blocked
		t.mutex.Unlock()
		if blocked {
			t.reqBox.Set(reqPrompt, nil)
			t.reqBox.Set(reqInfo, nil)
		}
	}()
}

// unblockWait lifts the block, leaving the pending actions to the caller:
// UpdateList keeps them for the main loop to replay, cancelWait discards them
func (t *Terminal) unblockWait() {
	t.wait.blocked = false
	// Restore the cursor unless it's still hidden for another reason
	if !t.inputless && !t.trackBlocked {
		t.tui.ShowCursor()
	}
}

// cancelWait unblocks and discards the pending actions (user abort)
func (t *Terminal) cancelWait() {
	t.unblockWait()
	t.wait.pending = nil
}

// Debounce visual feedback so quick searches don't cause flashing
func (t *Terminal) waitFeedback() bool {
	return t.wait.blocked && time.Since(t.wait.blockedAt) > progressMinDuration
}

func (t *Terminal) addClickHeaderWord(env []string) []string {
	/*
	 * echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK'
	 *
	 *   REVERSE      DEFAULT      REVERSE-LIST
	 *   H1      1            1    HL1     1
	 *   H2      2    HL2     2    HL2     2
	 *   -------      HL1     3            3
	 *   HL1     3    -------      -------
	 *   HL2     4    H1      4    H1      4
	 *           5    H2      5    H2      5
	 */
	clickHeaderLine := t.clickHeaderLine - 1
	if clickHeaderLine < 0 {
		// Never clicked on the header
		return env
	}

	nthBase := 0
	// Convert header items to strings for click handling
	headerStrs := make([]string, len(t.header))
	for i, item := range t.header {
		headerStrs[i] = item.text.ToString()
	}
	headers := [2][]string{headerStrs, t.header0}
	if t.layout == layoutReverse {
		headers[0], headers[1] = headers[1], headers[0]
	}
	var trimmedLine string
	var words []Token
	var lineNum int
	for lineNum = 0; lineNum <= clickHeaderLine; lineNum++ {
		currentLine := lineNum == clickHeaderLine
		var line string
		if lineNum < len(headers[0]) {
			index := lineNum
			if t.layout == layoutDefault {
				index = len(headers[0]) - index - 1
			}
			line = headers[0][index]
		} else if lineNum-len(headers[0]) < len(headers[1]) {
			line = headers[1][lineNum-len(headers[0])]
		}
		if currentLine && len(line) == 0 {
			return env
		}

		// NOTE: We can't expand tabs here because the delimiter can contain tabs.
		trimmedLine, _, _ = extractColor(line, nil, nil)
		words = Tokenize(trimmedLine, t.delimiter)
		if currentLine {
			break
		} else {
			// TODO: Counting can be incorrect when the delimiter contains new line
			// characters, and there are empty lines in the header.
			nthBase += len(words)
		}
	}

	colNum := t.clickHeaderColumn - 1
	prefixWidth, prefixLength := 0, 0
	for idx, token := range words {
		prefixWidth += t.displayWidthWithPrefix(trimmedLine[prefixLength:token.prefixLength], prefixWidth)
		prefixLength = int(token.prefixLength)

		word, _ := t.processTabs(token.text.ToRunes(), prefixWidth)
		trimmed := strings.TrimRightFunc(word, unicode.IsSpace)
		trimWidth := t.displayWidthWithPrefix(trimmed, prefixWidth)

		// Find the position of the first non-space character in the word
		minPos := strings.IndexFunc(trimmed, func(r rune) bool {
			return !unicode.IsSpace(r)
		})
		if colNum >= minPos && colNum >= prefixWidth && colNum < prefixWidth+trimWidth {
			env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_WORD=%s", trimmed))
			nth := fmt.Sprintf("FZF_CLICK_HEADER_NTH=%d", nthBase+idx+1)
			if lineNum == len(t.header)+len(t.header0)-1 && idx == len(words)-1 {
				nth += ".."
			}
			env = append(env, nth)
			return env
		}
	}
	return env
}

func (t *Terminal) addClickFooterWord(env []string) []string {
	clickFooterLine := t.clickFooterLine - 1
	if clickFooterLine < 0 || clickFooterLine >= len(t.footer) {
		// Never clicked on the footer
		return env
	}

	// NOTE: Unlike in click-header, we don't use --delimiter here, since we're
	// only interested in the word, not nth. Does this make sense?
	trimmed, _, _ := extractColor(t.footer[clickFooterLine], nil, nil)
	trimmed, _ = t.processTabsStr(trimmed, 0)
	words := Tokenize(trimmed, Delimiter{})
	colNum := t.clickFooterColumn - 1
	for _, token := range words {
		prefixWidth := int(token.prefixLength)
		word := token.text.ToString()
		trimmed := strings.TrimRightFunc(word, unicode.IsSpace)
		trimWidth := t.displayWidthWithPrefix(trimmed, prefixWidth)

		// Find the position of the first non-space character in the word
		minPos := strings.IndexFunc(trimmed, func(r rune) bool {
			return !unicode.IsSpace(r)
		})
		if colNum >= minPos && colNum >= prefixWidth && colNum < prefixWidth+trimWidth {
			env = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_WORD=%s", trimmed))
			return env
		}
	}
	return env
}

// startTimers spawns a goroutine per every() bind event. Forwarding ticks
// onto the unbuffered timerChan lets the ticker drop overlapping ticks
// while the main loop is busy.
func (t *Terminal) startTimers(ctx context.Context) {
	for evt := range t.keymap {
		switch evt.Type {
		case tui.Every:
			d := time.Duration(evt.Char) * time.Millisecond
			evt := evt
			go func() {
				ticker := time.NewTicker(d)
				defer ticker.Stop()
				for {
					select {
					case <-ctx.Done():
						return
					case <-ticker.C:
						select {
						case <-ctx.Done():
							return
						case t.timerChan <- evt:
						}
					}
				}
			}()
		}
	}
}

// nextValue returns the first valid, non-current value after the current one
// in a '|'-separated list, and the list rotated to end with it.
// isValid can be nil.
func nextValue(expr string, isCurrent func(string) bool, isValid func(string) bool) (string, string) {
	valid := func(value string) bool {
		return isValid == nil || isValid(value)
	}
	// An invalid value is never current, even if it resolves to the current one
	current := func(value string) bool {
		return valid(value) && isCurrent(value)
	}
	values := strings.Split(expr, "|")
	start := 0
	// Last value is the one applied by the previous call
	if !current(values[len(values)-1]) {
		for i, value := range values {
			if current(value) {
				start = i + 1
				break
			}
		}
	}
	for i := range values {
		pick := (start + i) % len(values)
		if value := values[pick]; valid(value) && !current(value) {
			return value, strings.Join(slices.Concat(values[pick+1:], values[:pick+1]), "|")
		}
	}
	// Nothing to pick
	for _, value := range values {
		if valid(value) {
			return value, expr
		}
	}
	return values[0], expr
}

// Loop is called to start Terminal I/O
func (t *Terminal) Loop() error {
	// prof := profile.Start(profile.ProfilePath("/tmp/"))
	fitpad := <-t.startChan
	fit := fitpad.fit
	if fit >= 0 {
		pad := fitpad.pad
		t.tui.Resize(func(termHeight int) int {
			contentHeight := fit + t.extraLines()
			if t.needPreviewWindow() {
				if t.activePreviewOpts.aboveOrBelow() {
					if t.activePreviewOpts.size.percent {
						newContentHeight := int(float64(contentHeight) * 100. / (100. - t.activePreviewOpts.size.size))
						contentHeight = max(contentHeight+1+borderLines(t.activePreviewOpts.Border(t.layout)), newContentHeight)
					} else {
						contentHeight += int(t.activePreviewOpts.size.size) + borderLines(t.activePreviewOpts.Border(t.layout))
					}
				} else {
					// Minimum height if preview window can appear
					contentHeight = max(contentHeight, 1+borderLines(t.activePreviewOpts.Border(t.layout)))
				}
			}
			return min(termHeight, contentHeight+pad)
		})
	}

	// Context
	ctx, cancel := context.WithCancel(context.Background())

	{ // Late initialization
		intChan := make(chan os.Signal, 1)
		signal.Notify(intChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
		go func() {
			for {
				select {
				case <-ctx.Done():
					signal.Stop(intChan)
					return
				case s := <-intChan:
					// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
					if !(s == os.Interrupt && t.executing.Get()) {
						t.reqBox.Set(reqQuit, nil)
					}
				}
			}
		}()

		if !t.tui.ShouldEmitResizeEvent() {
			resizeChan := make(chan os.Signal, 1)
			notifyOnResize(ctx, resizeChan) // Non-portable
			go func() {
				for {
					select {
					case <-ctx.Done():
						return
					case <-resizeChan:
						t.reqBox.Set(reqResize, nil)
					}
				}
			}()
		}

		t.mutex.Lock()
		if err := t.initFunc(); err != nil {
			t.mutex.Unlock()
			cancel()
			t.eventBox.Set(EvtQuit, quitSignal{ExitError, err})
			return err
		}
		t.termSize = t.tui.Size()
		t.resizeWindows(false, false)
		t.window.Erase()
		t.mutex.Unlock()

		t.reqBox.Set(reqPrompt, nil)
		t.reqBox.Set(reqInfo, nil)
		t.reqBox.Set(reqHeader, nil)
		t.reqBox.Set(reqFooter, nil)
		if t.initDelay > 0 {
			go func() {
				timer := time.NewTimer(t.initDelay)
				<-timer.C
				t.reqBox.Set(reqActivate, nil)
			}()
		}

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

	if t.hasPreviewer() {
		go func() {
			var version int64
			stop := false
			t.previewBox.WaitFor(reqPreviewReady)
			for {
				requested := false
				var items [3][]*Item
				var commandTemplate string
				var env []string
				var query string
				initialOffset := 0
				t.previewBox.Wait(func(events *util.Events) {
					for req, value := range *events {
						switch req {
						case reqQuit:
							stop = true
							return
						case reqPreviewEnqueue:
							request := value.(previewRequest)
							commandTemplate = request.template
							initialOffset = request.scrollOffset
							items = request.list
							env = request.env
							query = request.query
							requested = true
						}
					}
					events.Clear()
				})
				if stop {
					break
				}
				if !requested {
					continue
				}
				version++
				// We don't display preview window if no match
				if items[0] != nil {
					command, tempFiles := t.replacePlaceholder(commandTemplate, false, query, items)
					cmd := t.executor.ExecCommand(command, true)
					cmd.Env = env

					out, _ := cmd.StdoutPipe()
					cmd.Stderr = cmd.Stdout
					reader := bufio.NewReader(out)
					eofChan := make(chan bool)
					finishChan := make(chan bool, 1)
					err := cmd.Start()
					if err == nil {
						reapChan := make(chan bool)
						lineChan := make(chan eachLine)
						// Goroutine 1 reads process output
						go func() {
							for {
								line, err := reader.ReadString('\n')
								lineChan <- eachLine{line, err}
								if err != nil {
									break
								}
							}
							eofChan <- true
						}()

						// Goroutine 2 periodically requests rendering
						rendered := util.NewAtomicBool(false)
						go func(version int64) {
							lines := []string{}
							spinner := makeSpinner(t.unicode)
							spinnerIndex := -1 // Delay initial rendering by an extra tick
							ticker := time.NewTicker(previewChunkDelay)
							offset := initialOffset
						Loop:
							for {
								select {
								case <-ticker.C:
									if len(lines) > 0 && len(lines) >= initialOffset {
										if spinnerIndex >= 0 {
											spin := spinner[spinnerIndex%len(spinner)]
											t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
											rendered.Set(true)
											offset = -1
										}
										spinnerIndex++
									}
								case eachLine := <-lineChan:
									line := eachLine.line
									err := eachLine.err
									if len(line) > 0 {
										clearIndex := strings.Index(line, clearCode)
										if clearIndex >= 0 {
											lines = []string{}
											line = line[clearIndex+len(clearCode):]
											version--
											offset = 0
										}
										if split := t.splitOnIND(line); split != nil {
											lines = append(lines, split...)
										} else {
											lines = append(lines, line)
										}
									}
									if err != nil {
										t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
										rendered.Set(true)
										break Loop
									}
								}
							}
							ticker.Stop()
							reapChan <- true
						}(version)

						// Goroutine 3 is responsible for cancelling running preview command
						go func(version int64) {
							timer := time.NewTimer(previewDelayed)
						Loop:
							for {
								select {
								case <-ctx.Done():
									break Loop
								case <-timer.C:
									t.reqBox.Set(reqPreviewDelayed, version)
								case immediately := <-t.killChan:
									if immediately {
										util.KillCommand(cmd)
										t.killedChan <- true
									} else {
										// We can immediately kill a long-running preview program
										// once we started rendering its partial output
										delay := previewCancelWait
										if rendered.Get() {
											delay = 0
										}
										timer := time.NewTimer(delay)
										select {
										case <-timer.C:
											util.KillCommand(cmd)
										case <-finishChan:
										}
										timer.Stop()
									}
									break Loop
								case <-finishChan:
									break Loop
								}
							}
							timer.Stop()
							reapChan <- true
						}(version)

						<-eofChan          // Goroutine 1 finished
						cmd.Wait()         // NOTE: We should not call Wait before EOF
						finishChan <- true // Tell Goroutine 3 to stop
						<-reapChan         // Goroutine 2 and 3 finished
						<-reapChan
						removeFiles(tempFiles)
					} else {
						// Failed to start the command. Report the error immediately.
						t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
					}
				} else {
					t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
				}
			}
		}()
	}

	refreshPreview := func(command string) {
		if len(command) > 0 && t.canPreview() {
			_, list := t.buildPlusList(command, false)
			t.cancelPreview()
			t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
		}
	}

	go func() { // Render loop
		var focusedIndex = minItem.Index()
		var version int64 = -1
		running := true
		code := ExitError
		exit := func(getCode func() int) {
			if t.hasPreviewer() {
				t.previewBox.Set(reqQuit, nil)
			}
			if t.listener != nil {
				t.listener.Close()
			}
			t.tui.Close()
			code = getCode()
			if code <= ExitNoMatch && t.history != nil {
				t.history.append(string(t.input))
			}
			t.runningCmds.ForEach(func(cmd *runningCmd) {
				util.KillCommand(cmd.cmd)
				removeFiles(cmd.tempFiles)
			})
			running = false
			t.mutex.Unlock()
		}

		for running {
			t.reqBox.Wait(func(events *util.Events) {
				defer events.Clear()

				// Sort events.
				// e.g. Make sure that reqPrompt is processed before reqInfo
				keys := make([]int, 0, len(*events))
				for key := range *events {
					keys = append(keys, int(key))
				}
				sort.Ints(keys)

				// t.uiMutex must be locked first to avoid deadlock. Execute actions
				// will 1. unlock t.mutex to allow GET endpoint and 2. lock t.uiMutex
				// to block rendering during the execution.
				//
				// T1           T2 (good)       |  T1            T2 (bad)
				//               L t.uiMutex    |
				//  L t.mutex                   |   L t.mutex
				//  U t.mutex                   |   U t.mutex
				//               L t.mutex      |                 L t.mutex
				//               U t.mutex      |   L t.uiMutex
				//               U t.uiMutex    |                 L t.uiMutex!!
				//  L t.uiMutex                 |
				//                              |   L t.mutex!!
				//  L t.mutex                   |   U t.uiMutex
				//  U t.uiMutex                 |
				t.uiMutex.Lock()
				t.mutex.Lock()
				info := false
				header := false
				footer := false
				for _, key := range keys {
					req := util.EventType(key)
					value := (*events)[req]
					switch req {
					case reqPrompt:
						t.printPrompt()
						if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
							info = true
						}
					case reqInfo:
						info = true
					case reqList:
						t.printList()
						currentIndex := t.currentIndex()
						if t.track.Current() && t.track.index != currentIndex {
							t.track = trackDisabled
							info = true
						}
						focusChanged := focusedIndex != currentIndex
						if (t.hasFocusActions || t.infoCommand != "") && !t.trackBlocked && t.updateFocus() {
							t.eventChan <- tui.Focus.AsEvent()
							if t.infoCommand != "" {
								info = true
							}
						}
						if focusChanged || version != t.version {
							version = t.version
							focusedIndex = currentIndex
							refreshPreview(t.previewOpts.command)
						}
					case reqJump:
						if t.merger.Length() == 0 {
							t.jumping = jumpDisabled
						}
						t.printList()
					case reqHeader:
						header = true
					case reqFooter:
						footer = true
					case reqActivate:
						t.suppress = false
						if t.hasPreviewer() {
							t.previewBox.Set(reqPreviewReady, nil)
						}
					case reqRedrawInputLabel:
						t.printLabel(t.inputBorder, t.inputLabel, t.inputLabelOpts, t.inputLabelLen, t.inputBorderShape, true)
					case reqRedrawHeaderLabel:
						if t.headerBorderShape == tui.BorderInline {
							// Inline labels sit on the separator inside wborder; re-run the
							// full layout to repaint the separator + label together.
							t.printAll()
						} else {
							t.printLabel(t.headerBorder, t.headerLabel, t.headerLabelOpts, t.headerLabelLen, t.headerBorderShape, true)
						}
					case reqRedrawFooterLabel:
						if t.footerBorderShape == tui.BorderInline {
							t.printAll()
						} else {
							t.printLabel(t.footerBorder, t.footerLabel, t.footerLabelOpts, t.footerLabelLen, t.footerBorderShape, true)
						}
					case reqRedrawListLabel:
						// When inline sections are active, the label's bg depends on which
						// section owns the adjacent edge. Rerun the layout to reuse that
						// logic rather than duplicating it here.
						if t.headerBorderShape == tui.BorderInline || t.headerLinesShape == tui.BorderInline || t.footerBorderShape == tui.BorderInline {
							t.printAll()
						} else {
							t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
						}
					case reqRedrawBorderLabel:
						t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
					case reqRedrawPreviewLabel:
						t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(t.layout), true)
					case reqReinit, reqResize, reqFullRedraw, reqRedraw:
						if req == reqReinit {
							t.tui.Resume(t.fullscreen, true)
						}
						if req == reqResize {
							t.termSize = t.tui.Size()
						}
						wasHidden := t.pwindow == nil
						if req == reqRedraw {
							t.printAll()
						} else {
							t.fullRedraw()
						}
						if wasHidden && t.hasPreviewWindow() {
							refreshPreview(t.previewOpts.command)
						}
						if req == reqResize && t.hasResizeActions {
							t.eventChan <- tui.Resize.AsEvent()
						}
					case reqClose:
						exit(func() int {
							if t.output() {
								return ExitOk
							}
							return ExitNoMatch
						})
						return
					case reqPreviewDisplay:
						result := value.(previewResult)
						if t.previewer.version != result.version {
							t.previewer.version = result.version
							t.previewer.following.Force(t.activePreviewOpts.follow)
							if t.previewer.following.Enabled() {
								t.previewer.offset = 0
							}
						}
						t.previewer.lines = result.lines
						t.previewer.spinner = result.spinner
						if t.hasPreviewWindow() && t.previewer.following.Enabled() {
							t.previewer.offset = t.followOffset()
						} else if result.offset >= 0 {
							t.previewer.offset = util.Constrain(result.offset, t.activePreviewOpts.headerLines, len(t.previewer.lines)-1)
						}
						t.printPreview()
					case reqPreviewRefresh:
						t.printPreview()
					case reqPreviewDelayed:
						t.previewer.version = value.(int64)
						t.printPreviewDelayed()
					case reqPrintQuery:
						exit(func() int {
							t.printer(string(t.input))
							return ExitOk
						})
						return
					case reqBecome:
						exit(func() int { return ExitBecome })
						return
					case reqQuit:
						exit(func() int { return ExitInterrupt })
						return
					case reqFatal:
						exit(func() int { return ExitError })
						return
					}
				}
				if (info || header || footer) && !t.resizeIfNeeded() {
					if info {
						t.printInfo()
					}
					if header {
						t.printHeader()
					}
					if footer {
						t.printFooter()
					}
				}
				// Hide the cursor while the debounced waiting feedback is shown
				if !t.inputless && !t.trackBlocked && t.waitFeedback() {
					t.tui.HideCursor()
				}
				t.flush()
				t.mutex.Unlock()
				t.uiMutex.Unlock()
			})
		}

		t.running.Set(false)
		t.killPreview()
		cancel()
		t.eventBox.Set(EvtQuit, quitSignal{code, nil})
	}()

	looping := true
	barrier := make(chan bool)
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case <-barrier:
			}
			select {
			case <-ctx.Done():
				return
			case t.keyChan <- t.tui.GetChar(t.listenAddr != nil):
			}
		}
	}()
	t.startTimers(ctx)
	previewDraggingPos := -1
	barDragging := false
	pbarDragging := false
	pborderDragging := -1
	wasDown := false
	pmx, pmy := -1, -1
	needBarrier := true

	// If an action is bound to 'start', we're going to process it before reading
	// user input.
	if !t.hasStartActions {
		barrier <- true
		needBarrier = false
	}

	// These variables are defined outside the loop to be accessible from closures.
	// In particular, async bg-transform callbacks run in a later iteration than
	// the one that scheduled them, but still need to mutate state that the
	// loop-end reload/event dispatch reads.
	events := []util.EventType{}
	changed := false
	var newNth *[]Range
	var newWithNth *withNthSpec
	var newHeaderLines *int
	var newCommand *commandSpec
	var reloadSync bool
	var denylist []int32
	// True while running bg-transform callbacks. Declared outside the loop
	// because callbacks invoke the doActions closure of the iteration that
	// scheduled them, not the one executing actAsync.
	inBgCallback := false
	req := func(evts ...util.EventType) {
		for _, event := range evts {
			events = append(events, event)
			if isTerminalEvent(event) {
				looping = false
			}
		}
	}

	// The main event loop
	for loopIndex := int64(0); looping; loopIndex++ {
		events = []util.EventType{}
		changed = false
		newNth = nil
		newWithNth = nil
		newHeaderLines = nil
		newCommand = nil
		reloadSync = false
		denylist = nil
		beof := false
		queryChanged := false

		// Special handling of --sync. Activate the interface on the second tick.
		if loopIndex == 1 && t.deferActivation() {
			t.reqBox.Set(reqActivate, nil)
		}

		if loopIndex > 0 && needBarrier {
			barrier <- true
			needBarrier = false
		}

		var event tui.Event
		actions := []*action{}
		callbacks := []versionedCallback{}
		select {
		case event = <-t.keyChan:
			needBarrier = true
		case event = <-t.timerChan:
		case event = <-t.eventChan:
			// Drain channel to process all queued events at once without rendering
			// the intermediate states
		Drain:
			for {
				if eventActions, prs := t.keymap[event]; prs {
					actions = append(actions, eventActions...)
				}
				for {
					select {
					case event = <-t.eventChan:
						continue Drain
					default:
						break Drain
					}
				}
			}
		case serverActions := <-t.serverInputChan:
			event = tui.Invalid.AsEvent()
			if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
				actions = serverActions
			} else {
				for _, action := range serverActions {
					if !processExecution(action.t) {
						actions = append(actions, action)
					}
				}
			}
			for _, action := range actions {
				if action.t == actExecute {
					t.tui.CancelGetChar()
					break
				}
			}

		case callback := <-t.callbackChan:
			event = tui.Invalid.AsEvent()
			actions = append(actions, &action{t: actAsync})
			callbacks = append(callbacks, callback)
		DrainCallback:
			for {
				select {
				case callback = <-t.callbackChan:
					callbacks = append(callbacks, callback)
					continue DrainCallback
				default:
					break DrainCallback
				}
			}
		}

		t.mutex.Lock()
		// Ignore --expect keys while wait-blocked like the rest of the input
		if !t.wait.blocked {
			for key, ret := range t.expect {
				if keyMatch(key, event) {
					t.pressed = ret
					t.mutex.Unlock()
					t.reqBox.Set(reqClose, nil)
					return nil
				}
			}
		}
		triggering := map[tui.Event]struct{}{}
		previousInput := t.input
		previousCx := t.cx
		previousVersion := t.version
		if event.Type < tui.Invalid {
			t.lastKey = event.KeyName()
			t.lastActivity = time.Now()
		}
		updatePreviewWindow := func(forcePreview bool) {
			t.resizeWindows(forcePreview, false)
			req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter)
		}
		toggle := func() bool {
			current := t.currentItem()
			if current != nil && t.toggleItem(current) {
				req(reqInfo)
				return true
			}
			return false
		}
		scrollPreviewTo := func(newOffset int) {
			if !t.previewer.scrollable {
				return
			}
			numLines := len(t.previewer.lines)
			headerLines := t.activePreviewOpts.headerLines
			if t.activePreviewOpts.cycle {
				offsetRange := numLines - headerLines
				newOffset = ((newOffset-headerLines)+offsetRange)%offsetRange + headerLines
			}
			newOffset = util.Constrain(newOffset, headerLines, numLines-1)
			if t.previewer.offset != newOffset {
				t.previewer.offset = newOffset
				t.previewer.following.Set(t.previewer.offset >= numLines-(t.pwindow.Height()-headerLines))
				req(reqPreviewRefresh)
			}
		}
		scrollPreviewBy := func(amount int) {
			scrollPreviewTo(t.previewer.offset + amount)
		}

		actionsFor := func(eventType tui.EventType) []*action {
			return t.keymap[eventType.AsEvent()]
		}

		var doAction func(*action) bool
		doActions := func(actions []*action) bool {
			// Snapshot to detect query edits in this batch that trigger a
			// search at loop end without setting 'changed'. String copy:
			// edits mutate t.input in place.
			queryBefore := string(t.input)
			for iter := 0; iter <= maxFocusEvents; iter++ {
				currentIndex := t.currentIndex()
				for i, action := range actions {
					if action.t == actWait {
						// Already waiting. Actions parsed from a bg-transform
						// result join the current wait; user input can't reset
						// the blocked state
						if t.wait.blocked {
							if inBgCallback {
								t.wait.pending = append(t.wait.pending, actions[i+1:]...)
							}
							return true
						}
						// Block if search is in progress or will be triggered
						if changed || newCommand != nil || t.wait.searching || queryBefore != string(t.input) {
							t.blockWait(actions[i+1:])
							return true
						}
						// No search, wait is a no-op; continue to next action
						continue
					}
					blockedBefore := t.wait.blocked
					if !doAction(action) {
						return false
					}
					// If this action armed the wait through a nested list
					// (e.g. via trigger), defer the rest of this list too
					if !blockedBefore && t.wait.blocked {
						t.wait.pending = append(t.wait.pending, actions[i+1:]...)
						return true
					}
					// A terminal action performed. We should stop processing more.
					if !looping {
						break
					}
				}

				// Actions are ignored while blocked. The render loop will
				// report the focus after the reload.
				if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents && !t.trackBlocked {
					if t.currentIndex() != currentIndex {
						t.lastFocus = t.currentFocus()
						if t.infoCommand != "" {
							req(reqInfo)
						}
						actions = onFocus
						continue
					}
				}
				break
			}
			return true
		}
		doAction = func(a *action) bool {
			// Keep track of the current query before the action is executed,
			// so we can restore it when the input section is hidden (--no-input).
			// * By doing this, we don't have to add a conditional branch to each
			//   query modifying action.
			// * We restore the query after each action instead of after a set of
			//   actions to allow changing the query even when the input is hidden
			//     e.g. fzf --no-input --bind 'space:show-input+change-query(foo)+hide-input'
			currentInput := t.input
			capture := func(firstLineOnly bool, callback func(string)) {
				if a.t >= actBgTransform {
					// bg-transform-*
					t.captureAsync(*a, firstLineOnly, callback)
				} else if a.t >= actTransform {
					// transform-*
					if firstLineOnly {
						callback(t.captureLine(a.a))
					} else {
						callback(t.captureLines(a.a))
					}
				} else {
					// change-*
					callback(a.a)
				}
			}
			// Actions that run even while wait/track-blocked: bg-transform
			// callbacks and their parsed actions (results of processes
			// started before the block), and bracketed paste bookkeeping (a
			// swallowed paste-end would leave t.pasting set forever).
			passthrough := inBgCallback || a.t == actAsync ||
				a.t == actBracketedPasteBegin || a.t == actBracketedPasteEnd
			// When wait-blocked, only allow abort/cancel
			if t.wait.blocked && !passthrough {
				if a.t == actAbort || a.t == actCancel {
					t.cancelWait()
					req(reqPrompt, reqInfo)
				}
				return true
			}
			// When track-blocked, only allow abort/cancel and track-disabling actions
			if t.trackBlocked && !passthrough && a.t != actToggleTrack && a.t != actToggleTrackCurrent && a.t != actUntrackCurrent {
				if a.t == actAbort || a.t == actCancel {
					t.unblockTrack()
					req(reqPrompt, reqInfo)
				}
				return true
			}
		Action:
			switch a.t {
			case actIgnore, actStart, actClick:
			case actAsync:
				inBgCallback = true
				for _, callback := range callbacks {
					if t.bgVersion == callback.version {
						callback.callback()
					}
				}
				inBgCallback = false
			case actBecome:
				valid, list := t.buildPlusList(a.a, false)
				if valid {
					// We do not remove temp files in this case
					command, _ := t.replacePlaceholder(a.a, false, string(t.input), list)
					t.tui.Close()
					if t.history != nil {
						t.history.append(string(t.input))
					}

					if len(t.proxyScript) > 0 {
						data := strings.Join(append([]string{command}, t.environ()...), "\x00")
						os.WriteFile(t.proxyScript+becomeSuffix, []byte(data), 0600)
						req(reqBecome)
					} else {
						t.executor.Become(t.ttyin, t.environ(), command)
					}
				}
			case actBell:
				t.tui.Bell()
			case actExcludeMulti:
				if len(t.selected) > 0 {
					for _, item := range t.sortSelected() {
						denylist = append(denylist, item.item.Index())
					}
					// Clear selected items
					t.selected = make(map[int32]selectedItem)
					t.version++
				} else {
					item := t.currentItem()
					if item != nil {
						denylist = append(denylist, item.Index())
					}
				}
				changed = true
			case actExclude:
				if item := t.currentItem(); item != nil {
					denylist = append(denylist, item.Index())
					t.deselectItem(item)
					changed = true
				}
			case actExecute, actExecuteSilent:
				t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
			case actExecuteMulti:
				t.executeCommand(a.a, true, false, false, false, "")
			case actInvalid:
				t.mutex.Unlock()
				return false
			case actBracketedPasteBegin:
				// Clone: []rune(t.input) would alias t.input, and in-place
				// query edits during the paste would corrupt the snapshot
				current := slices.Clone(t.input)
				t.pasting = &current
			case actBracketedPasteEnd:
				if t.pasting != nil {
					queryChanged = string(t.input) != string(*t.pasting)
					t.pasting = nil
				}
			case actTogglePreview, actShowPreview, actHidePreview:
				var act bool
				switch a.t {
				case actShowPreview:
					act = !t.hasPreviewWindow() && len(t.previewOpts.command) > 0
				case actHidePreview:
					act = t.hasPreviewWindow()
				case actTogglePreview:
					act = t.hasPreviewWindow() || len(t.previewOpts.command) > 0
				}
				if act {
					t.activePreviewOpts.Toggle()
					updatePreviewWindow(false)
					if t.canPreview() {
						valid, list := t.buildPlusList(t.previewOpts.command, false)
						if valid {
							t.cancelPreview()
							t.previewBox.Set(reqPreviewEnqueue,
								previewRequest{t.previewOpts.command, t.evaluateScrollOffset(), list, t.environForPreview(), string(t.input)})
						}
					} else {
						// Discard the preview content so that it won't accidentally appear
						// when preview window is re-enabled and previewDelay is triggered
						t.previewer.lines = nil

						// Also kill the preview process if it's still running
						t.cancelPreview()
					}
				}
			case actTogglePreviewWrap, actTogglePreviewWrapWord:
				if t.hasPreviewWindow() {
					if a.t == actTogglePreviewWrapWord {
						t.activePreviewOpts.wrapWord = !t.activePreviewOpts.wrapWord
						t.activePreviewOpts.wrap = t.activePreviewOpts.wrapWord
					} else {
						t.activePreviewOpts.wrap = !t.activePreviewOpts.wrap
						if !t.activePreviewOpts.wrap {
							t.activePreviewOpts.wrapWord = false
						}
					}
					// Reset preview version so that full redraw occurs
					t.previewed.version = 0
					req(reqPreviewRefresh)
				}
			case actTransformPrompt, actBgTransformPrompt:
				capture(true, func(prompt string) {
					t.promptString = prompt
					t.prompt, t.promptLen = t.parsePrompt(prompt)
					req(reqPrompt)
				})
			case actTransformQuery, actBgTransformQuery:
				capture(true, func(query string) {
					t.input = []rune(query)
					t.cx = len(t.input)
				})
			case actToggleSort:
				t.sort = !t.sort
				changed = true
			case actPreviewTop:
				if t.hasPreviewWindow() {
					scrollPreviewTo(0)
				}
			case actPreviewBottom:
				if t.hasPreviewWindow() {
					scrollPreviewTo(len(t.previewer.lines) - t.pwindow.Height())
				}
			case actPreviewUp:
				if t.hasPreviewWindow() {
					scrollPreviewBy(-1)
				}
			case actPreviewDown:
				if t.hasPreviewWindow() {
					scrollPreviewBy(1)
				}
			case actPreviewPageUp:
				if t.hasPreviewWindow() {
					scrollPreviewBy(-t.pwindow.Height())
				}
			case actPreviewPageDown:
				if t.hasPreviewWindow() {
					scrollPreviewBy(t.pwindow.Height())
				}
			case actPreviewHalfPageUp:
				if t.hasPreviewWindow() {
					scrollPreviewBy(-t.pwindow.Height() / 2)
				}
			case actPreviewHalfPageDown:
				if t.hasPreviewWindow() {
					scrollPreviewBy(t.pwindow.Height() / 2)
				}
			case actBeginningOfLine:
				t.cx = 0
				t.xoffset = 0
			case actBackwardChar:
				if t.cx > 0 {
					t.cx--
				}
			case actPrintQuery:
				req(reqPrintQuery)
			case actChangeHeaderLines, actTransformHeaderLines, actBgTransformHeaderLines:
				capture(true, func(expr string) {
					if n, err := strconv.Atoi(expr); err == nil && n >= 0 && n != t.headerLines {
						t.headerLines = n
						newHeaderLines = &n
						changed = true
						// Deselect items that are now part of the header
						for idx := range t.selected {
							if int(idx) < n {
								delete(t.selected, idx)
							}
						}
						// Tell UpdateList to reposition cursor to the current item
						t.targetIndex = t.currentIndex()
						req(reqList, reqPrompt, reqInfo, reqHeader)
					}
				})
			case actChangeMulti:
				multi := t.multi
				if a.a == "" {
					multi = maxMulti
				} else if n, e := strconv.Atoi(a.a); e == nil && n >= 0 {
					multi = n
				}
				if t.multi > 0 && multi != t.multi {
					t.selected = make(map[int32]selectedItem)
					t.version++
				}
				t.multi = multi
				req(reqList, reqInfo)
			case actChangeNth, actTransformNth, actBgTransformNth:
				capture(true, func(expr string) {
					resolve := func(value string) ([]Range, bool) {
						// Empty value restores the default --nth
						if len(value) == 0 {
							return t.nth, true
						}
						nth, err := splitNth(value)
						return nth, err == nil
					}
					// Empty range list or a single '..' means all fields
					allFields := func(nth []Range) bool {
						return len(nth) == 0 || len(nth) == 1 && nth[0].IsFull()
					}
					isCurrent := func(nth []Range) bool {
						return compareRanges(t.nthCurrent, nth) || allFields(t.nthCurrent) && allFields(nth)
					}
					// Only change-nth cycles through '|'-separated values
					if a.t == actChangeNth {
						expr, a.a = nextValue(expr, func(value string) bool {
							nth, _ := resolve(value)
							return isCurrent(nth)
						}, func(value string) bool {
							_, ok := resolve(value)
							return ok
						})
					}
					if nth, ok := resolve(expr); ok && !isCurrent(nth) {
						newNth = &nth
						changed = true
						t.nthCurrent = nth
						t.forceRerenderList()
					}
				})
			case actChangeWithNth, actTransformWithNth, actBgTransformWithNth:
				if !t.withNthEnabled {
					break Action
				}
				capture(true, func(expr string) {
					resolve := func(value string) string {
						// Empty value restores the default --with-nth
						if len(value) == 0 {
							return t.withNthDefault
						}
						return value
					}
					if a.t == actChangeWithNth {
						expr, a.a = nextValue(expr, func(value string) bool {
							return resolve(value) == t.withNthExpr
						}, func(value string) bool {
							_, err := nthTransformer(resolve(value))
							return err == nil
						})
					}
					withNthExpr := resolve(expr)
					if withNthExpr != t.withNthExpr {
						if factory, err := nthTransformer(withNthExpr); err == nil {
							newWithNth = &withNthSpec{fn: factory(t.delimiter)}
						} else {
							return
						}
						t.withNthExpr = withNthExpr
						t.filterSelection = true
						changed = true
						t.clearNumLinesCache()
						t.forceRerenderList()
					}
				})
			case actChangeQuery:
				t.input = []rune(a.a)
				t.cx = len(t.input)
			case actChangeHeader, actTransformHeader, actBgTransformHeader:
				capture(false, func(header string) {
					if t.changeHeader(header) {
						// resizeIfNeeded() tolerates a shorter-than-wanted inline
						// window, so a length change can leave the inline slot
						// stale. Force a redraw to re-run the layout. Non-inline
						// shapes are handled by resizeIfNeeded.
						if t.headerBorderShape == tui.BorderInline {
							req(reqRedraw)
						} else {
							req(reqList, reqPrompt, reqInfo)
						}
					}
					req(reqHeader)
				})
			case actChangeFooter, actTransformFooter, actBgTransformFooter:
				capture(false, func(footer string) {
					if t.changeFooter(footer) && t.footerBorderShape == tui.BorderInline {
						// resizeIfNeeded() tolerates a shorter-than-wanted inline
						// window, so a length change can leave the inline slot
						// stale. Force a redraw to re-run the layout.
						req(reqRedraw)
					}
					req(reqFooter)
				})
			case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
				capture(true, func(label string) {
					t.headerLabelOpts.label = label
					t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
					req(reqRedrawHeaderLabel)
				})
			case actChangeFooterLabel, actTransformFooterLabel, actBgTransformFooterLabel:
				capture(true, func(label string) {
					t.footerLabelOpts.label = label
					t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false)
					req(reqRedrawFooterLabel)
				})
			case actChangeInfoCommand, actTransformInfoCommand, actBgTransformInfoCommand:
				capture(true, func(command string) {
					if t.infoCommand != command {
						t.infoCommand = command
						req(reqInfo)
					}
				})
			case actChangeInputLabel, actTransformInputLabel, actBgTransformInputLabel:
				capture(true, func(label string) {
					t.inputLabelOpts.label = label
					if t.inputBorder != nil {
						t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
						req(reqRedrawInputLabel)
					}
				})
			case actChangeListLabel, actTransformListLabel, actBgTransformListLabel:
				capture(true, func(label string) {
					t.listLabelOpts.label = label
					if t.wborder != nil {
						t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
						req(reqRedrawListLabel)
					}
				})
			case actChangeBorderLabel, actTransformBorderLabel, actBgTransformBorderLabel:
				capture(true, func(label string) {
					t.borderLabelOpts.label = label
					if t.border != nil {
						t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
						req(reqRedrawBorderLabel)
					} else if t.setNativeLabel != nil {
						// fzf draws no border of its own; the label is on the
						// native border of the floating pane
						t.setNativeLabel(label)
					}
				})
			case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel:
				capture(true, func(label string) {
					t.previewLabelOpts.label = label
					if t.pborder != nil {
						t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
						req(reqRedrawPreviewLabel)
					}
				})
			case actTransform, actBgTransform:
				capture(false, func(body string) {
					// Allow 'put' if the triggering key is a printable character
					if actions, err := parseSingleActionList(strings.Trim(body, "\r\n"), event.Printable()); err == nil {
						// NOTE: We're not properly passing the return value here
						doActions(actions)
					}
				})
			case actBgCancel:
				t.bgVersion++
				t.runningCmds.ForEach(func(cmd *runningCmd) {
					util.KillCommand(cmd.cmd)
				})
			case actChangePrompt:
				t.promptString = a.a
				t.prompt, t.promptLen = t.parsePrompt(a.a)
				req(reqPrompt)
			case actPreview:
				if !t.hasPreviewWindow() {
					updatePreviewWindow(true)
				}
				refreshPreview(a.a)
			case actRefreshPreview:
				refreshPreview(t.previewOpts.command)
			case actReplaceQuery:
				current := t.currentItem()
				if current != nil {
					// ToRunes aliases the item text in rune mode, and the
					// editing actions below append into t.input in place
					t.input = append([]rune{}, current.text.ToRunes()...)
					t.cx = len(t.input)
				}
			case actFatal:
				req(reqFatal)
			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 actBackwardDeleteCharEof:
				if len(t.input) == 0 {
					req(reqQuit)
				} else if t.cx > 0 {
					t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
					t.cx--
				}
			case actForwardChar:
				if t.cx < len(t.input) {
					t.cx++
				}
			case actBackwardDeleteChar:
				beof = len(t.input) == 0
				if t.cx > 0 {
					t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
					t.cx--
				}
			case actSelectAll:
				if t.multi > 0 {
					// Limit the scope only to the matching items
					for i := 0; i < t.resultMerger.Length(); i++ {
						if !t.selectItem(t.resultMerger.Get(i).item) {
							break
						}
					}
					req(reqList, reqInfo)
				}
			case actDeselectAll:
				if t.multi > 0 {
					// Also limit the scope only to the matching items, while this may
					// not be straightforward in raw mode.
					for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ {
						t.deselectItem(t.resultMerger.Get(i).item)
					}
					req(reqList, reqInfo)
				}
			case actClose:
				if t.hasPreviewWindow() {
					t.activePreviewOpts.Toggle()
					updatePreviewWindow(false)
				} else {
					req(reqQuit)
				}
			case actSelect:
				current := t.currentItem()
				if t.multi > 0 && current != nil && t.selectItemChanged(current) {
					req(reqList, reqInfo)
				}
			case actDeselect:
				current := t.currentItem()
				if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
					req(reqList, reqInfo)
				}
			case actToggle:
				if t.multi > 0 && t.merger.Length() > 0 && toggle() {
					req(reqList)
				}
			case actToggleAll:
				if t.multi > 0 {
					prevIndexes := make(map[int]struct{})
					for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ {
						item := t.resultMerger.Get(i).item
						if _, found := t.selected[item.Index()]; found {
							prevIndexes[i] = struct{}{}
							t.deselectItem(item)
						}
					}

					for i := 0; i < t.resultMerger.Length(); i++ {
						if _, found := prevIndexes[i]; !found {
							item := t.resultMerger.Get(i).item
							if !t.selectItem(item) {
								break
							}
						}
					}
					req(reqList, reqInfo)
				}
			case actToggleIn:
				if t.layout != layoutDefault {
					return doAction(&action{t: actToggleUp})
				}
				return doAction(&action{t: actToggleDown})
			case actToggleOut:
				if t.layout != layoutDefault {
					return doAction(&action{t: actToggleDown})
				}
				return doAction(&action{t: actToggleUp})
			case actToggleDown:
				if t.multi > 0 && t.merger.Length() > 0 && toggle() {
					t.vmove(-1, true)
					req(reqList)
				}
			case actToggleUp:
				if t.multi > 0 && t.merger.Length() > 0 && toggle() {
					t.vmove(1, true)
					req(reqList)
				}
			case actDown, actDownMatch, actUp, actUpMatch:
				dir := -1
				if a.t == actUp || a.t == actUpMatch {
					dir = 1
				}
				if t.raw && (a.t == actDownMatch || a.t == actUpMatch) {
					if t.resultMerger.Length() > 0 {
						prevCy := t.cy
						for t.vmove(dir, true) && !t.isCurrentItemMatch() {
						}
						if !t.isCurrentItemMatch() {
							t.vset(prevCy)
						}
					}
				} else {
					t.vmove(dir, true)
				}
				req(reqList)
			case actToggleRaw, actEnableRaw, actDisableRaw:
				prevRaw := t.raw
				newRaw := t.raw
				switch a.t {
				case actEnableRaw:
					newRaw = true
				case actDisableRaw:
					newRaw = false
				case actToggleRaw:
					newRaw = !t.raw
				}
				if prevRaw == newRaw {
					break
				}
				prevPos := t.cy - t.offset
				prevIndex := t.currentIndex()
				if newRaw {
					// Build matchMap if not available
					if len(t.matchMap) == 0 {
						t.matchMap = t.resultMerger.ToMap()
					}
					t.merger = t.passMerger
				} else {
					// Find the closest matching item
					if !t.isCurrentItemMatch() && t.resultMerger.Length() > 1 {
						distance := 0
					Loop:
						for {
							distance++
							checks := 0
							for _, cy := range []int{t.cy + distance, t.cy - distance} {
								if cy >= 0 && cy < t.merger.Length() {
									checks++
									item := t.merger.Get(cy).item
									if t.isItemMatch(item) {
										prevIndex = item.Index()
										break Loop
									}
								}
							}
							if checks == 0 {
								break
							}
						}
					}

					t.merger = t.resultMerger

					// Need to remove non-matching items from the selection
					if t.multi > 0 && len(t.selected) > 0 {
						t.filterSelected()
						req(reqInfo)
					}
				}
				t.raw = newRaw

				// Try to retain position
				if prevIndex != minItem.Index() {
					t.cy = max(0, t.merger.FindIndex(prevIndex))
					t.offset = t.cy - prevPos
				}

				// List needs to be rerendered
				t.forceRerenderList()
				req(reqList)
			case actAccept:
				req(reqClose)
			case actAcceptNonEmpty:
				if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
					req(reqClose)
				}
			case actAcceptOrPrintQuery:
				if len(t.selected) > 0 || t.merger.Length() > 0 {
					req(reqClose)
				} else {
					req(reqPrintQuery)
				}
			case actClearScreen:
				req(reqFullRedraw)
			case actClearQuery:
				t.input = []rune{}
				t.cx = 0
			case actClearSelection:
				if t.multi > 0 {
					t.selected = make(map[int32]selectedItem)
					t.version++
					req(reqList, reqInfo)
				}
			case actFirst, actBest:
				if t.raw && a.t == actBest {
					if t.resultMerger.Length() > 0 {
						t.vset(t.merger.FindIndex(t.resultMerger.Get(0).item.Index()))
					}
				} else {
					t.vset(0)
				}
				t.constrain()
				req(reqList)
			case actLast:
				t.vset(t.merger.Length() - 1)
				t.constrain()
				req(reqList)
			case actPosition:
				if n, e := strconv.Atoi(a.a); e == nil {
					if n > 0 {
						n--
					} else if n < 0 {
						n += t.merger.Length()
					}
					t.vset(n)
					t.constrain()
					req(reqList)
				}
			case actPut:
				str := []rune(a.a)
				suffix := copySlice(t.input[t.cx:])
				t.input = append(append(t.input[:t.cx], str...), suffix...)
				t.cx += len(str)
			case actPrint:
				t.printQueue = append(t.printQueue, a.a)
			case actUnixLineDiscard:
				beof = len(t.input) == 0
				if t.cx > 0 {
					t.yanked = copySlice(t.input[:t.cx])
					t.input = t.input[t.cx:]
					t.cx = 0
				}
			case actUnixWordRubout:
				beof = len(t.input) == 0
				if t.cx > 0 {
					t.rubout("\\s\\S")
				}
			case actBackwardKillWord:
				beof = len(t.input) == 0
				if t.cx > 0 {
					t.rubout(t.wordRubout)
				}
			case actBackwardKillSubWord:
				beof = len(t.input) == 0
				if t.cx > 0 {
					t.rubout(t.subWordRubout)
				}
			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, actPageDown, actHalfPageUp, actHalfPageDown:
				// Calculate the number of lines to move
				maxItems := t.maxItems()
				linesToMove := maxItems - 1
				if a.t == actHalfPageUp || a.t == actHalfPageDown {
					linesToMove = maxItems / 2
				}
				// Move at least one line even in a very short window
				linesToMove = max(1, linesToMove)

				// Determine the direction of the movement
				direction := -1
				if a.t == actPageUp || a.t == actHalfPageUp {
					direction = 1
				}

				// In non-default layout, items are listed from top to bottom
				if t.layout != layoutDefault {
					direction *= -1
				}

				// We can simply add the number of lines to the current position in
				// single-line mode
				if !t.canSpanMultiLines() {
					t.vset(t.cy + direction*linesToMove)
					req(reqList)
					break
				}

				// But in multi-line mode, we need to carefully limit the amount of
				// vertical movement so that items are not skipped. In order to do
				// this, we calculate the minimum or maximum offset based on the
				// direction of the movement and the number of lines of the items
				// around the current scroll offset.
				var minOffset, maxOffset, lineSum int
				if direction > 0 {
					maxOffset = t.offset
					for ; maxOffset < t.merger.Length(); maxOffset++ {
						itemLines, _ := t.numItemLines(t.merger.Get(maxOffset).item, maxItems)
						lineSum += itemLines
						if lineSum >= maxItems {
							break
						}
					}
				} else {
					minOffset = t.offset
					for ; minOffset >= 0 && minOffset < t.merger.Length(); minOffset-- {
						itemLines, _ := t.numItemLines(t.merger.Get(minOffset).item, maxItems)
						lineSum += itemLines
						if lineSum >= maxItems {
							if lineSum > maxItems {
								minOffset++
							}
							break
						}
					}
				}

				for i := 0; i < linesToMove; i++ {
					cy, offset := t.cy, t.offset
					t.vset(cy + direction)
					t.constrain()
					if cy == t.cy {
						break
					}
					if i > 0 && (direction > 0 && t.offset > maxOffset ||
						direction < 0 && t.offset < minOffset) {
						t.cy, t.offset = cy, offset
						break
					}
				}
				req(reqList)
			case actOffsetUp, actOffsetDown:
				diff := 1
				if a.t == actOffsetDown {
					diff = -1
				}
				if t.layout != layoutDefault {
					diff *= -1
				}
				t.offset += diff
				before := t.offset
				t.constrain()
				if before != t.offset {
					t.offset = before
					if t.layout != layoutDefault {
						diff *= -1
					}
					t.vmove(diff, false)
				}
				req(reqList)
			case actOffsetMiddle:
				soff := t.scrollOff
				t.scrollOff = t.window.Height()
				t.constrain()
				t.scrollOff = soff
				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 actBackwardSubWord:
				t.cx = findLastMatch(t.subWordRubout, string(t.input[:t.cx])) + 1
			case actForwardSubWord:
				t.cx += findFirstMatch(t.subWordNext, 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 actKillSubWord:
				ncx := t.cx +
					findFirstMatch(t.subWordNext, 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 actChar:
				prefix := copySlice(t.input[:t.cx])
				t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
				t.cx++
			case actPrevHistory:
				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 actToggleSearch:
				t.paused = !t.paused
				changed = !t.paused
				req(reqPrompt)
			case actToggleTrack:
				switch t.track {
				case trackEnabled:
					t.track = trackDisabled
				case trackDisabled:
					t.track = trackEnabled
				}
				t.unblockTrack()
				req(reqPrompt, reqInfo)
			case actToggleTrackCurrent:
				if t.track.Current() {
					t.track = trackDisabled
				} else if t.track.Disabled() {
					t.track = trackCurrent(t.currentIndex())
				}
				t.unblockTrack()
				req(reqPrompt, reqInfo)
			case actShowHeader:
				t.headerVisible = true
				req(reqList, reqInfo, reqPrompt, reqHeader)
			case actHideHeader:
				t.headerVisible = false
				req(reqList, reqInfo, reqPrompt, reqHeader)
			case actToggleHeader:
				t.headerVisible = !t.headerVisible
				req(reqList, reqInfo, reqPrompt, reqHeader)
			case actToggleWrap, actToggleWrapWord:
				if a.t == actToggleWrapWord {
					t.wrapWord = !t.wrapWord
					t.wrap = t.wrapWord
				} else {
					t.wrap = !t.wrap
					if !t.wrap {
						t.wrapWord = false
					}
				}
				t.clearNumLinesCache()
				req(reqList, reqHeader)
			case actToggleMultiLine:
				t.multiLine = !t.multiLine
				t.clearNumLinesCache()
				req(reqList)
			case actToggleHscroll:
				// Force re-rendering of the list
				t.forceRerenderList()
				t.hscroll = !t.hscroll
				req(reqList)
			case actToggleInput, actShowInput, actHideInput:
				switch a.t {
				case actToggleInput:
					t.inputless = !t.inputless
				case actShowInput:
					if !t.inputless {
						break Action
					}
					t.inputless = false
				case actHideInput:
					if t.inputless {
						break Action
					}
					t.inputless = true
				}
				t.forceRerenderList()
				if t.inputless {
					t.tui.HideCursor()
				} else {
					t.tui.ShowCursor()
				}
				req(reqList, reqInfo, reqPrompt, reqHeader)
			case actTrackCurrent:
				// Global tracking has higher priority
				if !t.track.Global() {
					t.track = trackCurrent(t.currentIndex())
				}
				req(reqInfo)
			case actUntrackCurrent:
				if t.track.Current() {
					t.track = trackDisabled
				}
				t.unblockTrack()
				req(reqPrompt, reqInfo)
			case actSearch:
				override := []rune(a.a)
				t.inputOverride = &override
				changed = true
			case actTransformSearch, actBgTransformSearch:
				capture(true, func(query string) {
					override := []rune(query)
					t.inputOverride = &override
					changed = true
				})
			case actEnableSearch:
				t.paused = false
				changed = true
				req(reqPrompt)
			case actDisableSearch:
				t.paused = true
				req(reqPrompt)
			case actTrigger:
				if _, chords, err := parseKeyChords(a.a, ""); err == nil {
					blockedBefore := t.wait.blocked
					for ci, chord := range chords {
						if _, prs := triggering[chord]; prs {
							// Avoid recursive triggering
							continue
						}
						if acts, prs := t.keymap[chord]; prs {
							triggering[chord] = struct{}{}
							doActions(acts)
							delete(triggering, chord)
						}
						// If this chord armed the wait, defer the remaining chords
						if !blockedBefore && t.wait.blocked {
							for _, rest := range chords[ci+1:] {
								if _, prs := triggering[rest]; prs {
									// Avoid recursive triggering
									continue
								}
								if acts, prs := t.keymap[rest]; prs {
									t.wait.pending = append(t.wait.pending, acts...)
								}
							}
							break
						}
					}
				}
			case actSigStop:
				p, err := os.FindProcess(os.Getpid())
				if err == nil {
					t.tui.Clear()
					t.tui.Pause(t.fullscreen)
					notifyStop(p)
					t.mutex.Unlock()
					t.reqBox.Set(reqReinit, nil)
					return false
				}
			case actMouse:
				me := event.MouseEvent
				mx, my := me.X, me.Y
				click := !wasDown && me.Down
				clicked := wasDown && !me.Down && (mx == pmx && my == pmy)
				wasDown = me.Down
				if click {
					pmx, pmy = mx, my
				}
				if !me.Down {
					barDragging = false
					pmx, pmy = -1, -1
				}
				if !me.Down || !t.hasPreviewWindow() {
					pbarDragging = false
					pborderDragging = -1
					previewDraggingPos = -1
				}

				// Scrolling
				if me.S != 0 {
					if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
						evt := tui.ScrollUp
						if me.Mod() {
							evt = tui.SScrollUp
						}
						if me.S < 0 {
							evt = tui.ScrollDown
							if me.Mod() {
								evt = tui.SScrollDown
							}
						}
						return doActions(actionsFor(evt))
					} else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
						evt := tui.PreviewScrollUp
						if me.S < 0 {
							evt = tui.PreviewScrollDown
						}
						return doActions(actionsFor(evt))
					}
					break
				}

				// Preview dragging
				if t.hasPreviewWindow() && me.Down && (previewDraggingPos >= 0 || click && t.pwindow.Enclose(my, mx)) {
					if previewDraggingPos > 0 {
						scrollPreviewBy(previewDraggingPos - my)
					}
					previewDraggingPos = my
					break
				}

				// Preview scrollbar dragging
				headerLines := t.activePreviewOpts.headerLines
				pbarDragging = t.hasPreviewWindow() && me.Down && (pbarDragging || click && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
				if pbarDragging {
					effectiveHeight := t.pwindow.Height() - headerLines
					numLines := len(t.previewer.lines) - headerLines
					barLength, _ := getScrollbar(1, numLines, effectiveHeight, min(numLines-effectiveHeight, t.previewer.offset-headerLines))
					if barLength > 0 {
						y := my - t.pwindow.Top() - headerLines - barLength/2
						y = util.Constrain(y, 0, effectiveHeight-barLength)
						// offset = (total - maxItems) * barStart / (maxItems - barLength)
						t.previewer.offset = headerLines + int(math.Ceil(float64(y)*float64(numLines-effectiveHeight)/float64(effectiveHeight-barLength)))
						t.previewer.following.Set(t.previewer.offset >= numLines-effectiveHeight)
						req(reqPreviewRefresh)
					}
					break
				}

				// Preview border dragging (resizing)
				if t.hasPreviewWindow() && pborderDragging < 0 && click {
					switch t.activePreviewOpts.position {
					case posUp:
						if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
							pborderDragging = 0
						} else if t.listBorderShape.HasTop() && t.pborder.EncloseX(mx) && my == t.wborder.Top() {
							pborderDragging = 1
						}
					case posDown:
						if t.pborder.Enclose(my, mx) && my == t.pborder.Top() {
							pborderDragging = 0
						} else if t.listBorderShape.HasBottom() && t.pborder.EncloseX(mx) && my == t.wborder.Top()+t.wborder.Height()-1 {
							pborderDragging = 1
						}
					case posLeft:
						if t.pborder.Enclose(my, mx) && mx == t.pborder.Left()+t.pborder.Width()-1 {
							pborderDragging = 0
						} else if t.listBorderShape.HasLeft() && t.pborder.EncloseY(my) && mx == t.wborder.Left() {
							pborderDragging = 1
						}
					case posRight:
						if t.pborder.Enclose(my, mx) && mx == t.pborder.Left() {
							pborderDragging = 0
						} else if t.listBorderShape.HasRight() && t.pborder.EncloseY(my) && mx == t.wborder.Left()+t.wborder.Width()-1 {
							pborderDragging = 1
						}
					case posNext:
						if t.layout == layoutReverse {
							if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
								pborderDragging = 0
							} else if t.listBorderShape.HasTop() && t.pborder.EncloseX(mx) && my == t.wborder.Top() {
								pborderDragging = 1
							}
						} else {
							if t.pborder.Enclose(my, mx) && my == t.pborder.Top() {
								pborderDragging = 0
							} else if t.listBorderShape.HasBottom() && t.pborder.EncloseX(mx) && my == t.wborder.Top()+t.wborder.Height()-1 {
								pborderDragging = 1
							}
						}
					}
				}

				if t.hasPreviewWindow() && pborderDragging >= 0 {
					var newSize int
					var prevSize int
					switch t.activePreviewOpts.position {
					case posLeft:
						prevSize = t.pwindow.Width()
						diff := t.pborder.Width() - prevSize
						newSize = mx - t.pborder.Left() - diff + 1
					case posUp:
						prevSize = t.pwindow.Height()
						diff := t.pborder.Height() - prevSize
						newSize = my - t.pborder.Top() - diff + 1
					case posDown:
						prevSize = t.pwindow.Height()
						offset := my - t.pborder.Top()
						newSize = prevSize - offset
					case posRight:
						prevSize = t.pwindow.Width()
						offset := mx - t.pborder.Left()
						newSize = prevSize - offset
					case posNext:
						prevSize = t.pwindow.Height()
						// In posNext, header/header-lines sections may sit
						// between preview and list. When the list border is
						// dragged (pborderDragging == 1), subtract that gap
						// so the initial click does not jump.
						headerGap := 0
						if pborderDragging == 1 && t.wborder != nil {
							if t.layout == layoutReverse {
								headerGap = t.wborder.Top() - (t.pborder.Top() + t.pborder.Height())
							} else {
								headerGap = t.pborder.Top() - (t.wborder.Top() + t.wborder.Height())
							}
						}
						if t.layout == layoutReverse {
							diff := t.pborder.Height() - prevSize
							newSize = my - t.pborder.Top() - diff + 1 - headerGap
						} else {
							offset := my - t.pborder.Top()
							newSize = prevSize - offset - headerGap
						}
					}
					newSize -= pborderDragging
					if newSize < 1 {
						newSize = 1
					}

					if prevSize == newSize {
						break
					}

					t.activePreviewOpts.size = sizeSpec{float64(newSize), false}
					updatePreviewWindow(true)
					req(reqPreviewRefresh)
					break
				}

				// Inside the input window
				if t.inputWindow != nil && t.inputWindow.Enclose(my, mx) {
					mx -= t.inputWindow.Left()
					my -= t.inputWindow.Top()
					y := t.inputWindow.Height() - 1
					if t.layout == layoutReverse {
						y = 0
					}
					mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
					if my == y && mxCons >= 0 {
						t.cx = mxCons + t.xoffset
					}
					break
				}

				// Inside the header window
				if clicked && t.headerVisible && t.headerWindow != nil && t.headerWindow.Enclose(my, mx) {
					mx -= t.headerWindow.Left() + t.headerIndent(t.headerBorderShape)
					my -= t.headerWindow.Top()
					if mx < 0 {
						break
					}
					t.clickHeaderLine = my + 1
					if t.layout != layoutReverse && t.headerLinesWindow != nil {
						t.clickHeaderLine += t.headerLines
					}
					t.clickHeaderColumn = mx + 1
					return doActions(actionsFor(tui.ClickHeader))
				}

				if clicked && t.headerVisible && t.headerLinesWindow != nil && t.headerLinesWindow.Enclose(my, mx) {
					_, shape := t.determineHeaderLinesShape()
					mx -= t.headerLinesWindow.Left() + t.headerIndent(shape)
					my -= t.headerLinesWindow.Top()
					if mx < 0 {
						break
					}
					t.clickHeaderLine = my + 1
					if t.layout == layoutReverse {
						t.clickHeaderLine += len(t.header0)
					}
					t.clickHeaderColumn = mx + 1
					return doActions(actionsFor(tui.ClickHeader))
				}

				// Inside the footer window
				if clicked && t.footerWindow != nil && t.footerWindow.Enclose(my, mx) {
					mx -= t.footerWindow.Left() + t.headerIndent(t.footerBorderShape)
					my -= t.footerWindow.Top()
					if mx < 0 {
						break
					}
					t.clickFooterLine = my + 1
					t.clickFooterColumn = mx + 1
					return doActions(actionsFor(tui.ClickFooter))
				}

				// Ignored
				if !t.window.Enclose(my, mx) && !barDragging {
					break
				}

				// Translate coordinates
				mx -= t.window.Left()
				my -= t.window.Top()
				min := t.promptLines() + t.visibleHeaderLinesInList()
				h := t.window.Height()
				switch t.layout {
				case layoutDefault:
					my = h - my - 1
				case layoutReverseList:
					if my < h-min {
						my += min
					} else {
						my = h - my - 1
					}
				}

				// Scrollbar dragging
				barDragging = me.Down && (barDragging || click && my >= min && mx == t.window.Width()-1)
				if barDragging {
					barLength, barStart := t.getScrollbar()
					if barLength > 0 {
						maxItems := t.maxItems()
						if newBarStart := util.Constrain(my-min-barLength/2, 0, maxItems-barLength); newBarStart != barStart {
							total := t.merger.Length()
							prevOffset := t.offset
							// barStart = (maxItems - barLength) * t.offset / (total - maxItems)
							perLine := t.avgNumLines()
							t.offset = int(math.Ceil(float64(newBarStart) * float64(total*perLine-maxItems) / float64(maxItems*perLine-barLength)))
							t.cy = t.offset + t.cy - prevOffset
							req(reqList)
						}
					}
					break
				}

				// There can be empty lines after the list in multi-line mode
				prevLine := t.prevLines[my]
				if prevLine.empty {
					break
				}

				// Double-click on an item
				cy := prevLine.cy
				if me.Double && mx < t.window.Width()-1 {
					// Double-click
					if my >= min {
						if t.vset(cy) && t.cy < t.merger.Length() {
							return doActions(actionsFor(tui.DoubleClick))
						}
					}
				}

				if me.Down {
					mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
					if !t.inputless && t.inputWindow == nil && my == 0 && mxCons >= 0 {
						// Prompt
						t.cx = mxCons + t.xoffset
					} else if my >= min {
						t.vset(cy)
						req(reqList)
						evt := tui.RightClick
						if me.Mod() {
							evt = tui.SRightClick
						}
						if me.Left {
							evt = tui.LeftClick
							if me.Mod() {
								evt = tui.SLeftClick
							}
						}
						return doActions(actionsFor(evt))
					}
				}
				if clicked && t.headerVisible && t.headerWindow == nil {
					// Header
					numLines := t.visibleHeaderLinesInList()
					lineOffset := 0
					if !t.inputless && t.inputWindow == nil && !t.headerFirst {
						// offset for info line
						if t.noSeparatorLine() {
							lineOffset = 1
						} else {
							lineOffset = 2
						}
					}
					my -= lineOffset
					mx -= t.pointerLen + t.markerLen
					if my >= 0 && my < numLines && mx >= 0 {
						if t.layout == layoutReverse {
							t.clickHeaderLine = my + 1
						} else {
							t.clickHeaderLine = numLines - my
						}
						t.clickHeaderColumn = mx + 1
						return doActions(actionsFor(tui.ClickHeader))
					}
				}
			case actReload, actReloadSync:
				t.failed = nil

				valid, list := t.buildPlusList(a.a, false)
				if !valid {
					// We run the command even when there's no match
					// 1. If the template doesn't have any slots
					// 2. If the template has {q}
					slot, _, _, forceUpdate := hasPreviewFlags(a.a)
					valid = !slot || forceUpdate
				}
				if valid {
					command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list)
					newCommand = &commandSpec{command, tempFiles}
					reloadSync = a.t == actReloadSync
					t.reading = true

					if len(t.idNth) > 0 {
						t.trackSync = reloadSync
					}
					// Capture tracking key before reload
					if !t.track.Disabled() && len(t.idNth) > 0 {
						if item := t.currentItem(); item != nil {
							t.trackKey = t.trackKeyFor(item, t.idNth)
							t.trackKeyCache = make(map[int32]bool)
							t.trackBlocked = true
							if !t.inputless {
								t.tui.HideCursor()
							}
							req(reqPrompt, reqInfo)
						}
					}
				}
			case actUnbind:
				if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil {
					for key := range keys {
						delete(t.keymap, key)
					}
				}
			case actRebind:
				if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil {
					for key := range keys {
						if originalAction, found := t.keymapOrg[key]; found {
							t.keymap[key] = originalAction
						}
					}
				}
			case actToggleBind:
				if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil {
					for key := range keys {
						if _, bound := t.keymap[key]; bound {
							delete(t.keymap, key)
						} else if originalAction, found := t.keymapOrg[key]; found {
							t.keymap[key] = originalAction
						}
					}
				}
			case actChangeGhost, actTransformGhost, actBgTransformGhost:
				capture(true, func(ghost string) {
					t.ghost = ghost
					if len(t.input) == 0 {
						req(reqPrompt)
					}
				})
			case actChangePointer, actTransformPointer, actBgTransformPointer:
				capture(true, func(pointer string) {
					length := uniseg.StringWidth(pointer)
					if length <= 2 {
						if length != t.pointerLen {
							t.forceRerenderList()
						}
						t.pointer = pointer
						t.pointerLen = length
						t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
						req(reqList)
					}
				})
			case actChangePreview:
				if t.previewOpts.command != a.a {
					t.previewOpts.command = a.a
					updatePreviewWindow(false)
					refreshPreview(t.previewOpts.command)
				}
			case actChangePreviewWindow:
				// NOTE: We intentionally use "previewOpts" instead of "activePreviewOpts" here
				currentPreviewOpts := t.previewOpts
				wasNoSeparatorLine := t.noSeparatorLine()
				wasSeparatorLength := t.separatorLength()

				// Reset preview options and apply the additional options
				t.previewOpts = t.initialPreviewOpts
				t.previewOpts.command = currentPreviewOpts.command

				// Carry over toggle-driven state so toggle-preview-wrap survives
				// a change-preview-window. Tokens below can still override.
				t.previewOpts.wrap = currentPreviewOpts.wrap
				t.previewOpts.wrapWord = currentPreviewOpts.wrapWord

				// Split window options
				tokens := strings.Split(a.a, "|")
				if len(tokens[0]) > 0 && t.initialPreviewOpts.hidden {
					t.previewOpts.hidden = false
				}
				parsePreviewWindow(&t.previewOpts, tokens[0])
				if len(tokens) > 1 {
					a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
				}

				// Do not drop a preview window forced by the preview(...) action
				keepForcedPreview := t.hasPreviewWindow() && !t.activePreviewOpts.hidden

				// The default separator is decided from the whole threshold
				// chain (separatedByBorder), which compare does not fully
				// inspect, so it can change even when compare finds no
				// layout difference
				checkSeparator := func() {
					if t.noSeparatorLine() != wasNoSeparatorLine {
						// Number of lines changed; relayout
						updatePreviewWindow(keepForcedPreview)
						req(reqPreviewRefresh)
					} else if t.separatorLength() != wasSeparatorLength {
						// Only the content of the info line changed
						req(reqInfo)
					}
				}

				// Full redraw
				switch currentPreviewOpts.compare(t.activePreviewOpts, &t.previewOpts) {
				case previewOptsSame:
					checkSeparator()
				case previewOptsDifferentLayout:
					// Preview command can be running in the background if the size of
					// the preview window is 0 but not 'hidden'
					wasHidden := currentPreviewOpts.hidden

					// FIXME: One-time preview window can't reappear once hidden
					// fzf --bind space:preview:ls --bind 'enter:change-preview-window:down|left|up|hidden|'
					updatePreviewWindow(keepForcedPreview)
					if wasHidden && t.hasPreviewWindow() {
						// Restart
						refreshPreview(t.previewOpts.command)
					} else if t.activePreviewOpts.hidden {
						// Cancel
						t.cancelPreview()
					} else {
						// Refresh
						req(reqPreviewRefresh)
					}
				case previewOptsDifferentContentLayout:
					t.previewed.version = 0
					req(reqPreviewRefresh)
					checkSeparator()
				}

				// Adjust scroll offset
				if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.activePreviewOpts.scroll {
					scrollPreviewTo(t.evaluateScrollOffset())
				}

				// Resume following
				t.previewer.following.Force(t.previewOpts.follow)
			case actNextSelected, actPrevSelected:
				if len(t.selected) > 0 {
					total := t.merger.Length()
					for i := 1; i < total; i++ {
						y := (t.cy + i) % total
						if t.layout == layoutDefault && a.t == actNextSelected ||
							t.layout != layoutDefault && a.t == actPrevSelected {
							y = (t.cy - i + total) % total
						}
						if _, found := t.selected[t.merger.Get(y).item.Index()]; found {
							t.vset(y)
							req(reqList)
							break
						}
					}
				}
			}

			if !processExecution(a.t) {
				t.lastAction = a.t
			}

			if t.inputless {
				// Always just discard the change
				t.input = currentInput
				t.cx = len(t.input)
				beof = false
			} else if string(t.input) != string(currentInput) {
				t.inputOverride = nil
			}
			return true
		}

		// Execute pending actions if wait just unblocked. Capture the jump
		// state first so that a pending 'jump' action isn't cancelled right
		// away by the wake-up event below.
		jumpingBefore := t.jumping
		if len(t.wait.pending) > 0 && !t.wait.blocked {
			pending := t.wait.pending
			t.wait.pending = nil
			if !doActions(pending) {
				continue
			}
		}

		if jumpingBefore == jumpDisabled || len(actions) > 0 {
			// Break out of jump mode if any action is submitted to the server
			if jumpingBefore != jumpDisabled {
				t.jumping = jumpDisabled
				if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
					continue
				}
				req(reqList)
			}
			if len(actions) == 0 {
				actions = t.keymap[event.Comparable()]
			}
			if len(actions) == 0 && event.Type == tui.Rune {
				doAction(&action{t: actChar})
			} else if !doActions(actions) {
				continue
			}
			if !t.inputless {
				t.truncateQuery()
			}
			queryChanged = queryChanged || t.pasting == nil && string(previousInput) != string(t.input)
			changed = changed || queryChanged
			if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
				continue
			}
			if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
				continue
			}
			if onMultis, prs := t.keymap[tui.Multi.AsEvent()]; t.version != previousVersion && prs && !doActions(onMultis) {
				continue
			}
		} else {
			jumpEvent := tui.JumpCancel
			if event.Type == tui.Rune {
				if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
					jumpEvent = tui.Jump
					t.cy = idx + t.offset
					if t.jumping == jumpAcceptEnabled {
						req(reqClose)
					}
				}
			}
			t.jumping = jumpDisabled
			if acts, prs := t.keymap[jumpEvent.AsEvent()]; prs && !doActions(acts) {
				continue
			}
			req(reqList)
		}

		if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
			_, _, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
			if forceUpdate {
				t.version++
			}
		}

		if queryChanged || t.cx != previousCx {
			req(reqPrompt)
		}

		reload := changed || newCommand != nil
		if reload {
			t.wait.searching = true
			t.progress = 0
		}
		var reloadRequest *searchRequest
		if reload {
			reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, withNth: newWithNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
		}

		// Dispatch queued background requests
		t.dispatchAsync()

		if t.pendingReqList {
			t.pendingReqList = false
			req(reqList)
		}

		t.mutex.Unlock() // Must be unlocked before touching reqBox

		if reload {
			t.eventBox.Set(EvtSearchNew, *reloadRequest)
		}
		for _, event := range events {
			t.reqBox.Set(event, nil)
		}
	}
	return nil
}

func (t *Terminal) constrain() {
	// count of items to display allowed by filtering
	count := t.merger.Length()
	maxLines := t.maxItems()

	// May need to try again after adjusting the offset
	t.offset = util.Constrain(t.offset, 0, count)
	for range maxLines {
		numItems := maxLines
		// How many items can be fit on screen including the current item?
		if t.canSpanMultiLines() && t.merger.Length() > 0 {
			numItemsFound := 0
			linesSum := 0

			add := func(i int) bool {
				lines, overflow := t.numItemLines(t.merger.Get(i).item, numItems-linesSum)
				linesSum += lines
				if linesSum >= numItems {
					/*
						# Should show all 3 items
						printf "file1\0file2\0file3\0" | fzf --height=5 --read0 --bind load:last --reverse

						# Should not truncate the last item
						printf "file\n1\0file\n2\0file\n3\0" | fzf --height=5 --read0 --bind load:last --reverse
					*/
					if numItemsFound == 0 || !overflow {
						numItemsFound++
					}
					return false
				}
				numItemsFound++
				return true
			}

			for i := t.offset; i < t.merger.Length(); i++ {
				if !add(i) {
					break
				}
			}

			// We can possibly fit more items "before" the offset on screen
			if linesSum < numItems {
				for i := t.offset - 1; i >= 0; i-- {
					if !add(i) {
						break
					}
				}
			}

			numItems = numItemsFound
		}

		t.cy = util.Constrain(t.cy, 0, max(0, count-1))
		minOffset := max(t.cy-numItems+1, 0)
		maxOffset := max(min(count-numItems, t.cy), 0)
		prevOffset := t.offset
		t.offset = util.Constrain(t.offset, minOffset, maxOffset)
		if t.scrollOff > 0 {
			scrollOff := min(maxLines/2, t.scrollOff)
			newOffset := t.offset
			// 2-phase adjustment to avoid infinite loop of alternating between moving up and down
			for phase := range 2 {
				for {
					prevOffset := newOffset
					numItems := t.merger.Length()
					itemLines := 1 + t.gap
					if t.canSpanMultiLines() && t.cy < numItems {
						itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
					}
					linesBefore := t.cy - newOffset
					if t.canSpanMultiLines() {
						linesBefore = 0
						for i := newOffset; i < t.cy && i < numItems; i++ {
							lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)
							linesBefore += lines
						}
					}
					linesAfter := maxLines - (linesBefore + itemLines)

					// Stuck in the middle, nothing to do
					if linesBefore < scrollOff && linesAfter < scrollOff {
						break
					}

					if phase == 0 && linesBefore < scrollOff {
						newOffset = max(minOffset, newOffset-1)
					} else if phase == 1 && linesAfter < scrollOff {
						newOffset = min(maxOffset, newOffset+1)
					}
					if newOffset == prevOffset {
						break
					}
				}
				t.offset = newOffset
			}
		}
		if t.offset == prevOffset {
			break
		}
	}
}

// Returns true if the cursor position is successfully updated
func (t *Terminal) vmove(o int, allowCycle bool) bool {
	if t.layout != layoutDefault {
		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
			}
		}
	}
	return t.vset(dest)
}

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

// Number of prompt lines in the list window
func (t *Terminal) promptLines() int {
	if t.inputless {
		return 0
	}
	if t.inputWindow != nil {
		return 0
	}
	if t.noSeparatorLine() {
		return 1
	}
	return 2
}

// Number of item lines in the list window
func (t *Terminal) maxItems() int {
	maximum := t.window.Height() - t.visibleHeaderLinesInList() - t.promptLines()
	return max(maximum, 0)
}

func (t *Terminal) dumpItem(i *Item) StatusItem {
	if i == nil {
		return StatusItem{}
	}
	item := StatusItem{
		Index: int(i.Index()),
		Text:  i.AsString(t.ansi),
	}
	if t.resultMerger.pattern != nil {
		_, _, pos := t.resultMerger.pattern.MatchItem(i, true, t.slab)
		if pos != nil {
			sort.Ints(*pos)
			item.Positions = *pos
		}
	}
	return item
}

func (t *Terminal) tryLock(timeout time.Duration) bool {
	sleepDuration := 10 * time.Millisecond

	for {
		if t.mutex.TryLock() {
			return true
		}

		timeout -= sleepDuration
		if timeout <= 0 {
			break
		}
		time.Sleep(sleepDuration)
	}
	return false
}

func (t *Terminal) dumpStatus(params getParams) string {
	if !t.tryLock(channelTimeout) {
		return ""
	}
	defer t.mutex.Unlock()

	selectedItems := t.sortSelected()
	selected := make([]StatusItem, max(0, min(params.limit, len(selectedItems)-params.offset)))
	for i := range selected {
		selected[i] = t.dumpItem(selectedItems[i+params.offset].item)
	}

	matches := make([]StatusItem, max(0, min(params.limit, t.resultMerger.Length()-params.offset)))
	for i := range matches {
		matches[i] = t.dumpItem(t.resultMerger.Get(i + params.offset).item)
	}

	var current *StatusItem
	currentItem := t.currentItem()
	if currentItem != nil {
		item := t.dumpItem(currentItem)
		current = &item
	}

	dump := Status{
		Reading:    t.reading,
		Progress:   t.progress,
		Query:      string(t.input),
		Position:   t.cy,
		Sort:       t.sort,
		TotalCount: t.count,
		MatchCount: t.resultMerger.Length(),
		Current:    current,
		Matches:    matches,
		Selected:   selected,
	}
	bytes, _ := json.Marshal(&dump) // TODO: Errors?
	return string(bytes)
}