fzf/src/terminal.go

3783 lines
97 KiB
Go
Raw Normal View History

2015-01-01 19:49:30 +00:00
package fzf
import (
"bufio"
2015-01-01 19:49:30 +00:00
"fmt"
"io"
"math"
2015-01-01 19:49:30 +00:00
"os"
"os/exec"
2015-01-23 11:30:50 +00:00
"os/signal"
2015-01-01 19:49:30 +00:00
"regexp"
"sort"
"strconv"
2015-01-18 07:59:04 +00:00
"strings"
2015-01-01 19:49:30 +00:00
"sync"
2015-01-23 11:30:50 +00:00
"syscall"
2015-01-01 19:49:30 +00:00
"time"
2015-01-12 03:56:17 +00:00
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"github.com/junegunn/fzf/src/tui"
2015-01-12 03:56:17 +00:00
"github.com/junegunn/fzf/src/util"
2015-01-01 19:49:30 +00:00
)
2016-09-07 00:58:18 +00:00
// import "github.com/pkg/profile"
/*
2022-08-12 13:11:15 +00:00
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.
2022-08-12 13:11:15 +00:00
The following pseudo regex has been reverse engineered from the
2022-11-18 01:23:04 +00:00
implementation. It is overly strict, but better describes what's possible.
2022-08-12 13:11:15 +00:00
As such it is not useful for validation, but rather to generate test
cases for example.
\\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type
|{q} # 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 activeTempFiles []string
const clearCode string = "\x1b[2J"
func init() {
Improvements to code quality and readability (#1737) * Remove 1 unused field and 3 unused functions unused elements fount by running golangci-lint run --disable-all --enable unused src/result.go:19:2: field `index` is unused (unused) index int32 ^ src/tui/light.go:716:23: func `(*LightWindow).stderr` is unused (unused) func (w *LightWindow) stderr(str string) { ^ src/terminal.go:1015:6: func `numLinesMax` is unused (unused) func numLinesMax(str string, max int) int { ^ src/tui/tui.go:167:20: func `ColorPair.is24` is unused (unused) func (p ColorPair) is24() bool { ^ * Address warnings from "gosimple" linter src/options.go:389:83: S1003: should use strings.Contains(str, ",,,") instead (gosimple) if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 { ^ src/options.go:630:18: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) executeRegexp = regexp.MustCompile( ^ src/terminal.go:29:16: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})") ^ src/terminal_test.go:92:10: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) regex = regexp.MustCompile("\\w+") ^ * Address warnings from "staticcheck" linter src/algo/algo.go:374:2: SA4006: this value of `offset32` is never used (staticcheck) offset32, T := alloc32(offset32, slab, N) ^ src/algo/algo.go:456:2: SA4006: this value of `offset16` is never used (staticcheck) offset16, C := alloc16(offset16, slab, width*M) ^ src/tui/tui.go:119:2: SA9004: only the first constant in this group has an explicit type (staticcheck) colUndefined Color = -2 ^
2019-11-05 00:46:51 +00:00
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{}
}
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 previewer struct {
2020-10-24 07:55:55 +00:00
version int64
lines []string
offset int
scrollable bool
final bool
following resumableState
spinner string
2023-01-06 06:36:12 +00:00
bar []bool
}
type previewed struct {
2020-10-24 07:55:55 +00:00
version int64
numLines int
offset int
filled bool
}
type eachLine struct {
line string
err error
}
2017-01-07 16:30:31 +00:00
type itemLine struct {
2023-07-25 13:11:15 +00:00
offset int
2017-01-07 17:29:31 +00:00
current bool
selected bool
label string
queryLen int
width int
2023-01-01 05:48:14 +00:00
bar bool
2017-01-07 17:29:31 +00:00
result Result
2017-01-07 16:30:31 +00:00
}
type fitpad struct {
fit int
pad int
}
2017-01-07 16:30:31 +00:00
var emptyLine = itemLine{}
type labelPrinter func(tui.Window, int)
2015-01-11 18:01:24 +00:00
// Terminal represents terminal input/output
2015-01-01 19:49:30 +00:00
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
infoSep string
separator labelPrinter
separatorLen int
spinner []string
prompt func()
promptLen int
borderLabel labelPrinter
borderLabelLen int
borderLabelOpts labelOpts
previewLabel labelPrinter
previewLabelLen int
previewLabelOpts labelOpts
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
2023-04-22 14:39:35 +00:00
track trackOption
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
keymapOrg map[tui.Event][]*action
pressed string
printQuery bool
history *History
cycle bool
2023-07-25 13:11:15 +00:00
headerVisible bool
headerFirst bool
headerLines int
header []string
header0 []string
ellipsis string
2023-01-01 05:48:14 +00:00
scrollbar string
previewScrollbar string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
listenPort *int
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
borderWidth int
count int
progress int
hasLoadActions bool
triggerLoad bool
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
revision int
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
activePreviewOpts *previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan fitpad
killChan chan int
serverChan chan []*action
eventChan chan tui.Event
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
2015-01-01 19:49:30 +00:00
}
type selectedItem struct {
at time.Time
item *Item
}
2015-03-22 07:05:54 +00:00
type byTimeOrder []selectedItem
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Len() int {
return len(a)
}
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
2015-01-01 19:49:30 +00:00
const (
2015-01-12 03:56:17 +00:00
reqPrompt util.EventType = iota
2015-01-11 18:01:24 +00:00
reqInfo
2015-07-21 18:21:20 +00:00
reqHeader
2015-01-11 18:01:24 +00:00
reqList
reqJump
2015-01-11 18:01:24 +00:00
reqRefresh
2017-04-28 13:58:08 +00:00
reqReinit
reqFullRedraw
reqRedrawBorderLabel
reqRedrawPreviewLabel
2015-01-11 18:01:24 +00:00
reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqPreviewRefresh
reqPreviewDelayed
2015-01-11 18:01:24 +00:00
reqQuit
2015-01-01 19:49:30 +00:00
)
type action struct {
t actionType
a string
}
2015-05-20 12:25:15 +00:00
type actionType int
const (
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actAcceptNonEmpty
2015-05-20 12:25:15 +00:00
actBackwardChar
actBackwardDeleteChar
actBackwardDeleteCharEOF
2015-05-20 12:25:15 +00:00
actBackwardWord
2015-07-23 12:05:33 +00:00
actCancel
actChangeBorderLabel
actChangeHeader
actChangePreviewLabel
2020-12-04 11:34:41 +00:00
actChangePrompt
2022-12-17 09:59:16 +00:00
actChangeQuery
2015-05-20 12:25:15 +00:00
actClearScreen
actClearQuery
actClearSelection
2021-02-01 15:08:54 +00:00
actClose
2015-05-20 12:25:15 +00:00
actDeleteChar
2015-08-02 04:06:15 +00:00
actDeleteCharEOF
2015-05-20 12:25:15 +00:00
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleSearch
actToggleAll
2015-05-20 12:25:15 +00:00
actToggleDown
actToggleUp
actToggleIn
actToggleOut
2023-04-22 06:48:51 +00:00
actToggleTrack
2023-07-25 13:11:15 +00:00
actToggleHeader
2023-04-22 14:39:35 +00:00
actTrack
2015-05-20 12:25:15 +00:00
actDown
actUp
actPageUp
actPageDown
actPosition
2017-01-16 02:58:13 +00:00
actHalfPageUp
actHalfPageDown
actJump
actJumpAccept
actPrintQuery
2020-06-20 13:04:09 +00:00
actRefreshPreview
actReplaceQuery
2015-05-20 12:25:15 +00:00
actToggleSort
actShowPreview
actHidePreview
actTogglePreview
2017-02-18 14:49:00 +00:00
actTogglePreviewWrap
actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel
2022-12-31 00:27:11 +00:00
actTransformPrompt
actTransformQuery
actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviewHalfPageUp
actPreviewHalfPageDown
actPrevHistory
actPrevSelected
actPut
actNextHistory
actNextSelected
actExecute
2017-01-27 08:46:56 +00:00
actExecuteSilent
actExecuteMulti // Deprecated
2017-04-28 13:58:08 +00:00
actSigStop
actFirst
actLast
actReload
2022-12-29 11:03:51 +00:00
actReloadSync
actDisableSearch
actEnableSearch
actSelect
actDeselect
2021-05-22 04:13:55 +00:00
actUnbind
actRebind
actBecome
2015-05-20 12:25:15 +00:00
)
type placeholderFlags struct {
plus bool
preserveSpace bool
number bool
query bool
file bool
}
type searchRequest struct {
sort bool
2022-12-29 11:03:51 +00:00
sync bool
command *string
changed bool
}
type previewRequest struct {
template string
pwindow tui.Window
scrollOffset int
list []*Item
}
type previewResult struct {
2020-10-24 07:55:55 +00:00
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.Invalid, actInvalid)
add(tui.Resize, actClearScreen)
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.CtrlH, actBackwardDeleteChar)
add(tui.BSpace, actBackwardDeleteChar)
add(tui.Tab, actToggleDown)
add(tui.BTab, actToggleUp)
add(tui.CtrlJ, actDown)
add(tui.CtrlK, actUp)
add(tui.CtrlL, actClearScreen)
add(tui.CtrlM, actAccept)
add(tui.CtrlN, actDown)
add(tui.CtrlP, actUp)
add(tui.CtrlU, actUnixLineDiscard)
add(tui.CtrlW, actUnixWordRubout)
add(tui.CtrlY, actYank)
2017-04-28 13:58:08 +00:00
if !util.IsWindows() {
add(tui.CtrlZ, actSigStop)
}
addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.SLeft, actBackwardWord)
addEvent(tui.AltKey('f'), actForwardWord)
add(tui.SRight, actForwardWord)
addEvent(tui.AltKey('d'), actKillWord)
add(tui.AltBS, 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.Del, actDeleteChar)
add(tui.PgUp, actPageUp)
add(tui.PgDn, actPageDown)
add(tui.SUp, actPreviewUp)
add(tui.SDown, actPreviewDown)
add(tui.Mouse, actMouse)
add(tui.LeftClick, actIgnore)
add(tui.RightClick, actToggle)
2015-05-20 12:25:15 +00:00
return keymap
}
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
if action.t == actPreview || action.t == actChangePreview {
return true
}
}
}
return false
}
func makeSpinner(unicode bool) []string {
if unicode {
return []string{``, ``, ``, ``, ``, ``, ``, ``, ``, ``}
}
return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
}
func evaluateHeight(opts *Options, termHeight int) int {
if opts.Height.percent {
return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
}
return int(opts.Height.size)
}
2015-01-11 18:01:24 +00:00
// NewTerminal returns new Terminal object
2015-01-12 03:56:17 +00:00
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := trimQuery(opts.Query)
var header []string
switch opts.Layout {
case layoutDefault, layoutReverseList:
header = reverseStringArray(opts.Header)
default:
header = opts.Header
}
var delay time.Duration
if opts.Tac {
delay = initialDelayTac
} else {
delay = initialDelay
}
var previewBox *util.EventBox
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
previewBox = util.NewEventBox()
}
2017-01-07 16:30:31 +00:00
var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
2017-04-28 13:58:08 +00:00
if fullscreen {
if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h })
}
} else {
2017-01-07 16:30:31 +00:00
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)
2017-01-07 16:30:31 +00:00
}
2023-06-10 15:04:24 +00:00
if opts.InfoStyle.noExtraLine() {
effectiveMinHeight--
}
effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
2017-01-07 16:30:31 +00:00
}
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
2017-01-07 16:30:31 +00:00
}
wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
2017-01-15 10:42:28 +00:00
if opts.FileWord {
sep := regexp.QuoteMeta(string(os.PathSeparator))
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
keymapCopy := make(map[tui.Event][]*action)
for key, action := range opts.Keymap {
keymapCopy[key] = action
}
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
infoSep: opts.InfoSep,
separator: nil,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
track: opts.Track,
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,
listenPort: opts.ListenPort,
borderShape: opts.BorderShape,
borderWidth: 1,
borderLabel: nil,
borderLabelOpts: opts.BorderLabel,
previewLabel: nil,
previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
cycle: opts.Cycle,
2023-07-25 13:11:15 +00:00
headerVisible: true,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: []string{},
header0: header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
hasLoadActions: false,
triggerLoad: false,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger(0),
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview,
2023-02-01 09:16:58 +00:00
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan fitpad, 1),
killChan: make(chan int),
serverChan: make(chan []*action, 10),
eventChan: make(chan tui.Event, 1),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
// Pre-calculated empty pointer and marker signs
t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
t.markerEmpty = strings.Repeat(" ", t.markerLen)
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false)
2022-12-09 03:05:27 +00:00
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColPreviewLabel, false)
if opts.Separator == nil || len(*opts.Separator) > 0 {
bar := "─"
if opts.Separator != nil {
bar = *opts.Separator
} else if !t.unicode {
bar = "-"
}
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
if t.unicode {
t.borderWidth = runewidth.RuneWidth('│')
}
2023-01-01 05:48:14 +00:00
if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 {
t.scrollbar = "│"
2023-01-01 05:48:14 +00:00
} else {
t.scrollbar = "|"
}
t.previewScrollbar = t.scrollbar
2023-01-01 05:48:14 +00:00
} 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])
}
}
2023-01-01 05:48:14 +00:00
}
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenPort != nil {
err, port := startHttpServer(*t.listenPort, t.serverChan)
if err != nil {
errorExit(err.Error())
}
t.listenPort = &port
}
return &t
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) environ() []string {
env := os.Environ()
if t.listenPort != nil {
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
}
return env
}
func borderLines(shape tui.BorderShape) int {
switch shape {
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
return 2
case tui.BorderTop, tui.BorderBottom:
return 1
}
return 0
}
2023-07-25 13:11:15 +00:00
func (t *Terminal) visibleHeaderLines() int {
if !t.headerVisible {
return 0
}
return len(t.header0) + t.headerLines
}
// Extra number of lines needed to display fzf
func (t *Terminal) extraLines() int {
2023-07-25 13:11:15 +00:00
extra := t.visibleHeaderLines() + 1
if !t.noInfoLine() {
extra++
}
return extra
}
func (t *Terminal) MaxFitAndPad(opts *Options) (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(str, '\t') {
length := util.StringWidth(str)
if length == 0 {
return nil, 0
}
printFn := func(window tui.Window, limit int) {
if length > limit {
trimmedRunes, _ := t.trimRight(runes, limit)
window.CPrint(*color, string(trimmedRunes))
} else if fill {
window.CPrint(*color, util.RepeatToFill(str, length, limit))
} else {
window.CPrint(*color, str)
}
}
return printFn, len(text)
}
// 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, t.theme, *color, *color, 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
}
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{-1, -1, tui.AttrClear, -1}}
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() {
t.printHighlighted(
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false)
}
_, promptLen := t.processTabs([]rune(trimmed), 0)
return output, promptLen
}
func (t *Terminal) noInfoLine() bool {
2023-06-10 15:04:24 +00:00
return t.infoStyle.noExtraLine()
}
2023-01-06 06:36:12 +00:00
func getScrollbar(total int, height int, offset int) (int, int) {
if total == 0 || total <= height {
2023-01-01 05:48:14 +00:00
return 0, 0
}
2023-01-06 06:36:12 +00:00
barLength := util.Max(1, height*height/total)
2023-01-01 05:48:14 +00:00
var barStart int
2023-01-06 06:36:12 +00:00
if total == height {
2023-01-01 05:48:14 +00:00
barStart = 0
} else {
2023-01-06 06:36:12 +00:00
barStart = (height - barLength) * offset / (total - height)
2023-01-01 05:48:14 +00:00
}
return barLength, barStart
}
2023-01-06 06:36:12 +00:00
func (t *Terminal) getScrollbar() (int, int) {
return getScrollbar(t.merger.Length(), t.maxItems(), t.offset)
}
2015-01-11 18:01:24 +00:00
// Input returns current query string
func (t *Terminal) Input() (bool, []rune) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
defer t.mutex.Unlock()
return t.paused, copySlice(t.input)
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
t.count = cnt
if t.hasLoadActions && t.reading && final {
t.triggerLoad = true
}
2015-01-01 19:49:30 +00:00
t.reading = !final
t.failed = failedCommand
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
if final {
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqRefresh, nil)
}
2015-01-01 19:49:30 +00:00
}
func reverseStringArray(input []string) []string {
size := len(input)
reversed := make([]string, size)
for idx, str := range input {
reversed[size-idx-1] = str
}
return reversed
}
func (t *Terminal) changeHeader(header string) bool {
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
switch t.layout {
case layoutDefault, layoutReverseList:
lines = reverseStringArray(lines)
}
needFullRedraw := len(t.header0) != len(lines)
t.header0 = lines
return needFullRedraw
}
2015-07-21 18:21:20 +00:00
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) {
2015-07-21 18:21:20 +00:00
t.mutex.Lock()
t.header = header
2015-07-21 18:21:20 +00:00
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
2015-01-11 18:01:24 +00:00
// UpdateProgress updates the search progress
2015-01-01 19:49:30 +00:00
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
changed := t.progress != newProgress
t.progress = newProgress
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
if changed {
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
}
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
var prevIndex int32 = -1
reset := t.revision != merger.Revision()
2023-04-22 14:39:35 +00:00
if !reset && t.track != trackDisabled {
if t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index()
} else if merger.Length() > 0 {
prevIndex = merger.First().item.Index()
}
}
2015-01-01 19:49:30 +00:00
t.progress = 100
t.merger = merger
if reset {
t.selected = make(map[int32]selectedItem)
t.revision = merger.Revision()
t.version++
}
2023-04-30 16:55:14 +00:00
if t.triggerLoad {
t.triggerLoad = false
t.eventChan <- tui.Load.AsEvent()
}
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
2023-04-22 14:39:35 +00:00
} else if t.track == trackCurrent {
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 - util.Min(count, t.maxItems()) + pos
}
}
2023-04-26 06:13:08 +00:00
if !t.reading {
switch t.merger.Length() {
case 0:
zero := tui.Zero.AsEvent()
if _, prs := t.keymap[zero]; prs {
t.eventChan <- zero
}
case 1:
one := tui.One.AsEvent()
if _, prs := t.keymap[one]; prs {
t.eventChan <- one
}
}
}
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) output() bool {
2015-01-01 19:49:30 +00:00
if t.printQuery {
2016-09-17 19:52:47 +00:00
t.printer(string(t.input))
2015-01-01 19:49:30 +00:00
}
if len(t.expect) > 0 {
2016-09-17 19:52:47 +00:00
t.printer(t.pressed)
}
found := len(t.selected) > 0
if !found {
current := t.currentItem()
if current != nil {
t.printer(current.AsString(t.ansi))
found = true
2015-01-01 19:49:30 +00:00
}
} else {
2015-11-08 16:42:01 +00:00
for _, sel := range t.sortSelected() {
t.printer(sel.item.AsString(t.ansi))
2015-01-01 19:49:30 +00:00
}
}
return found
2015-01-01 19:49:30 +00:00
}
2015-11-08 16:42:01 +00:00
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
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width
2015-01-01 19:49:30 +00:00
}
const (
minWidth = 4
minHeight = 3
)
2015-07-26 14:02:04 +00:00
2020-10-06 10:37:33 +00:00
func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad 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)
}
2020-10-06 10:37:33 +00:00
return util.Constrain(int(size.size)+pad, minSize, max)
}
func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
2017-01-07 16:30:31 +00:00
screenWidth := t.tui.MaxX()
screenHeight := t.tui.MaxY()
2020-11-09 11:34:08 +00:00
marginInt := [4]int{} // TRBL
paddingInt := [4]int{} // TRBL
sizeSpecToInt := func(index int, spec sizeSpec) int {
if spec.percent {
var max float64
2020-11-09 11:34:08 +00:00
if index%2 == 0 {
max = float64(screenHeight)
2015-07-26 14:02:04 +00:00
} else {
max = float64(screenWidth)
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
return int(max * spec.size * 0.01)
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
return int(spec.size)
}
for idx, sizeSpec := range t.padding {
paddingInt[idx] = sizeSpecToInt(idx, sizeSpec)
}
bw := t.borderWidth
2020-11-09 11:34:08 +00:00
extraMargin := [4]int{} // TRBL
for idx, sizeSpec := range t.margin {
switch t.borderShape {
case tui.BorderHorizontal:
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 1 - idx%2
case tui.BorderVertical:
extraMargin[idx] += (1 + bw) * (idx % 2)
case tui.BorderTop:
if idx == 0 {
2020-11-09 11:34:08 +00:00
extraMargin[idx]++
}
case tui.BorderRight:
if idx == 1 {
extraMargin[idx] += 1 + bw
}
case tui.BorderBottom:
if idx == 2 {
2020-11-09 11:34:08 +00:00
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:
extraMargin[idx] += 1 + bw*(idx%2)
}
2020-11-09 11:34:08 +00:00
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
2015-07-26 14:02:04 +00:00
adjust := func(idx1 int, idx2 int, max int, min int) {
if min > max {
min = max
}
margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2]
if max-margin < min {
desired := max - min
paddingInt[idx1] = desired * paddingInt[idx1] / margin
paddingInt[idx2] = desired * paddingInt[idx2] / margin
marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin)
marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin)
2015-07-26 14:02:04 +00:00
}
}
minAreaWidth := minWidth
minAreaHeight := minHeight
if t.noInfoLine() {
minAreaHeight -= 1
}
2023-02-01 09:16:58 +00:00
if t.needPreviewWindow() {
minPreviewHeight := 1 + borderLines(t.previewOpts.border)
minPreviewWidth := 5
2020-12-05 12:16:35 +00:00
switch t.previewOpts.position {
case posUp, posDown:
minAreaHeight += minPreviewHeight
minAreaWidth = util.Max(minPreviewWidth, minAreaWidth)
case posLeft, posRight:
minAreaWidth += minPreviewWidth
minAreaHeight = util.Max(minPreviewHeight, minAreaHeight)
}
}
adjust(1, 3, screenWidth, minAreaWidth)
adjust(0, 2, screenHeight, minAreaHeight)
return screenWidth, screenHeight, marginInt, paddingInt
}
func (t *Terminal) resizeWindows(forcePreview bool) {
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
t.prevLines = make([]itemLine, screenHeight)
if t.border != nil {
t.border.Close()
}
if t.window != nil {
t.window.Close()
t.window = nil
}
if t.pborder != nil {
t.pborder.Close()
t.pborder = nil
}
if t.pwindow != nil {
t.pwindow.Close()
t.pwindow = nil
}
// Reset preview version so that full redraw occurs
t.previewed.version = 0
2023-01-16 10:34:28 +00:00
bw := t.borderWidth
switch t.borderShape {
case tui.BorderHorizontal:
t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3], width, height+2,
false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode))
case tui.BorderVertical:
t.border = t.tui.NewWindow(
2023-01-16 10:34:28 +00:00
marginInt[0], marginInt[3]-(1+bw), width+(1+bw)*2, height,
false, tui.MakeBorderStyle(tui.BorderVertical, t.unicode))
case tui.BorderTop:
t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3], width, height+1,
false, tui.MakeBorderStyle(tui.BorderTop, t.unicode))
case tui.BorderBottom:
t.border = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height+1,
false, tui.MakeBorderStyle(tui.BorderBottom, t.unicode))
case tui.BorderLeft:
t.border = t.tui.NewWindow(
2023-01-16 10:34:28 +00:00
marginInt[0], marginInt[3]-(1+bw), width+(1+bw), height,
false, tui.MakeBorderStyle(tui.BorderLeft, t.unicode))
case tui.BorderRight:
t.border = t.tui.NewWindow(
2023-01-16 10:34:28 +00:00
marginInt[0], marginInt[3], width+(1+bw), height,
false, tui.MakeBorderStyle(tui.BorderRight, t.unicode))
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
t.border = t.tui.NewWindow(
2023-01-16 10:34:28 +00:00
marginInt[0]-1, marginInt[3]-(1+bw), width+(1+bw)*2, height+2,
false, tui.MakeBorderStyle(t.borderShape, t.unicode))
}
2020-11-09 11:34:08 +00:00
// Add padding to margin
2020-11-09 11:34:08 +00:00
for idx, val := range paddingInt {
marginInt[idx] += val
}
width -= paddingInt[1] + paddingInt[3]
height -= paddingInt[0] + paddingInt[2]
2020-11-09 11:34:08 +00:00
// Set up preview window
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
2023-02-01 09:16:58 +00:00
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
var previewBorder tui.BorderStyle
if previewOpts.border == tui.BorderNone {
previewBorder = tui.MakeTransparentBorder()
} else {
previewBorder = tui.MakeBorderStyle(previewOpts.border, t.unicode)
}
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
switch previewOpts.border {
case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
pwidth -= (1 + bw) * 2
pheight -= 2
x += 1 + bw
y += 1
case tui.BorderLeft:
pwidth -= 1 + bw
x += 1 + bw
case tui.BorderRight:
pwidth -= 1 + bw
case tui.BorderTop:
pheight -= 1
y += 1
case tui.BorderBottom:
pheight -= 1
case tui.BorderHorizontal:
pheight -= 2
y += 1
case tui.BorderVertical:
pwidth -= (1 + bw) * 2
x += 1 + bw
}
2023-01-06 06:36:12 +00:00
if len(t.scrollbar) > 0 && !previewOpts.border.HasRight() {
// Need a column to show scrollbar
pwidth -= 1
}
pwidth = util.Max(0, pwidth)
pheight = util.Max(0, pheight)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
}
verticalPad := 2
minPreviewHeight := 3
switch previewOpts.border {
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
verticalPad = 0
minPreviewHeight = 1
case tui.BorderTop, tui.BorderBottom:
verticalPad = 1
minPreviewHeight = 2
}
switch previewOpts.position {
case posUp, posDown:
pheight := calculateSize(height, previewOpts.size, minHeight, minPreviewHeight, verticalPad)
if hasThreshold && pheight < previewOpts.threshold {
t.activePreviewOpts = previewOpts.alternative
if forcePreview {
previewOpts.alternative.hidden = false
}
2022-07-20 03:29:45 +00:00
if !previewOpts.alternative.hidden {
resizePreviewWindows(previewOpts.alternative)
2022-07-20 03:29:45 +00:00
}
return
}
if forcePreview {
previewOpts.hidden = false
}
if previewOpts.hidden {
return
}
2023-01-06 06:36:12 +00:00
// Put scrollbar closer to the right border for consistent look
if t.borderShape.HasRight() {
width++
}
if previewOpts.position == posUp {
t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
} else {
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
}
case posLeft, posRight:
pwidth := calculateSize(width, previewOpts.size, minWidth, 5, 4)
if hasThreshold && pwidth < previewOpts.threshold {
t.activePreviewOpts = previewOpts.alternative
if forcePreview {
previewOpts.alternative.hidden = false
}
2022-07-20 03:29:45 +00:00
if !previewOpts.alternative.hidden {
resizePreviewWindows(previewOpts.alternative)
2022-07-20 03:29:45 +00:00
}
return
}
if forcePreview {
previewOpts.hidden = false
}
if previewOpts.hidden {
return
}
if previewOpts.position == posLeft {
2023-01-06 06:36:12 +00:00
// Put scrollbar closer to the right border for consistent look
if t.borderShape.HasRight() {
width++
}
// Add a 1-column margin between the preview window and the main window
t.window = t.tui.NewWindow(
2023-01-06 06:36:12 +00:00
marginInt[0], marginInt[3]+pwidth+1, width-pwidth-1, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
} else {
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
2023-01-06 06:36:12 +00:00
// NOTE: fzf --preview 'cat {}' --preview-window border-left --border
x := marginInt[3] + width - pwidth
if !previewOpts.border.HasRight() && t.borderShape.HasRight() {
pwidth++
}
createPreviewWindow(marginInt[0], x, pwidth, height)
}
}
}
resizePreviewWindows(&t.previewOpts)
} else {
t.activePreviewOpts = &t.previewOpts
}
2023-01-06 06:36:12 +00:00
// Without preview window
if t.window == nil {
2023-01-06 06:36:12 +00:00
if t.borderShape.HasRight() {
// Put scrollbar closer to the right border for consistent look
width++
}
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false, noBorder)
}
// Print border label
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false)
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
2015-07-26 14:02:04 +00:00
}
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
if window == nil {
return
}
switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
if redrawBorder {
window.DrawHBorder()
}
if render == nil {
return
}
var col int
if opts.column == 0 {
col = util.Max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = util.Max(0, window.Width()+opts.column+1-length)
} else {
col = util.Min(opts.column-1, window.Width()-length)
}
row := 0
if borderShape == tui.BorderBottom || opts.bottom {
row = window.Height() - 1
}
window.Move(row, col)
render(window, window.Width())
}
}
2015-01-01 19:49:30 +00:00
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:
2023-07-25 13:11:15 +00:00
n := 2 + t.visibleHeaderLines()
if t.noInfoLine() {
n--
}
if y < n {
y = h - y - 1
} else {
y -= n
}
2015-01-01 19:49:30 +00:00
}
if clear {
t.window.MoveAndClear(y, x)
2015-01-01 19:49:30 +00:00
} else {
t.window.Move(y, x)
2015-01-01 19:49:30 +00:00
}
}
func (t *Terminal) truncateQuery() {
t.input, _ = t.trimRight(t.input, maxPatternLength)
t.cx = util.Constrain(t.cx, 0, len(t.input))
}
func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
maxWidth := util.Max(1, t.window.Width()-t.promptLen-1)
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
minOffset := int(overflow)
maxOffset := minOffset + (maxWidth-util.Max(0, maxWidth-t.cx))/2
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
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) promptLine() int {
if t.headerFirst {
max := t.window.Height() - 1
if !t.noInfoLine() {
max--
}
2023-07-25 13:11:15 +00:00
return util.Min(t.visibleHeaderLines(), max)
}
return 0
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) placeCursor() {
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printPrompt() {
t.move(t.promptLine(), 0, true)
t.prompt()
before, after := t.updatePromptOffset()
color := tui.ColInput
if t.paused {
color = tui.ColDisabled
}
t.window.CPrint(color, string(before))
t.window.CPrint(color, string(after))
2015-01-01 19:49:30 +00:00
}
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))
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) printInfo() {
2017-02-18 14:17:29 +00:00
pos := 0
line := t.promptLine()
2023-06-10 14:11:05 +00:00
printSpinner := func() {
if t.reading {
duration := int64(spinnerDuration)
2020-01-15 01:43:09 +00:00
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
} else {
t.window.Print(" ") // Clear spinner
}
2023-06-10 14:11:05 +00:00
}
switch t.infoStyle {
case infoDefault:
t.move(line+1, 0, t.separatorLen == 0)
printSpinner()
t.move(line+1, 2, false)
pos = 2
2023-06-10 15:04:24 +00:00
case infoRight:
t.move(line+1, 0, false)
2023-06-10 14:11:05 +00:00
case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
t.move(line, pos, true)
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
str := t.infoSep
maxWidth := t.window.Width() - pos
width := util.StringWidth(str)
if width > maxWidth {
trimmed, _ := t.trimRight([]rune(str), maxWidth)
str = string(trimmed)
width = maxWidth
2017-02-18 14:17:29 +00:00
}
t.move(line, pos, t.separatorLen == 0)
2015-04-21 14:50:53 +00:00
if t.reading {
t.window.CPrint(tui.ColSpinner, str)
2015-04-21 14:50:53 +00:00
} else {
t.window.CPrint(tui.ColPrompt, str)
2015-04-21 14:50:53 +00:00
}
pos += width
case infoHidden:
return
2015-01-01 19:49:30 +00:00
}
found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
2015-05-20 12:25:15 +00:00
if t.toggleSort {
if t.sort {
output += " +S"
} else {
output += " -S"
}
}
2023-04-22 14:39:35 +00:00
if t.track != trackDisabled {
2023-04-22 06:48:51 +00:00
output += " +T"
}
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)
}
2015-01-01 19:49:30 +00:00
}
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)
}
2023-06-10 15:04:24 +00:00
printSeparator := func(fillLength int, pad bool) {
// --------_
if t.separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
} else if pad {
t.window.Print(strings.Repeat(" ", fillLength+1))
}
}
if t.infoStyle == infoRight {
maxWidth := t.window.Width()
if t.reading {
// Need space for spinner and a margin column
maxWidth -= 2
}
output = t.trimMessage(output, maxWidth)
fillLength := t.window.Width() - len(output) - 2
if t.reading {
if fillLength >= 2 {
printSeparator(fillLength-2, true)
}
printSpinner()
t.window.Print(" ")
} else if fillLength >= 0 {
printSeparator(fillLength, true)
}
t.window.CPrint(tui.ColInfo, output)
return
}
2023-06-10 14:11:05 +00:00
if t.infoStyle == infoInlineRight {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
if pos >= t.window.Width() {
return
}
t.move(line, pos, false)
printSpinner()
t.window.Print(" ")
pos += 2
}
2023-06-10 15:04:24 +00:00
maxWidth := t.window.Width() - pos
output = t.trimMessage(output, maxWidth)
t.window.CPrint(tui.ColInfo, output)
fillLength := maxWidth - len(output) - 2
2023-06-10 15:04:24 +00:00
if fillLength > 0 {
t.window.CPrint(tui.ColSeparator, " ")
2023-06-10 15:04:24 +00:00
printSeparator(fillLength, false)
}
2015-07-26 14:02:04 +00:00
}
2015-07-21 15:19:37 +00:00
func (t *Terminal) printHeader() {
2023-07-25 13:11:15 +00:00
if t.visibleHeaderLines() == 0 {
2015-07-21 15:19:37 +00:00
return
}
2017-01-07 16:30:31 +00:00
max := t.window.Height()
if t.headerFirst {
max--
if !t.noInfoLine() {
max--
}
}
var state *ansiState
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx
if !t.headerFirst {
line++
if !t.noInfoLine() {
line++
}
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState
2015-07-21 15:19:37 +00:00
item := &Item{
text: util.ToChars([]byte(trimmed)),
colors: colors}
2015-07-21 15:19:37 +00:00
2023-07-25 13:11:15 +00:00
t.move(line, 0, true)
t.window.Print(" ")
t.printHighlighted(Result{item: item},
tui.ColHeader, tui.ColHeader, false, false)
2015-07-21 15:19:37 +00:00
}
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) printList() {
t.constrain()
2023-01-01 05:48:14 +00:00
barLength, barStart := t.getScrollbar()
2015-01-01 19:49:30 +00:00
2015-04-21 14:50:53 +00:00
maxy := t.maxItems()
count := t.merger.Length() - t.offset
2017-01-07 16:30:31 +00:00
for j := 0; j < maxy; j++ {
i := j
if t.layout == layoutDefault {
2017-01-07 16:30:31 +00:00
i = maxy - 1 - j
}
2023-07-25 13:11:15 +00:00
line := i + 2 + t.visibleHeaderLines()
if t.noInfoLine() {
2015-08-02 04:06:15 +00:00
line--
2015-04-21 14:50:53 +00:00
}
2015-01-01 19:49:30 +00:00
if i < count {
2023-01-01 05:48:14 +00:00
t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
2023-07-25 13:11:15 +00:00
} else if t.prevLines[i] != emptyLine || t.prevLines[i].offset != line {
2017-01-07 16:30:31 +00:00
t.prevLines[i] = emptyLine
t.move(line, 0, true)
2015-01-01 19:49:30 +00:00
}
}
}
2023-01-01 05:48:14 +00:00
func (t *Terminal) printItem(result Result, line int, i int, current bool, bar bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := ""
if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) {
// Striped
current = i%2 == 0
label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
}
} else if current {
label = t.pointer
}
2017-01-07 16:30:31 +00:00
// Avoid unnecessary redraw
2023-07-25 13:11:15 +00:00
newLine := itemLine{offset: line, current: current, selected: selected, label: label,
2023-01-01 05:48:14 +00:00
result: result, queryLen: len(t.input), width: 0, bar: bar}
prevLine := t.prevLines[i]
2023-07-25 13:11:15 +00:00
forceRedraw := prevLine.offset != newLine.offset
2023-01-01 12:16:09 +00:00
printBar := func() {
2023-07-25 13:11:15 +00:00
if len(t.scrollbar) > 0 && (bar != prevLine.bar || forceRedraw) {
2023-01-01 16:10:01 +00:00
t.prevLines[i].bar = bar
2023-01-01 12:16:09 +00:00
t.move(line, t.window.Width()-1, true)
if bar {
t.window.CPrint(tui.ColScrollbar, t.scrollbar)
}
}
}
2023-07-25 13:11:15 +00:00
if !forceRedraw &&
prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
2023-01-01 12:16:09 +00:00
printBar()
2017-01-07 16:30:31 +00:00
return
}
2023-07-25 13:11:15 +00:00
t.move(line, 0, forceRedraw)
2015-01-01 19:49:30 +00:00
if current {
if len(label) == 0 {
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCurrentCursor, label)
}
2015-01-01 19:49:30 +00:00
if selected {
t.window.CPrint(tui.ColCurrentSelected, t.marker)
2015-01-01 19:49:30 +00:00
} else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
2015-01-01 19:49:30 +00:00
} else {
if len(label) == 0 {
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCursor, label)
}
2015-01-01 19:49:30 +00:00
if selected {
t.window.CPrint(tui.ColSelected, t.marker)
2015-01-01 19:49:30 +00:00
} else {
t.window.Print(t.markerEmpty)
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true)
}
fillSpaces := prevLine.width - newLine.width
if fillSpaces > 0 {
t.window.Print(strings.Repeat(" ", fillSpaces))
2015-01-01 19:49:30 +00:00
}
2023-01-01 12:16:09 +00:00
printBar()
t.prevLines[i] = newLine
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
2015-01-18 07:59:04 +00:00
// We start from the beginning to handle tab characters
_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 {
return runes[:overflowIdx], true
2015-01-18 07:59:04 +00:00
}
return runes, false
2015-01-18 07:59:04 +00:00
}
2015-01-01 19:49:30 +00:00
2017-01-07 16:30:31 +00:00
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
return width
2015-01-01 19:49:30 +00:00
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
width = util.Max(0, width)
var trimmed int32
// Assume that each rune takes at least one column on screen
if len(runes) > width+2 {
diff := len(runes) - width - 2
trimmed = int32(diff)
runes = runes[diff:]
}
2017-01-07 16:30:31 +00:00
currentWidth := t.displayWidth(runes)
2015-01-01 19:49:30 +00:00
for currentWidth > width && len(runes) > 0 {
runes = runes[1:]
2015-01-11 18:01:24 +00:00
trimmed++
2017-01-07 16:30:31 +00:00
currentWidth = t.displayWidthWithLimit(runes, 2, width)
2015-01-01 19:49:30 +00:00
}
return runes, trimmed
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
2016-08-14 08:44:11 +00:00
}
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int {
item := result.item
2015-01-01 19:49:30 +00:00
// Overflow
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
2016-08-19 16:46:54 +00:00
matchOffsets := []Offset{}
2016-09-07 00:58:18 +00:00
var pos *[]int
if match && t.merger.pattern != nil {
2016-09-07 00:58:18 +00:00
_, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab)
}
charOffsets := matchOffsets
if pos != nil {
charOffsets = make([]Offset, len(*pos))
for idx, p := range *pos {
offset := Offset{int32(p), int32(p + 1)}
charOffsets[idx] = offset
}
sort.Sort(ByOrder(charOffsets))
2016-08-19 16:46:54 +00:00
}
var maxe int
2016-09-07 00:58:18 +00:00
for _, offset := range charOffsets {
2016-08-19 16:46:54 +00:00
maxe = util.Max(maxe, int(offset[1]))
}
2016-09-07 00:58:18 +00:00
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
2020-09-24 02:06:20 +00:00
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth {
transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
el := int32(len(ellipsis))
b += el - diff
e += el - diff
b = util.Max32(b, el)
if rightTrim {
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
}
if t.hscroll {
if t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
transformOffsets(diff, false)
text = append(ellipsis, trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
// Stri..
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, ellipsis...)
} else {
// Stri..
rightTrim := false
if t.overflow(text[maxe:], ellipsisWidth) {
text = append(text[:maxe], ellipsis...)
rightTrim = true
}
// ..ri..
var diff int32
text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
// Transform offsets
transformOffsets(diff, rightTrim)
text = append(ellipsis, text...)
2015-01-01 19:49:30 +00:00
}
} else {
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, ellipsis...)
2015-01-01 19:49:30 +00:00
2015-03-18 16:59:14 +00:00
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
2015-01-01 19:49:30 +00:00
}
}
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
2015-01-01 19:49:30 +00:00
}
t.printColoredString(t.window, text, offsets, colBase)
return displayWidth
}
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
2015-01-11 18:01:24 +00:00
var index int32
2015-01-18 07:59:04 +00:00
var substr string
var prefixWidth int
2015-03-18 16:59:14 +00:00
maxOffset := int32(len(text))
2015-01-01 19:49:30 +00:00
for _, offset := range offsets {
2015-03-18 16:59:14 +00:00
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
2015-01-18 07:59:04 +00:00
2017-01-07 16:30:31 +00:00
substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
window.CPrint(colBase, substr)
2015-01-18 07:59:04 +00:00
2015-03-19 03:14:26 +00:00
if b < e {
2017-01-07 16:30:31 +00:00
substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
window.CPrint(offset.color, substr)
2015-03-19 03:14:26 +00:00
}
2015-01-18 07:59:04 +00:00
2015-01-01 19:49:30 +00:00
index = e
2015-03-18 16:59:14 +00:00
if index >= maxOffset {
break
}
2015-01-01 19:49:30 +00:00
}
2015-03-18 16:59:14 +00:00
if index < maxOffset {
2017-01-07 16:30:31 +00:00
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 {
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 unchanged {
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else {
t.previewed.filled = false
t.pwindow.Erase()
}
height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines
headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) {
header = t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
if !unchanged {
t.pwindow.FinishFill()
}
2023-01-06 06:36:12 +00:00
if len(t.scrollbar) == 0 {
return
}
effectiveHeight := height - headerLines
barLength, barStart := getScrollbar(len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
t.renderPreviewScrollbar(headerLines, barLength, barStart)
}
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState
spinnerRedraw := t.pwindow.Y() == 0
for _, line := range lines {
var lbg tui.Color = -1
if ansi != nil {
ansi.lbg = -1
}
line = strings.TrimRight(line, "\r\n")
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 {
if spinnerRedraw && lineNo > 0 {
spinnerRedraw = false
y := t.pwindow.Y()
x := t.pwindow.X()
t.renderPreviewSpinner()
t.pwindow.Move(y, x)
}
var fillRet tui.FillReturn
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
isTrimmed := false
2020-12-05 12:16:35 +00:00
if !t.previewOpts.wrap {
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, width := t.processTabs(trimmed, prefixWidth)
if width > prefixWidth {
prefixWidth = width
if t.theme.Colored && ansi != nil && ansi.colored() {
lbg = ansi.lbg
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
}
return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
})
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
2019-03-29 08:29:24 +00:00
if fillRet == tui.FillNextLine {
continue
2019-03-29 08:29:24 +00:00
} else if fillRet == tui.FillSuspend {
t.previewed.filled = true
break
}
if unchanged && lineNo == 0 {
break
2017-01-07 16:30:31 +00:00
}
if lbg >= 0 {
t.pwindow.CFill(-1, lbg, tui.AttrRegular,
strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
} else {
t.pwindow.Fill("\n")
}
}
lineNo++
}
2015-01-18 07:59:04 +00:00
}
2023-01-06 06:36:12 +00:00
func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
height := t.pwindow.Height()
w := t.pborder.Width()
redraw := false
2023-01-06 06:36:12 +00:00
if len(t.previewer.bar) != height {
redraw = true
2023-01-06 06:36:12 +00:00
t.previewer.bar = make([]bool, height)
}
xshift := -1 - t.borderWidth
2023-01-06 06:36:12 +00:00
if !t.previewOpts.border.HasRight() {
xshift = -1
}
yshift := 1
if !t.previewOpts.border.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() {
2023-01-06 06:36:12 +00:00
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)
2023-01-06 06:36:12 +00:00
} else {
t.pborder.CPrint(tui.ColPreviewScrollbar, " ")
2023-01-06 06:36:12 +00:00
}
}
}
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 > 0 || 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)
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf strings.Builder
2015-01-18 07:59:04 +00:00
l := prefixWidth
gr := uniseg.NewGraphemes(string(runes))
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
2015-01-18 07:59:04 +00:00
strbuf.WriteString(strings.Repeat(" ", w))
} else {
w = util.StringWidth(str)
strbuf.WriteString(str)
2015-01-18 07:59:04 +00:00
}
l += w
2015-01-01 19:49:30 +00:00
}
2015-01-18 07:59:04 +00:00
return strbuf.String(), l
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printAll() {
t.resizeWindows(false)
2015-01-01 19:49:30 +00:00
t.printList()
t.printPrompt()
t.printInfo()
2015-07-21 15:47:14 +00:00
t.printHeader()
t.printPreview()
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) refresh() {
2019-03-29 17:06:54 +00:00
t.placeCursor()
if !t.suppress {
windows := make([]tui.Window, 0, 4)
if t.borderShape != tui.BorderNone {
windows = append(windows, t.border)
}
if t.hasPreviewWindow() {
if t.pborder != nil {
windows = append(windows, t.pborder)
}
windows = append(windows, t.pwindow)
}
windows = append(windows, t.window)
t.tui.RefreshWindows(windows)
}
2015-01-01 19:49:30 +00:00
}
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)
2015-01-01 19:49:30 +00:00
}
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)
2015-01-01 19:49:30 +00:00
}
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
2015-03-31 13:05:02 +00:00
}
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags := placeholderFlags{}
if match[0] == '\\' {
// Escaped placeholder pattern
return true, match[1:], flags
}
skipChars := 1
for _, char := range match[1:] {
switch char {
case '+':
flags.plus = true
skipChars++
case 's':
flags.preserveSpace = true
skipChars++
case 'n':
flags.number = true
skipChars++
case 'f':
flags.file = true
skipChars++
case 'q':
flags.query = true
// query flag is not skipped
default:
break
}
}
matchWithoutFlags := "{" + match[skipChars:]
return false, matchWithoutFlags, flags
}
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) {
for _, match := range placeholder.FindAllString(template, -1) {
_, _, flags := parsePlaceholder(match)
if flags.plus {
plus = true
}
if flags.query {
query = true
}
slot = true
}
return
}
func writeTemporaryFile(data []string, printSep string) string {
2023-07-16 08:14:22 +00:00
f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil {
errorExit("Unable to create temporary file")
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
activeTempFiles = append(activeTempFiles, f.Name())
return f.Name()
}
func cleanTemporaryFiles() {
for _, filename := range activeTempFiles {
os.Remove(filename)
}
activeTempFiles = []string{}
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder(
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
}
return n
}
base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.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 util.Max(0, base)
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
// replace placeholders one by one
return placeholder.ReplaceAllStringFunc(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}":
return quoteEntry(query)
case match == "{}":
replace = func(item *Item) string {
switch {
case flags.number:
n := int(item.text.Index)
if n < 0 {
return ""
}
return strconv.Itoa(n)
case flags.file:
return item.AsString(stripAnsi)
default:
return quoteEntry(item.AsString(stripAnsi))
}
}
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(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := joinTokens(trans)
// trim the last delimiter
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := 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 {
str = quoteEntry(str)
}
return str
}
}
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || forcePlus {
items = selected
}
replacements := make([]string, len(items))
for idx, item := range items {
replacements[idx] = replace(item)
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ")
})
}
func (t *Terminal) redraw() {
t.tui.Clear()
2017-04-28 13:58:08 +00:00
t.tui.Refresh()
t.printAll()
}
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
line := ""
valid, list := t.buildPlusList(template, forcePlus)
// 'capture' is used for transform-* and we don't want to
2022-12-31 00:27:11 +00:00
// return an empty string in those cases
if !valid && !capture {
return line
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
2017-01-27 08:46:56 +00:00
if !background {
cmd.Stdin = tui.TtyIn()
2017-01-27 08:46:56 +00:00
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
2017-04-28 13:58:08 +00:00
t.tui.Pause(true)
2017-01-27 08:46:56 +00:00
cmd.Run()
t.tui.Resume(true, false)
t.redraw()
2017-01-27 08:46:56 +00:00
t.refresh()
} else {
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()
}
2017-01-27 08:46:56 +00:00
}
t.executing.Set(false)
cleanTemporaryFiles()
return line
}
func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil
}
2023-02-01 09:16:58 +00:00
func (t *Terminal) needPreviewWindow() bool {
return t.hasPreviewer() && len(t.previewOpts.command) > 0 && t.previewOpts.Visible()
}
// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
2023-02-01 09:16:58 +00:00
func (t *Terminal) canPreview() bool {
return t.hasPreviewer() && (!t.previewOpts.Visible() && !t.previewOpts.hidden || t.hasPreviewWindow())
}
func (t *Terminal) hasPreviewWindow() bool {
return t.pwindow != nil
}
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) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
current := t.currentItem()
slot, plus, query := hasPreviewFlags(template)
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current}
}
// We would still want to update preview window even if there is no match if
2023-07-05 05:55:53 +00:00
// 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 sels []*Item
if len(t.selected) == 0 {
sels = []*Item{current, current}
} else {
sels = make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
}
}
return true, sels
}
func (t *Terminal) selectItem(item *Item) bool {
if len(t.selected) >= t.multi {
return false
}
if _, found := t.selected[item.Index()]; found {
2019-11-11 14:31:31 +00:00
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(code int) {
select {
case t.killChan <- code:
default:
if code != exitCancel {
t.eventBox.Set(EvtQuit, code)
}
}
}
func (t *Terminal) cancelPreview() {
t.killPreview(exitCancel)
}
2015-01-11 18:01:24 +00:00
// Loop is called to start Terminal I/O
2015-01-01 19:49:30 +00:00
func (t *Terminal) Loop() {
2016-09-07 00:58:18 +00:00
// 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()
2023-02-01 09:16:58 +00:00
if t.needPreviewWindow() {
if t.previewOpts.aboveOrBelow() {
if t.previewOpts.size.percent {
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size))
contentHeight = util.Max(contentHeight+1+borderLines(t.previewOpts.border), newContentHeight)
} else {
contentHeight += int(t.previewOpts.size.size) + borderLines(t.previewOpts.border)
}
} else {
// Minimum height if preview window can appear
contentHeight = util.Max(contentHeight, 1+borderLines(t.previewOpts.border))
}
}
return util.Min(termHeight, contentHeight+pad)
})
}
2015-01-01 19:49:30 +00:00
{ // Late initialization
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() {
for s := range 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)
}
}
}()
2017-04-28 13:58:08 +00:00
contChan := make(chan os.Signal, 1)
notifyOnCont(contChan)
go func() {
for {
<-contChan
t.reqBox.Set(reqReinit, nil)
}
}()
2015-01-23 11:30:50 +00:00
resizeChan := make(chan os.Signal, 1)
notifyOnResize(resizeChan) // Non-portable
2015-01-23 11:30:50 +00:00
go func() {
for {
<-resizeChan
t.reqBox.Set(reqFullRedraw, nil)
2015-01-23 11:30:50 +00:00
}
}()
t.mutex.Lock()
t.initFunc()
t.resizeWindows(false)
t.printPrompt()
t.printInfo()
t.printHeader()
t.refresh()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(t.initDelay)
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
time.Sleep(spinnerDuration)
if reading {
t.reqBox.Set(reqInfo, nil)
}
}
}()
2015-01-01 19:49:30 +00:00
}
if t.hasPreviewer() {
go func() {
2020-10-24 07:55:55 +00:00
var version int64
for {
var items []*Item
var commandTemplate string
var pwindow tui.Window
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list
pwindow = request.pwindow
}
}
events.Clear()
})
version++
// We don't display preview window if no match
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true)
env := t.environ()
if pwindow != nil {
height := pwindow.Height()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
}
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)
2020-10-24 07:55:55 +00:00
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
}
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 <-timer.C:
t.reqBox.Set(reqPreviewDelayed, version)
case code := <-t.killChan:
if code != exitCancel {
util.KillCommand(cmd)
t.eventBox.Set(EvtQuit, code)
} 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
} else {
// Failed to start the command. Report the error immediately.
t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
}
cleanTemporaryFiles()
} else {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
}
}
}()
}
refreshPreview := func(command string) {
2023-02-01 09:16:58 +00:00
if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false)
2020-06-20 13:04:09 +00:00
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
2020-06-20 13:04:09 +00:00
}
}
2015-01-01 19:49:30 +00:00
go func() {
var focusedIndex int32 = minItem.Index()
var version int64 = -1
2021-03-07 12:43:24 +00:00
running := true
2021-03-07 15:08:10 +00:00
code := exitError
exit := func(getCode func() int) {
t.tui.Close()
code = getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
running = false
t.mutex.Unlock()
}
2021-03-07 12:43:24 +00:00
for running {
2015-01-12 03:56:17 +00:00
t.reqBox.Wait(func(events *util.Events) {
2015-01-01 19:49:30 +00:00
defer events.Clear()
t.mutex.Lock()
for req, value := range *events {
2015-01-01 19:49:30 +00:00
switch req {
2015-01-11 18:01:24 +00:00
case reqPrompt:
2015-01-01 19:49:30 +00:00
t.printPrompt()
if t.noInfoLine() {
2015-04-21 14:50:53 +00:00
t.printInfo()
}
2015-01-11 18:01:24 +00:00
case reqInfo:
2015-01-01 19:49:30 +00:00
t.printInfo()
2015-01-11 18:01:24 +00:00
case reqList:
2015-01-01 19:49:30 +00:00
t.printList()
var currentIndex int32 = minItem.Index()
currentItem := t.currentItem()
if currentItem != nil {
currentIndex = currentItem.Index()
}
focusChanged := focusedIndex != currentIndex
2023-04-22 14:39:35 +00:00
if focusChanged && t.track == trackCurrent {
t.track = trackDisabled
t.printInfo()
}
2023-05-17 01:55:12 +00:00
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
t.serverChan <- onFocus
}
if focusChanged || version != t.version {
version = t.version
focusedIndex = currentIndex
2020-12-05 12:16:35 +00:00
refreshPreview(t.previewOpts.command)
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
}
t.printList()
2015-07-21 18:21:20 +00:00
case reqHeader:
t.printHeader()
2015-01-11 18:01:24 +00:00
case reqRefresh:
t.suppress = false
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.previewOpts.border, true)
2017-04-28 13:58:08 +00:00
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
case reqFullRedraw:
wasHidden := t.pwindow == nil
t.redraw()
2023-02-01 09:16:58 +00:00
if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command)
}
2015-01-11 18:01:24 +00:00
case reqClose:
2021-03-07 15:08:10 +00:00
exit(func() int {
if t.output() {
return exitOk
}
return exitNoMatch
})
return
case reqPreviewDisplay:
result := value.(previewResult)
2020-12-05 12:16:35 +00:00
if t.previewer.version != result.version {
t.previewer.version = result.version
t.previewer.following.Force(t.previewOpts.follow)
if t.previewer.following.Enabled() {
t.previewer.offset = 0
}
2020-12-05 12:16:35 +00:00
}
t.previewer.lines = result.lines
t.previewer.spinner = result.spinner
if t.previewer.following.Enabled() {
t.previewer.offset = util.Max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.previewOpts.headerLines))
2020-12-05 12:16:35 +00:00
} else if result.offset >= 0 {
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
}
t.printPreview()
case reqPreviewRefresh:
t.printPreview()
case reqPreviewDelayed:
2020-10-24 07:55:55 +00:00
t.previewer.version = value.(int64)
t.printPreviewDelayed()
case reqPrintQuery:
2021-03-07 15:08:10 +00:00
exit(func() int {
t.printer(string(t.input))
return exitOk
})
return
2015-01-11 18:01:24 +00:00
case reqQuit:
2021-03-07 15:08:10 +00:00
exit(func() int { return exitInterrupt })
return
2015-01-01 19:49:30 +00:00
}
}
t.refresh()
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
})
}
2021-03-07 15:08:10 +00:00
// prof.Stop()
t.killPreview(code)
2015-01-01 19:49:30 +00:00
}()
looping := true
_, startEvent := t.keymap[tui.Start.AsEvent()]
needBarrier := true
barrier := make(chan bool)
go func() {
for {
<-barrier
t.eventChan <- t.tui.GetChar()
}
}()
2023-01-02 16:46:37 +00:00
previewDraggingPos := -1
barDragging := false
2023-01-06 06:36:12 +00:00
pbarDragging := false
2023-01-02 16:46:37 +00:00
wasDown := false
2015-01-01 19:49:30 +00:00
for looping {
var newCommand *string
2022-12-29 11:03:51 +00:00
var reloadSync bool
changed := false
2020-06-07 14:07:03 +00:00
beof := false
queryChanged := false
var event tui.Event
actions := []*action{}
if startEvent {
event = tui.Start.AsEvent()
startEvent = false
} else {
if needBarrier {
barrier <- true
}
select {
case event = <-t.eventChan:
2023-04-26 06:13:08 +00:00
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
case actions = <-t.serverChan:
event = tui.Invalid.AsEvent()
needBarrier = false
}
}
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
previousInput := t.input
2019-03-29 06:02:31 +00:00
previousCx := t.cx
events := []util.EventType{}
2015-01-12 03:56:17 +00:00
req := func(evts ...util.EventType) {
2015-01-01 19:49:30 +00:00
for _, event := range evts {
events = append(events, event)
2015-01-11 18:01:24 +00:00
if event == reqClose || event == reqQuit {
2015-01-01 19:49:30 +00:00
looping = false
}
}
}
updatePreviewWindow := func(forcePreview bool) {
t.resizeWindows(forcePreview)
req(reqPrompt, reqList, reqInfo, reqHeader)
}
toggle := func() bool {
current := t.currentItem()
if current != nil && t.toggleItem(current) {
2015-01-11 18:01:24 +00:00
req(reqInfo)
return true
}
return false
}
scrollPreviewTo := func(newOffset int) {
if !t.previewer.scrollable {
return
}
numLines := len(t.previewer.lines)
headerLines := t.previewOpts.headerLines
2020-12-05 12:16:35 +00:00
if t.previewOpts.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)
}
for key, ret := range t.expect {
2015-03-31 13:05:02 +00:00
if keyMatch(key, event) {
t.pressed = ret
t.reqBox.Set(reqClose, nil)
t.mutex.Unlock()
return
}
}
2015-05-20 12:25:15 +00:00
actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()]
}
var doAction func(*action) bool
doActions := func(actions []*action) bool {
for _, action := range actions {
if !doAction(action) {
return false
}
}
return true
}
doAction = func(a *action) bool {
switch a.t {
2015-10-12 17:24:38 +00:00
case actIgnore:
case actBecome:
valid, list := t.buildPlusList(a.a, false)
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
shellPath, err := exec.LookPath(shell)
if err == nil {
t.tui.Close()
if t.history != nil {
t.history.append(string(t.input))
}
/*
FIXME: It is not at all clear why this is required.
The following command will report 'not a tty', unless we open
/dev/tty *twice* after closing the standard input for 'reload'
in Reader.terminate().
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
*/
tui.TtyIn()
2023-02-14 14:21:34 +00:00
util.SetStdin(tui.TtyIn())
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
}
}
2017-01-27 08:46:56 +00:00
case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
2015-11-08 16:42:01 +00:00
case actExecuteMulti:
t.executeCommand(a.a, true, false, false, false)
2015-10-12 17:24:38 +00:00
case actInvalid:
t.mutex.Unlock()
return false
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)
2023-02-01 09:16:58 +00:00
if t.canPreview() {
valid, list := t.buildPlusList(t.previewOpts.command, false)
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
}
2017-02-18 14:49:00 +00:00
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
2020-12-05 12:16:35 +00:00
t.previewOpts.wrap = !t.previewOpts.wrap
// Reset preview version so that full redraw occurs
t.previewed.version = 0
2017-02-18 14:49:00 +00:00
req(reqPreviewRefresh)
}
2022-12-31 00:27:11 +00:00
case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true, true)
2022-12-31 00:27:11 +00:00
t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt)
case actTransformQuery:
query := t.executeCommand(a.a, false, true, true, true)
t.input = []rune(query)
t.cx = len(t.input)
2015-10-12 17:24:38 +00:00
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)
}
2015-10-12 17:24:38 +00:00
case actBeginningOfLine:
2015-07-23 12:05:33 +00:00
t.cx = 0
2015-10-12 17:24:38 +00:00
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actPrintQuery:
req(reqPrintQuery)
2022-12-17 09:59:16 +00:00
case actChangeQuery:
t.input = []rune(a.a)
t.cx = len(t.input)
case actTransformHeader:
header := t.executeCommand(a.a, false, true, true, false)
if t.changeHeader(header) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeHeader:
if t.changeHeader(a.a) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeBorderLabel:
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actChangePreviewLabel:
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actTransformBorderLabel:
if t.border != nil {
label := t.executeCommand(a.a, false, true, true, true)
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actTransformPreviewLabel:
if t.pborder != nil {
label := t.executeCommand(a.a, false, true, true, true)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
2020-12-04 11:34:41 +00:00
case actChangePrompt:
t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt)
case actPreview:
updatePreviewWindow(true)
refreshPreview(a.a)
2020-06-20 13:04:09 +00:00
case actRefreshPreview:
2020-12-05 12:16:35 +00:00
refreshPreview(t.previewOpts.command)
case actReplaceQuery:
current := t.currentItem()
if current != nil {
t.input = current.text.ToRunes()
t.cx = len(t.input)
}
2015-10-12 17:24:38 +00:00
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
2015-10-12 17:24:38 +00:00
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--
}
2015-10-12 17:24:38 +00:00
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi > 0 {
2015-10-12 17:24:38 +00:00
for i := 0; i < t.merger.Length(); i++ {
if !t.selectItem(t.merger.Get(i).item) {
break
}
2015-10-12 17:24:38 +00:00
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi > 0 {
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
2019-11-02 10:55:05 +00:00
t.deselectItem(t.merger.Get(i).item)
}
2015-10-12 17:24:38 +00:00
req(reqList, reqInfo)
}
2021-02-01 15:08:54 +00:00
case actClose:
if t.hasPreviewWindow() {
t.activePreviewOpts.Toggle()
updatePreviewWindow(false)
2021-02-01 15:08:54 +00:00
} 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)
}
2015-10-12 17:24:38 +00:00
case actToggle:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actToggleAll:
if t.multi > 0 {
prevIndexes := make(map[int]struct{})
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
item := t.merger.Get(i).item
if _, found := t.selected[item.Index()]; found {
prevIndexes[i] = struct{}{}
t.deselectItem(item)
}
}
2015-10-12 17:24:38 +00:00
for i := 0; i < t.merger.Length(); i++ {
if _, found := prevIndexes[i]; !found {
item := t.merger.Get(i).item
if !t.selectItem(item) {
break
}
}
2015-10-12 17:24:38 +00:00
}
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})
2015-10-12 17:24:38 +00:00
case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actToggleUp:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(1, true)
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actDown:
t.vmove(-1, true)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-10-12 17:24:38 +00:00
case actUp:
t.vmove(1, true)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-10-12 17:24:38 +00:00
case actAccept:
req(reqClose)
case actAcceptNonEmpty:
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
req(reqClose)
}
2015-10-12 17:24:38 +00:00
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:
t.vset(0)
req(reqList)
case actLast:
t.vset(t.merger.Length() - 1)
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)
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)
2015-10-12 17:24:38 +00:00
case actUnixLineDiscard:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
2015-07-26 15:06:44 +00:00
}
2015-10-12 17:24:38 +00:00
case actUnixWordRubout:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.rubout("\\s\\S")
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
case actBackwardKillWord:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
2017-01-15 10:42:28 +00:00
t.rubout(t.wordRubout)
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems()-1, false)
2015-10-12 17:24:38 +00:00
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1), false)
2015-10-12 17:24:38 +00:00
req(reqList)
2017-01-16 02:58:13 +00:00
case actHalfPageUp:
t.vmove(t.maxItems()/2, false)
2017-01-16 02:58:13 +00:00
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2), false)
2017-01-16 02:58:13 +00:00
req(reqList)
case actJump:
t.jumping = jumpEnabled
req(reqJump)
case actJumpAccept:
t.jumping = jumpAcceptEnabled
req(reqJump)
2015-10-12 17:24:38 +00:00
case actBackwardWord:
2017-01-15 10:42:28 +00:00
t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1
2015-10-12 17:24:38 +00:00
case actForwardWord:
2017-01-15 10:42:28 +00:00
t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
2015-10-12 17:24:38 +00:00
case actKillWord:
ncx := t.cx +
2017-01-15 10:42:28 +00:00
findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
2015-10-12 17:24:38 +00:00
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPrevHistory:
2015-10-12 17:24:38 +00:00
if t.history != nil {
t.history.override(string(t.input))
t.input = trimQuery(t.history.previous())
2015-10-12 17:24:38 +00:00
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = trimQuery(t.history.next())
2015-10-12 17:24:38 +00:00
t.cx = len(t.input)
}
case actToggleSearch:
t.paused = !t.paused
changed = !t.paused
req(reqPrompt)
2023-04-22 06:48:51 +00:00
case actToggleTrack:
2023-04-22 14:39:35 +00:00
switch t.track {
case trackEnabled:
t.track = trackDisabled
case trackDisabled:
t.track = trackEnabled
}
req(reqInfo)
2023-07-25 13:11:15 +00:00
case actToggleHeader:
t.headerVisible = !t.headerVisible
req(reqList, reqInfo, reqPrompt, reqHeader)
2023-04-22 14:39:35 +00:00
case actTrack:
if t.track == trackDisabled {
t.track = trackCurrent
}
2023-04-22 06:48:51 +00:00
req(reqInfo)
case actEnableSearch:
t.paused = false
changed = true
req(reqPrompt)
case actDisableSearch:
t.paused = true
req(reqPrompt)
2017-04-28 13:58:08 +00:00
case actSigStop:
p, err := os.FindProcess(os.Getpid())
if err == nil {
t.sigstop = true
2017-04-28 13:58:08 +00:00
t.tui.Clear()
t.tui.Pause(t.fullscreen)
notifyStop(p)
t.mutex.Unlock()
return false
}
2015-10-12 17:24:38 +00:00
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
2023-01-02 16:46:37 +00:00
clicked := !wasDown && me.Down
wasDown = me.Down
if !me.Down {
barDragging = false
2023-01-06 06:36:12 +00:00
pbarDragging = false
2023-01-02 16:46:37 +00:00
previewDraggingPos = -1
}
// Scrolling
2015-10-12 17:24:38 +00:00
if me.S != 0 {
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
if t.multi > 0 && me.Mod {
2015-07-26 14:02:04 +00:00
toggle()
}
t.vmove(me.S, true)
2015-07-26 14:02:04 +00:00
req(reqList)
} else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
scrollPreviewBy(-me.S)
2015-01-01 19:49:30 +00:00
}
2023-01-02 16:46:37 +00:00
break
}
// Preview dragging
2023-01-06 06:36:12 +00:00
if me.Down && (previewDraggingPos >= 0 || clicked && t.hasPreviewWindow() && t.pwindow.Enclose(my, mx)) {
2023-01-02 16:46:37 +00:00
if previewDraggingPos > 0 {
scrollPreviewBy(previewDraggingPos - my)
2015-10-12 17:24:38 +00:00
}
2023-01-02 16:46:37 +00:00
previewDraggingPos = my
break
}
2023-02-22 15:36:04 +00:00
// Preview scrollbar dragging
2023-01-06 06:36:12 +00:00
headerLines := t.previewOpts.headerLines
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && 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(numLines, effectiveHeight, util.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)
2023-01-06 06:36:12 +00:00
req(reqPreviewRefresh)
}
break
}
2023-01-02 16:46:37 +00:00
// Ignored
if !t.window.Enclose(my, mx) && !barDragging {
break
}
// Translate coordinates
mx -= t.window.Left()
my -= t.window.Top()
2023-07-25 13:11:15 +00:00
min := 2 + t.visibleHeaderLines()
2023-01-02 16:46:37 +00:00
if t.noInfoLine() {
min--
}
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
2023-01-02 16:46:37 +00:00
}
}
// Scrollbar dragging
barDragging = me.Down && (barDragging || clicked && 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)
t.offset = int(math.Ceil(float64(newBarStart) * float64(total-maxItems) / float64(maxItems-barLength)))
t.cy = t.offset + t.cy - prevOffset
req(reqList)
}
}
2023-01-02 16:46:37 +00:00
break
}
// Double-click on an item
if me.Double && mx < t.window.Width()-1 {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doActions(actionsFor(tui.DoubleClick))
2023-01-01 09:50:01 +00:00
}
2023-01-02 16:46:37 +00:00
}
break
}
if me.Down {
mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
if my == t.promptLine() && mx >= 0 {
// Prompt
t.cx = mx + t.xoffset
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod {
toggle()
2023-01-01 05:48:14 +00:00
}
2023-01-02 16:46:37 +00:00
req(reqList)
if me.Left {
return doActions(actionsFor(tui.LeftClick))
2015-10-12 17:24:38 +00:00
}
2023-01-02 16:46:37 +00:00
return doActions(actionsFor(tui.RightClick))
2015-10-12 17:24:38 +00:00
}
2015-01-01 19:49:30 +00:00
}
2022-12-29 11:03:51 +00:00
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, _, query := hasPreviewFlags(a.a)
valid = !slot || query
}
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
2022-12-29 11:03:51 +00:00
reloadSync = a.t == actReloadSync
t.reading = true
}
2021-05-22 04:13:55 +00:00
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
delete(t.keymap, key)
}
case actRebind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
if originalAction, found := t.keymapOrg[key]; found {
t.keymap[key] = originalAction
}
}
case actChangePreview:
if t.previewOpts.command != a.a {
t.previewOpts.command = a.a
2023-02-01 09:16:58 +00:00
updatePreviewWindow(false)
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
// 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]), "|")
}
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
wasHidden := t.pwindow == nil
updatePreviewWindow(false)
2023-02-01 09:16:58 +00:00
if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command)
} else {
req(reqPreviewRefresh)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.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
}
}
}
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
return true
}
if t.jumping == jumpDisabled || len(actions) > 0 {
// Break out of jump mode if any action is submitted to the server
if t.jumping != jumpDisabled {
t.jumping = jumpDisabled
req(reqList)
}
if len(actions) == 0 {
actions = t.keymap[event.Comparable()]
}
if len(actions) == 0 && event.Type == tui.Rune {
doAction(&action{t: actRune})
} else if !doActions(actions) {
continue
}
2017-02-18 14:17:29 +00:00
t.truncateQuery()
queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs {
if !doActions(onChanges) {
continue
}
}
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs {
if !doActions(onEOFs) {
2020-06-07 14:07:03 +00:00
continue
}
}
} else {
if event.Type == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled {
req(reqClose)
}
}
}
t.jumping = jumpDisabled
req(reqList)
}
2015-01-01 19:49:30 +00:00
2023-02-01 09:16:58 +00:00
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
_, _, q := hasPreviewFlags(t.previewOpts.command)
if q {
t.version++
}
2015-01-01 19:49:30 +00:00
}
2019-03-29 06:02:31 +00:00
if queryChanged || t.cx != previousCx {
2019-03-29 06:02:31 +00:00
req(reqPrompt)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
2019-03-29 17:06:54 +00:00
if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
2019-03-29 17:06:54 +00:00
}
2015-01-01 19:49:30 +00:00
for _, event := range events {
t.reqBox.Set(event, nil)
}
}
}
func (t *Terminal) constrain() {
// count of items to display allowed by filtering
count := t.merger.Length()
// count of lines can be displayed
2015-04-21 14:50:53 +00:00
height := t.maxItems()
2015-01-01 19:49:30 +00:00
2015-01-12 03:56:17 +00:00
t.cy = util.Constrain(t.cy, 0, count-1)
minOffset := util.Max(t.cy-height+1, 0)
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
if t.scrollOff == 0 {
return
}
scrollOff := util.Min(height/2, t.scrollOff)
for {
prevOffset := t.offset
if t.cy-t.offset < scrollOff {
t.offset = util.Max(minOffset, t.offset-1)
}
if t.cy-t.offset >= height-scrollOff {
t.offset = util.Min(maxOffset, t.offset+1)
}
if t.offset == prevOffset {
break
}
}
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) vmove(o int, allowCycle 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
}
}
2015-01-01 19:49:30 +00:00
}
t.vset(dest)
2015-01-10 05:50:24 +00:00
}
func (t *Terminal) vset(o int) bool {
2015-01-12 03:56:17 +00:00
t.cy = util.Constrain(o, 0, t.merger.Length()-1)
2015-01-10 05:50:24 +00:00
return t.cy == o
2015-01-01 19:49:30 +00:00
}
2015-04-21 14:50:53 +00:00
func (t *Terminal) maxItems() int {
2023-07-25 13:11:15 +00:00
max := t.window.Height() - 2 - t.visibleHeaderLines()
if t.noInfoLine() {
2015-08-02 04:06:15 +00:00
max++
2015-04-21 14:50:53 +00:00
}
return util.Max(max, 0)
2015-01-01 19:49:30 +00:00
}