You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fzf/src/terminal.go

4295 lines
112 KiB
Go

10 years ago
package fzf
import (
"bufio"
"encoding/json"
10 years ago
"fmt"
"io"
"math"
10 years ago
"os"
"os/signal"
10 years ago
"regexp"
"sort"
"strconv"
"strings"
10 years ago
"sync"
"syscall"
10 years ago
"time"
"github.com/rivo/uniseg"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
10 years ago
)
// import "github.com/pkg/profile"
/*
Placeholder regex is used to extract placeholders from fzf's template
strings. Acts as input validation for parsePlaceholder function.
Describes the syntax, but it is fairly lenient.
The following pseudo regex has been reverse engineered from the
implementation. It is overly strict, but better describes what's possible.
As such it is not useful for validation, but rather to generate test
cases for example.
\\?(?: # escaped type
{\+?s?f?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
Experimental support for Kitty image protocol in preview window Close #3228 * Works inside and outside of tmux * There is a problem where fzf unnecessarily displays the scroll offset indicator at the topbright of the screen when the image just fits the preview window. This is because `kitty icat` generates an extra line after the image area. # A 5-row images; an extra row at the end confuses fzf ["\e_Ga ... \e[9C􎻮̅̅ࠪ􎻮̅̍ࠪ􎻮̅̎ࠪ􎻮̅̐ࠪ􎻮̅̒ࠪ􎻮̅̽ࠪ􎻮̅̾ࠪ􎻮̅̿ࠪ􎻮̅͆ࠪ􎻮̅͊ࠪ􎻮̅͋ࠪ\n", "\r\e[9C􎻮̍̅ࠪ􎻮̍̍ࠪ􎻮̍̎ࠪ􎻮̍̐ࠪ􎻮̍̒ࠪ􎻮̍̽ࠪ􎻮̍̾ࠪ􎻮̍̿ࠪ􎻮̍͆ࠪ􎻮̍͊ࠪ􎻮̍͋ࠪ\n", "\r\e[9C􎻮̎̅ࠪ􎻮̎̍ࠪ􎻮̎̎ࠪ􎻮̎̐ࠪ􎻮̎̒ࠪ􎻮̎̽ࠪ􎻮̎̾ࠪ􎻮̎̿ࠪ􎻮̎͆ࠪ􎻮̎͊ࠪ􎻮̎͋ࠪ\n", "\r\e[9C􎻮̐̅ࠪ􎻮̐̍ࠪ􎻮̐̎ࠪ􎻮̐̐ࠪ􎻮̐̒ࠪ􎻮̐̽ࠪ􎻮̐̾ࠪ􎻮̐̿ࠪ􎻮̐͆ࠪ􎻮̐͊ࠪ􎻮̐͋ࠪ\n", "\r\e[9C􎻮̒̅ࠪ􎻮̒̍ࠪ􎻮̒̎ࠪ􎻮̒̐ࠪ􎻮̒̒ࠪ􎻮̒̽ࠪ􎻮̒̾ࠪ􎻮̒̿ࠪ􎻮̒͆ࠪ􎻮̒͊ࠪ􎻮̒͋ࠪ\n", "\r\e[39m\e8"] * Example: fzf --preview=' if file --mime-type {} | grep -qF 'image/'; then # --transfer-mode=memory is the fastest option but if you want fzf to be able # to redraw the image on terminal resize or on 'change-preview-window', # you need to use --transfer-mode=stream. kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} else bat --color=always {} fi '
7 months ago
var passThroughRegex *regexp.Regexp
var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J"
// Number of maximum focus events to process synchronously
const maxFocusEvents = 10000
func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{}
Experimental support for Kitty image protocol in preview window Close #3228 * Works inside and outside of tmux * There is a problem where fzf unnecessarily displays the scroll offset indicator at the topbright of the screen when the image just fits the preview window. This is because `kitty icat` generates an extra line after the image area. # A 5-row images; an extra row at the end confuses fzf ["\e_Ga ... \e[9C􎻮̅̅ࠪ􎻮̅̍ࠪ􎻮̅̎ࠪ􎻮̅̐ࠪ􎻮̅̒ࠪ􎻮̅̽ࠪ􎻮̅̾ࠪ􎻮̅̿ࠪ􎻮̅͆ࠪ􎻮̅͊ࠪ􎻮̅͋ࠪ\n", "\r\e[9C􎻮̍̅ࠪ􎻮̍̍ࠪ􎻮̍̎ࠪ􎻮̍̐ࠪ􎻮̍̒ࠪ􎻮̍̽ࠪ􎻮̍̾ࠪ􎻮̍̿ࠪ􎻮̍͆ࠪ􎻮̍͊ࠪ􎻮̍͋ࠪ\n", "\r\e[9C􎻮̎̅ࠪ􎻮̎̍ࠪ􎻮̎̎ࠪ􎻮̎̐ࠪ􎻮̎̒ࠪ􎻮̎̽ࠪ􎻮̎̾ࠪ􎻮̎̿ࠪ􎻮̎͆ࠪ􎻮̎͊ࠪ􎻮̎͋ࠪ\n", "\r\e[9C􎻮̐̅ࠪ􎻮̐̍ࠪ􎻮̐̎ࠪ􎻮̐̐ࠪ􎻮̐̒ࠪ􎻮̐̽ࠪ􎻮̐̾ࠪ􎻮̐̿ࠪ􎻮̐͆ࠪ􎻮̐͊ࠪ􎻮̐͋ࠪ\n", "\r\e[9C􎻮̒̅ࠪ􎻮̒̍ࠪ􎻮̒̎ࠪ􎻮̒̐ࠪ􎻮̒̒ࠪ􎻮̒̽ࠪ􎻮̒̾ࠪ􎻮̒̿ࠪ􎻮̒͆ࠪ􎻮̒͊ࠪ􎻮̒͋ࠪ\n", "\r\e[39m\e8"] * Example: fzf --preview=' if file --mime-type {} | grep -qF 'image/'; then # --transfer-mode=memory is the fastest option but if you want fzf to be able # to redraw the image on terminal resize or on 'change-preview-window', # you need to use --transfer-mode=stream. kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} else bat --color=always {} fi '
7 months ago
// Parts of the preview output that should be passed through to the terminal
// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
// * https://sw.kovidgoyal.net/kitty/graphics-protocol
// * https://en.wikipedia.org/wiki/Sixel
// * https://iterm2.com/documentation-images.html
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?(\a|\x1b\\)`)
}
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 {
version int64
lines []string
offset int
scrollable bool
final bool
following resumableState
spinner string
bar []bool
}
type previewed struct {
version int64
numLines int
offset int
filled bool
image bool
wipe bool
wireframe bool
}
type eachLine struct {
line string
err error
}
type itemLine struct {
offset int
current bool
selected bool
label string
queryLen int
width int
bar bool
result Result
}
type fitpad struct {
fit int
pad int
}
var emptyLine = itemLine{}
type labelPrinter func(tui.Window, int)
type StatusItem struct {
Index int `json:"index"`
Text string `json:"text"`
}
type Status struct {
Reading bool `json:"reading"`
Progress int `json:"progress"`
Query string `json:"query"`
Position int `json:"position"`
Sort bool `json:"sort"`
TotalCount int `json:"totalCount"`
MatchCount int `json:"matchCount"`
Current *StatusItem `json:"current"`
Matches []StatusItem `json:"matches"`
Selected []StatusItem `json:"selected"`
}
9 years ago
// Terminal represents terminal input/output
10 years ago
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
infoPrefix string
separator labelPrinter
separatorLen int
spinner []string
promptString 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
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
headerVisible bool
headerFirst bool
headerLines int
header []string
header0 []string
ellipsis string
scrollbar string
previewScrollbar string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
listenAddr *listenAddress
listenPort *int
listenUnsafe bool
borderShape tui.BorderShape
cleanExit bool
executor *util.Executor
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
borderWidth int
count int
progress int
hasResultActions bool
hasFocusActions bool
hasLoadActions bool
hasResizeActions 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
serverInputChan chan []*action
serverOutputChan chan string
eventChan chan tui.Event
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
termSize tui.TermSize
lastAction actionType
lastKey string
lastFocus int32
areaLines int
areaColumns int
forcePreview bool
10 years ago
}
type selectedItem struct {
at time.Time
item *Item
}
9 years ago
type byTimeOrder []selectedItem
9 years ago
func (a byTimeOrder) Len() int {
return len(a)
}
9 years ago
func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
9 years ago
func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
10 years ago
const (
reqPrompt util.EventType = iota
9 years ago
reqInfo
reqHeader
9 years ago
reqList
reqJump
9 years ago
reqRefresh
reqReinit
reqFullRedraw
reqResize
reqRedrawBorderLabel
reqRedrawPreviewLabel
9 years ago
reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqPreviewRefresh
reqPreviewDelayed
9 years ago
reqQuit
10 years ago
)
type action struct {
t actionType
a string
}
//go:generate stringer -type=actionType
type actionType int
const (
actIgnore actionType = iota
actStart
actClick
actInvalid
actChar
actMouse
actBeginningOfLine
actAbort
actAccept
actAcceptNonEmpty
actAcceptOrPrintQuery
actBackwardChar
actBackwardDeleteChar
actBackwardDeleteCharEof
actBackwardWord
actCancel
actChangeBorderLabel
actChangeHeader
actChangeMulti
actChangePreviewLabel
actChangePrompt
actChangeQuery
actClearScreen
actClearQuery
actClearSelection
actClose
actDeleteChar
actDeleteCharEof
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleSearch
actToggleAll
actToggleDown
actToggleUp
actToggleIn
actToggleOut
actToggleTrack
actToggleTrackCurrent
actToggleHeader
actTrackCurrent
actUntrackCurrent
actDown
actUp
actPageUp
actPageDown
actPosition
actHalfPageUp
actHalfPageDown
actOffsetUp
actOffsetDown
actJump
actJumpAccept // XXX Deprecated in favor of jump:accept binding
actPrintQuery
actRefreshPreview
actReplaceQuery
actToggleSort
actShowPreview
actHidePreview
actTogglePreview
actTogglePreviewWrap
actTransform
actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel
actTransformPrompt
actTransformQuery
actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviewHalfPageUp
actPreviewHalfPageDown
actPrevHistory
actPrevSelected
actPut
actNextHistory
actNextSelected
actExecute
actExecuteSilent
actExecuteMulti // Deprecated
actSigStop
actFirst
actLast
actReload
actReloadSync
actDisableSearch
actEnableSearch
actSelect
actDeselect
actUnbind
actRebind
actBecome
actResponse
actShowHeader
actHideHeader
)
func (a actionType) Name() string {
return util.ToKebabCase(a.String()[3:])
}
func processExecution(action actionType) bool {
switch action {
case actTransform,
actTransformBorderLabel,
actTransformHeader,
actTransformPreviewLabel,
actTransformPrompt,
actTransformQuery,
actPreview,
actChangePreview,
actRefreshPreview,
actExecute,
actExecuteSilent,
actExecuteMulti,
actReload,
actReloadSync,
actBecome:
return true
}
return false
}
type placeholderFlags struct {
plus bool
preserveSpace bool
number bool
forceUpdate bool
file bool
}
type searchRequest struct {
sort bool
sync bool
command *string
environ []string
changed bool
}
type previewRequest struct {
template string
pwindow tui.Window
pwindowSize tui.TermSize
scrollOffset int
list []*Item
}
type previewResult struct {
version int64
lines []string
offset int
spinner string
}
func toActions(types ...actionType) []*action {
actions := make([]*action, len(types))
for idx, t := range types {
actions[idx] = &action{t: t, a: ""}
}
return actions
}
func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a)
}
addEvent := func(e tui.Event, a actionType) {
keymap[e] = toActions(a)
}
add(tui.Invalid, actInvalid)
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.Backspace, actBackwardDeleteChar)
add(tui.Tab, actToggleDown)
add(tui.ShiftTab, 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)
if !util.IsWindows() {
add(tui.CtrlZ, actSigStop)
}
addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.ShiftLeft, actBackwardWord)
addEvent(tui.AltKey('f'), actForwardWord)
add(tui.ShiftRight, actForwardWord)
addEvent(tui.AltKey('d'), actKillWord)
add(tui.AltBackspace, actBackwardKillWord)
add(tui.Up, actUp)
add(tui.Down, actDown)
add(tui.Left, actBackwardChar)
add(tui.Right, actForwardChar)
add(tui.Home, actBeginningOfLine)
add(tui.End, actEndOfLine)
add(tui.Delete, actDeleteChar)
add(tui.PageUp, actPageUp)
add(tui.PageDown, actPageDown)
add(tui.ShiftUp, actPreviewUp)
add(tui.ShiftDown, actPreviewDown)
add(tui.Mouse, actMouse)
add(tui.LeftClick, actClick)
add(tui.RightClick, actToggle)
add(tui.SLeftClick, actToggle)
add(tui.SRightClick, actToggle)
add(tui.ScrollUp, actUp)
add(tui.ScrollDown, actDown)
keymap[tui.SScrollUp.AsEvent()] = toActions(actToggle, actUp)
keymap[tui.SScrollDown.AsEvent()] = toActions(actToggle, actDown)
add(tui.PreviewScrollUp, actPreviewUp)
add(tui.PreviewScrollDown, actPreviewDown)
return keymap
}
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
func mayTriggerPreview(opts *Options) bool {
if opts.ListenAddr != nil {
return true
}
for _, actions := range opts.Keymap {
for _, action := range actions {
switch action.t {
case actPreview, actChangePreview, actTransform:
return true
}
}
}
return false
}
func makeSpinner(unicode bool) []string {
if unicode {
return []string{``, ``, ``, ``, ``, ``, ``, ``, ``, ``}
}
return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
}
func evaluateHeight(opts *Options, termHeight int) int {
size := opts.Height.size
if opts.Height.percent {
if opts.Height.inverse {
size = 100 - size
}
return util.Max(int(size*float64(termHeight)/100.0), opts.MinHeight)
}
if opts.Height.inverse {
size = float64(termHeight) - size
}
return int(size)
}
9 years ago
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal {
input := trimQuery(opts.Query)
var delay time.Duration
if opts.Tac {
delay = initialDelayTac
} else {
delay = initialDelay
}
var previewBox *util.EventBox
// We need to start the previewer even when --preview option is not specified
// * if HTTP server is enabled
// * if 'preview' or 'change-preview' action is bound to a key
// * if 'transform' action is bound to a key
if len(opts.Preview.command) > 0 || mayTriggerPreview(opts) {
previewBox = util.NewEventBox()
}
var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
if fullscreen {
if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h })
}
} else {
maxHeightFunc := func(termHeight int) int {
// Minimum height required to render fzf excluding margin and padding
effectiveMinHeight := minHeight
if previewBox != nil && opts.Preview.aboveOrBelow() {
effectiveMinHeight += 1 + borderLines(opts.Preview.border)
}
if noSeparatorLine(opts.InfoStyle, opts.Separator == nil || uniseg.StringWidth(*opts.Separator) > 0) {
effectiveMinHeight--
}
effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
}
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
}
wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
if opts.FileWord {
sep := regexp.QuoteMeta(string(os.PathSeparator))
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
keymapCopy := make(map[tui.Event][]*action)
for key, action := range opts.Keymap {
keymapCopy[key] = action
}
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
infoPrefix: opts.InfoPrefix,
separator: nil,
spinner: makeSpinner(opts.Unicode),
promptString: opts.Prompt,
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff,
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,
listenAddr: opts.ListenAddr,
listenUnsafe: opts.Unsafe,
borderShape: opts.BorderShape,
borderWidth: 1,
borderLabel: nil,
borderLabelOpts: opts.BorderLabel,
previewLabel: nil,
previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit,
executor: executor,
paused: opts.Phony,
cycle: opts.Cycle,
headerVisible: true,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: []string{},
header0: opts.Header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
hasResultActions: false,
hasFocusActions: false,
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,
1 year ago
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
previewed: previewed{0, 0, 0, false, false, false, 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),
serverInputChan: make(chan []*action, 100),
serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false),
lastAction: actStart,
lastFocus: minItem.Index()}
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)
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 = uniseg.StringWidth("│")
}
if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 {
t.scrollbar = "│"
} else {
t.scrollbar = "|"
}
t.previewScrollbar = t.scrollbar
} else {
runes := []rune(*opts.Scrollbar)
if len(runes) > 0 {
t.scrollbar = string(runes[0])
t.previewScrollbar = t.scrollbar
if len(runes) > 1 {
t.previewScrollbar = string(runes[1])
}
}
}
var resizeActions []*action
resizeActions, t.hasResizeActions = t.keymap[tui.Resize.AsEvent()]
if t.tui.ShouldEmitResizeEvent() {
t.keymap[tui.Resize.AsEvent()] = append(toActions(actClearScreen), resizeActions...)
}
_, t.hasResultActions = t.keymap[tui.Result.AsEvent()]
_, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()]
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenAddr != nil {
port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil {
errorExit(err.Error())
}
t.listenPort = &port
}
return &t
10 years ago
}
func (t *Terminal) environ() []string {
env := os.Environ()
if t.listenPort != nil {
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
}
env = append(env, "FZF_QUERY="+string(t.input))
env = append(env, "FZF_ACTION="+t.lastAction.Name())
env = append(env, "FZF_KEY="+t.lastKey)
env = append(env, "FZF_PROMPT="+string(t.promptString))
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1)))
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
}
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 {
extra := t.visibleHeaderLines() + 1
if !t.noSeparatorLine() {
extra++
}
return extra
}
func (t *Terminal) MaxFitAndPad() (int, int) {
_, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
fit := screenHeight - padHeight - t.extraLines()
return fit, padHeight
}
func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) (labelPrinter, int) {
// Nothing to do
if len(str) == 0 {
return nil, 0
}
// Extract ANSI color codes
str = firstLine(str)
text, colors, _ := extractColor(str, nil, nil)
runes := []rune(text)
// Simpler printer for strings without ANSI colors or tab characters
if colors == nil && !strings.ContainsRune(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, length
}
// Printer that correctly handles ANSI color codes and tab characters
item := &Item{text: util.RunesToChars(runes), colors: colors}
length := t.displayWidth(runes)
if length == 0 {
return nil, 0
}
result := Result{item: item}
var offsets []colorOffset
printFn := func(window tui.Window, limit int) {
if offsets == nil {
// tui.Col* are not initialized until renderer.Init()
offsets = result.colorOffsets(nil, 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 noSeparatorLine(style infoStyle, separator bool) bool {
switch style {
case infoInline:
return true
case infoHidden, infoInlineRight:
return !separator
}
return false
}
func (t *Terminal) noSeparatorLine() bool {
return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
}
func getScrollbar(total int, height int, offset int) (int, int) {
if total == 0 || total <= height {
return 0, 0
}
barLength := util.Max(1, height*height/total)
var barStart int
if total == height {
barStart = 0
} else {
barStart = (height - barLength) * offset / (total - height)
}
return barLength, barStart
}
func (t *Terminal) getScrollbar() (int, int) {
return getScrollbar(t.merger.Length(), t.maxItems(), t.offset)
}
9 years ago
// Input returns current query string
func (t *Terminal) Input() (bool, []rune) {
10 years ago
t.mutex.Lock()
defer t.mutex.Unlock()
return t.paused, copySlice(t.input)
10 years ago
}
9 years ago
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
10 years ago
t.mutex.Lock()
t.count = cnt
if t.hasLoadActions && t.reading && final {
t.triggerLoad = true
}
10 years ago
t.reading = !final
t.failed = failedCommand
10 years ago
t.mutex.Unlock()
9 years ago
t.reqBox.Set(reqInfo, nil)
if final {
9 years ago
t.reqBox.Set(reqRefresh, nil)
}
10 years ago
}
func (t *Terminal) changeHeader(header string) bool {
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
needFullRedraw := len(t.header0) != len(lines)
t.header0 = lines
return needFullRedraw
}
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock()
t.header = header
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
9 years ago
// UpdateProgress updates the search progress
10 years ago
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
changed := t.progress != newProgress
t.progress = newProgress
10 years ago
t.mutex.Unlock()
if changed {
9 years ago
t.reqBox.Set(reqInfo, nil)
}
10 years ago
}
9 years ago
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
10 years ago
t.mutex.Lock()
prevIndex := minItem.Index()
reset := t.revision != merger.Revision()
if !reset && t.track != trackDisabled {
if t.merger.Length() > 0 {
prevIndex = t.currentIndex()
} else if merger.Length() > 0 {
prevIndex = merger.First().item.Index()
}
}
10 years ago
t.progress = 100
t.merger = merger
if reset {
t.selected = make(map[int32]selectedItem)
t.revision = merger.Revision()
t.version++
}
1 year ago
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
} 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
}
}
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
}
}
if triggerResultEvent && t.hasResultActions {
t.eventChan <- tui.Result.AsEvent()
}
}
10 years ago
t.mutex.Unlock()
9 years ago
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
10 years ago
}
func (t *Terminal) output() bool {
10 years ago
if t.printQuery {
t.printer(string(t.input))
10 years ago
}
if len(t.expect) > 0 {
t.printer(t.pressed)
}
found := len(t.selected) > 0
if !found {
current := t.currentItem()
if current != nil {
t.printer(current.AsString(t.ansi))
found = true
10 years ago
}
} else {
for _, sel := range t.sortSelected() {
t.printer(sel.item.AsString(t.ansi))
10 years ago
}
}
return found
10 years ago
}
func (t *Terminal) sortSelected() []selectedItem {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
return sels
}
func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width
10 years ago
}
const (
minWidth = 4
minHeight = 3
)
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)
}
return util.Constrain(int(size.size)+pad, minSize, max)
}
func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
screenWidth := t.tui.MaxX()
screenHeight := t.tui.MaxY()
marginInt := [4]int{} // TRBL
paddingInt := [4]int{} // TRBL
sizeSpecToInt := func(index int, spec sizeSpec) int {
if spec.percent {
var max float64
if index%2 == 0 {
max = float64(screenHeight)
} else {
max = float64(screenWidth)
}
return int(max * spec.size * 0.01)
}
return int(spec.size)
}
for idx, sizeSpec := range t.padding {
paddingInt[idx] = sizeSpecToInt(idx, sizeSpec)
}
bw := t.borderWidth
extraMargin := [4]int{} // TRBL
for idx, sizeSpec := range t.margin {
switch t.borderShape {
case tui.BorderHorizontal:
extraMargin[idx] += 1 - idx%2
case tui.BorderVertical:
extraMargin[idx] += (1 + bw) * (idx % 2)
case tui.BorderTop:
if idx == 0 {
extraMargin[idx]++
}
case tui.BorderRight:
if idx == 1 {
extraMargin[idx] += 1 + bw
}
case tui.BorderBottom:
if idx == 2 {
extraMargin[idx]++
}
case tui.BorderLeft:
if idx == 3 {
extraMargin[idx] += 1 + bw
}
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderThinBlock, tui.BorderDouble:
extraMargin[idx] += 1 + bw*(idx%2)
}
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
}
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)
}
}
minAreaWidth := minWidth
minAreaHeight := minHeight
if t.noSeparatorLine() {
minAreaHeight -= 1
}
1 year ago
if t.needPreviewWindow() {
minPreviewHeight := 1 + borderLines(t.previewOpts.border)
minPreviewWidth := 5
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) {
t.forcePreview = forcePreview
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
t.prevLines = make([]itemLine, 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
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(
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(
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(
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(
marginInt[0]-1, marginInt[3]-(1+bw), width+(1+bw)*2, height+2,
false, tui.MakeBorderStyle(t.borderShape, t.unicode))
}
// Add padding to margin
for idx, val := range paddingInt {
marginInt[idx] += val
}
width -= paddingInt[1] + paddingInt[3]
height -= paddingInt[0] + paddingInt[2]
t.areaLines = height
t.areaColumns = width
// Set up preview window
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
1 year ago
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
}
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
}
if !previewOpts.alternative.hidden {
resizePreviewWindows(previewOpts.alternative)
}
return
}
if forcePreview {
previewOpts.hidden = false
}
if previewOpts.hidden {
return
}
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
}
if !previewOpts.alternative.hidden {
resizePreviewWindows(previewOpts.alternative)
}
return
}
if forcePreview {
previewOpts.hidden = false
}
if previewOpts.hidden {
return
}
if previewOpts.position == posLeft {
// Put scrollbar closer to the right border for consistent look
if t.borderShape.HasRight() {
width++
}
// Add a 1-column margin between the preview window and the main window
t.window = t.tui.NewWindow(
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)
// 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
}
// Without preview window
if t.window == nil {
if t.borderShape.HasRight() {
// Put scrollbar closer to the right border for consistent look
width++
}
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)
}
}
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())
}
}
10 years ago
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:
n := 2 + t.visibleHeaderLines()
if t.noSeparatorLine() {
n--
}
if y < n {
y = h - y - 1
} else {
y -= n
}
10 years ago
}
if clear {
t.window.MoveAndClear(y, x)
10 years ago
} else {
t.window.Move(y, x)
10 years ago
}
}
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.noSeparatorLine() {
max--
}
return util.Min(t.visibleHeaderLines(), max)
}
return 0
}
10 years ago
func (t *Terminal) placeCursor() {
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
10 years ago
}
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))
10 years ago
}
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))
}
10 years ago
func (t *Terminal) printInfo() {
pos := 0
line := t.promptLine()
printSpinner := func() {
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
} else {
t.window.Print(" ") // Clear spinner
}
}
printInfoPrefix := func() {
str := t.infoPrefix
maxWidth := t.window.Width() - pos
width := util.StringWidth(str)
if width > maxWidth {
trimmed, _ := t.trimRight([]rune(str), maxWidth)
str = string(trimmed)
width = maxWidth
}
t.move(line, pos, t.separatorLen == 0)
if t.reading {
t.window.CPrint(tui.ColSpinner, str)
} else {
t.window.CPrint(tui.ColPrompt, str)
}
pos += width
}
printSeparator := func(fillLength int, pad bool) {
// --------_
if t.separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
} else if pad {
t.window.Print(strings.Repeat(" ", fillLength+1))
}
}
switch t.infoStyle {
case infoDefault:
t.move(line+1, 0, t.separatorLen == 0)
printSpinner()
t.move(line+1, 2, false)
pos = 2
case infoRight:
t.move(line+1, 0, false)
case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
printInfoPrefix()
case infoHidden:
if t.separatorLen > 0 {
t.move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return
10 years ago
}
found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
if t.toggleSort {
if t.sort {
output += " +S"
} else {
output += " -S"
}
}
switch t.track {
case trackEnabled:
output += " +T"
case trackCurrent:
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)
}
10 years ago
}
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)
}
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
}
if t.infoStyle == infoInlineRight {
if len(t.infoPrefix) == 0 {
t.move(line, pos, false)
newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
t.window.Print(strings.Repeat(" ", newPos-pos))
pos = newPos
if pos < t.window.Width() {
printSpinner()
pos++
}
if pos < t.window.Width()-1 {
t.window.Print(" ")
pos++
}
} else {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-util.StringWidth(t.infoPrefix)-1)
printInfoPrefix()
}
}
maxWidth := t.window.Width() - pos
output = t.trimMessage(output, maxWidth)
t.window.CPrint(tui.ColInfo, output)
if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 {
t.move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return
}
fillLength := maxWidth - len(output) - 2
if fillLength > 0 {
t.window.CPrint(tui.ColSeparator, " ")
printSeparator(fillLength, false)
}
}
func (t *Terminal) printHeader() {
if t.visibleHeaderLines() == 0 {
return
}
max := t.window.Height()
if t.headerFirst {
max--
if !t.noSeparatorLine() {
max--
}
}
var state *ansiState
needReverse := false
switch t.layout {
case layoutDefault, layoutReverseList:
needReverse = true
}
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx
if needReverse && idx < len(t.header0) {
line = len(t.header0) - idx - 1
}
if !t.headerFirst {
line++
if !t.noSeparatorLine() {
line++
}
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState
item := &Item{
text: util.ToChars([]byte(trimmed)),
colors: colors}
t.move(line, 0, true)
t.window.Print(" ")
t.printHighlighted(Result{item: item},
tui.ColHeader, tui.ColHeader, false, false)
}
}
10 years ago
func (t *Terminal) printList() {
t.constrain()
barLength, barStart := t.getScrollbar()
10 years ago
maxy := t.maxItems()
count := t.merger.Length() - t.offset
for j := 0; j < maxy; j++ {
i := j
if t.layout == layoutDefault {
i = maxy - 1 - j
}
line := i + 2 + t.visibleHeaderLines()
if t.noSeparatorLine() {
9 years ago
line--
}
10 years ago
if i < count {
t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
} else if t.prevLines[i] != emptyLine || t.prevLines[i].offset != line {
t.prevLines[i] = emptyLine
t.move(line, 0, true)
10 years ago
}
}
}
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
}
// Avoid unnecessary redraw
newLine := itemLine{offset: line, current: current, selected: selected, label: label,
result: result, queryLen: len(t.input), width: 0, bar: bar}
prevLine := t.prevLines[i]
forceRedraw := prevLine.offset != newLine.offset
printBar := func() {
if len(t.scrollbar) > 0 && (bar != prevLine.bar || forceRedraw) {
t.prevLines[i].bar = bar
t.move(line, t.window.Width()-1, true)
if bar {
t.window.CPrint(tui.ColScrollbar, t.scrollbar)
}
}
}
if !forceRedraw &&
prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
printBar()
return
}
t.move(line, 0, forceRedraw)
10 years ago
if current {
if len(label) == 0 {
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCurrentCursor, label)
}
10 years ago
if selected {
t.window.CPrint(tui.ColCurrentSelected, t.marker)
10 years ago
} else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
10 years ago
}
newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
10 years ago
} else {
if len(label) == 0 {
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCursor, label)
}
10 years ago
if selected {
t.window.CPrint(tui.ColSelected, t.marker)
10 years ago
} else {
t.window.Print(t.markerEmpty)
10 years ago
}
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))
10 years ago
}
printBar()
t.prevLines[i] = newLine
10 years ago
}
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters
_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 {
return runes[:overflowIdx], true
}
return runes, false
}
10 years ago
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
return width
10 years ago
}
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:]
}
currentWidth := t.displayWidth(runes)
10 years ago
for currentWidth > width && len(runes) > 0 {
runes = runes[1:]
9 years ago
trimmed++
currentWidth = t.displayWidthWithLimit(runes, 2, width)
10 years ago
}
return runes, trimmed
}
func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
}
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int {
item := result.item
10 years ago
// Overflow
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
matchOffsets := []Offset{}
var pos *[]int
if match && t.merger.pattern != nil {
_, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab)
}
charOffsets := matchOffsets
if pos != nil {
charOffsets = make([]Offset, len(*pos))
for idx, p := range *pos {
offset := Offset{int32(p), int32(p + 1)}
charOffsets[idx] = offset
}
sort.Sort(ByOrder(charOffsets))
}
var maxe int
for _, offset := range charOffsets {
maxe = util.Max(maxe, int(offset[1]))
}
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
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...)
10 years ago
}
} else {
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, ellipsis...)
10 years ago
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))
10 years ago
}
}
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
10 years ago
}
t.printColoredString(t.window, text, offsets, colBase)
return displayWidth
}
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
9 years ago
var index int32
var substr string
var prefixWidth int
maxOffset := int32(len(text))
10 years ago
for _, offset := range offsets {
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
window.CPrint(colBase, substr)
if b < e {
substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
window.CPrint(offset.color, substr)
}
10 years ago
index = e
if index >= maxOffset {
break
}
10 years ago
}
if index < maxOffset {
substr, _ = t.processTabs(text[index:], prefixWidth)
window.CPrint(colBase, substr)
}
}
func (t *Terminal) renderPreviewSpinner() {
numLines := len(t.previewer.lines)
spin := t.previewer.spinner
if len(spin) > 0 || t.previewer.scrollable {
maxWidth := t.pwindow.Width()
if !t.previewer.scrollable {
if maxWidth > 0 {
t.pwindow.Move(0, maxWidth-1)
t.pwindow.CPrint(tui.ColPreviewSpinner, spin)
}
} else {
offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines)
if len(spin) > 0 {
spin += " "
maxWidth -= 2
}
offsetRunes, _ := t.trimRight([]rune(offsetString), maxWidth)
pos := maxWidth - t.displayWidth(offsetRunes)
t.pwindow.Move(0, pos)
if maxWidth > 0 {
t.pwindow.CPrint(tui.ColPreviewSpinner, spin)
t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), string(offsetRunes))
}
}
}
}
func (t *Terminal) renderPreviewArea(unchanged bool) {
if t.previewed.wipe && t.previewed.version != t.previewer.version {
t.previewed.wipe = false
t.pwindow.Erase()
} else if unchanged {
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else {
t.previewed.filled = false
// We don't erase the window here to avoid flickering during scroll.
// However, tcell renderer uses double-buffering technique and there's no
// flickering. So we just erase the window and make the rest of the code
// simpler.
if !t.pwindow.EraseMaybe() {
t.pwindow.DrawBorder()
t.pwindow.Move(0, 0)
}
}
height := t.pwindow.Height()
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()
}
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) makeImageBorder(width int, top bool) string {
tl := "┌"
tr := "┐"
v := "╎"
h := "╌"
if !t.unicode {
tl = "+"
tr = "+"
h = "-"
v = "|"
}
repeat := util.Max(0, width-2)
if top {
return tl + strings.Repeat(h, repeat) + tr
}
return v + strings.Repeat(" ", repeat) + v
}
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState
spinnerRedraw := t.pwindow.Y() == 0
wiped := false
image := false
wireframe := false
Loop:
for _, line := range lines {
var lbg tui.Color = -1
if ansi != nil {
ansi.lbg = -1
}
Experimental support for Kitty image protocol in preview window Close #3228 * Works inside and outside of tmux * There is a problem where fzf unnecessarily displays the scroll offset indicator at the topbright of the screen when the image just fits the preview window. This is because `kitty icat` generates an extra line after the image area. # A 5-row images; an extra row at the end confuses fzf ["\e_Ga ... \e[9C􎻮̅̅ࠪ􎻮̅̍ࠪ􎻮̅̎ࠪ􎻮̅̐ࠪ􎻮̅̒ࠪ􎻮̅̽ࠪ􎻮̅̾ࠪ􎻮̅̿ࠪ􎻮̅͆ࠪ􎻮̅͊ࠪ􎻮̅͋ࠪ\n", "\r\e[9C􎻮̍̅ࠪ􎻮̍̍ࠪ􎻮̍̎ࠪ􎻮̍̐ࠪ􎻮̍̒ࠪ􎻮̍̽ࠪ􎻮̍̾ࠪ􎻮̍̿ࠪ􎻮̍͆ࠪ􎻮̍͊ࠪ􎻮̍͋ࠪ\n", "\r\e[9C􎻮̎̅ࠪ􎻮̎̍ࠪ􎻮̎̎ࠪ􎻮̎̐ࠪ􎻮̎̒ࠪ􎻮̎̽ࠪ􎻮̎̾ࠪ􎻮̎̿ࠪ􎻮̎͆ࠪ􎻮̎͊ࠪ􎻮̎͋ࠪ\n", "\r\e[9C􎻮̐̅ࠪ􎻮̐̍ࠪ􎻮̐̎ࠪ􎻮̐̐ࠪ􎻮̐̒ࠪ􎻮̐̽ࠪ􎻮̐̾ࠪ􎻮̐̿ࠪ􎻮̐͆ࠪ􎻮̐͊ࠪ􎻮̐͋ࠪ\n", "\r\e[9C􎻮̒̅ࠪ􎻮̒̍ࠪ􎻮̒̎ࠪ􎻮̒̐ࠪ􎻮̒̒ࠪ􎻮̒̽ࠪ􎻮̒̾ࠪ􎻮̒̿ࠪ􎻮̒͆ࠪ􎻮̒͊ࠪ􎻮̒͋ࠪ\n", "\r\e[39m\e8"] * Example: fzf --preview=' if file --mime-type {} | grep -qF 'image/'; then # --transfer-mode=memory is the fastest option but if you want fzf to be able # to redraw the image on terminal resize or on 'change-preview-window', # you need to use --transfer-mode=stream. kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} else bat --color=always {} fi '
7 months ago
passThroughs := passThroughRegex.FindAllString(line, -1)
if passThroughs != nil {
line = passThroughRegex.ReplaceAllString(line, "")
}
line = strings.TrimLeft(strings.TrimRight(line, "\r\n"), "\r")
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
t.previewed.filled = true
t.previewer.scrollable = true
break
} else if lineNo >= 0 {
x := t.pwindow.X()
y := t.pwindow.Y()
if spinnerRedraw && lineNo > 0 {
spinnerRedraw = false
t.renderPreviewSpinner()
t.pwindow.Move(y, x)
}
for idx, passThrough := range passThroughs {
// Handling Sixel/iTerm image
requiredLines := 0
isSixel := strings.HasPrefix(passThrough, "\x1bP")
isItermImage := strings.HasPrefix(passThrough, "\x1b]1337;")
isImage := isSixel || isItermImage
if isImage {
t.previewed.wipe = true
// NOTE: We don't have a good way to get the height of an iTerm image,
// so we assume that it requires the full height of the preview
// window.
requiredLines = height
if isSixel && t.termSize.PxHeight > 0 {
rows := strings.Count(passThrough, "-")
requiredLines = int(math.Ceil(float64(rows*6*t.termSize.Lines) / float64(t.termSize.PxHeight)))
}
}
// Render wireframe when the image cannot be displayed entirely
if requiredLines > 0 && y+requiredLines > height {
top := true
for ; y < height; y++ {
t.pwindow.MoveAndClear(y, 0)
t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, t.makeImageBorder(maxWidth, top))
top = false
}
wireframe = true
t.previewed.filled = true
t.previewer.scrollable = true
break Loop
}
// Clear previous wireframe or any other text
if (t.previewed.wireframe || isImage && !t.previewed.image) && !wiped {
wiped = true
for i := y + 1; i < height; i++ {
t.pwindow.MoveAndClear(i, 0)
}
}
image = image || isImage
if idx == 0 {
t.pwindow.MoveAndClear(y, x)
} else {
t.pwindow.Move(y, x)
}
t.tui.PassThrough(passThrough)
if requiredLines > 0 {
if y+requiredLines == height {
t.pwindow.Move(height-1, maxWidth-1)
t.previewed.filled = true
break Loop
} else {
t.pwindow.MoveAndClear(y+requiredLines, 0)
}
}
Experimental support for Kitty image protocol in preview window Close #3228 * Works inside and outside of tmux * There is a problem where fzf unnecessarily displays the scroll offset indicator at the topbright of the screen when the image just fits the preview window. This is because `kitty icat` generates an extra line after the image area. # A 5-row images; an extra row at the end confuses fzf ["\e_Ga ... \e[9C􎻮̅̅ࠪ􎻮̅̍ࠪ􎻮̅̎ࠪ􎻮̅̐ࠪ􎻮̅̒ࠪ􎻮̅̽ࠪ􎻮̅̾ࠪ􎻮̅̿ࠪ􎻮̅͆ࠪ􎻮̅͊ࠪ􎻮̅͋ࠪ\n", "\r\e[9C􎻮̍̅ࠪ􎻮̍̍ࠪ􎻮̍̎ࠪ􎻮̍̐ࠪ􎻮̍̒ࠪ􎻮̍̽ࠪ􎻮̍̾ࠪ􎻮̍̿ࠪ􎻮̍͆ࠪ􎻮̍͊ࠪ􎻮̍͋ࠪ\n", "\r\e[9C􎻮̎̅ࠪ􎻮̎̍ࠪ􎻮̎̎ࠪ􎻮̎̐ࠪ􎻮̎̒ࠪ􎻮̎̽ࠪ􎻮̎̾ࠪ􎻮̎̿ࠪ􎻮̎͆ࠪ􎻮̎͊ࠪ􎻮̎͋ࠪ\n", "\r\e[9C􎻮̐̅ࠪ􎻮̐̍ࠪ􎻮̐̎ࠪ􎻮̐̐ࠪ􎻮̐̒ࠪ􎻮̐̽ࠪ􎻮̐̾ࠪ􎻮̐̿ࠪ􎻮̐͆ࠪ􎻮̐͊ࠪ􎻮̐͋ࠪ\n", "\r\e[9C􎻮̒̅ࠪ􎻮̒̍ࠪ􎻮̒̎ࠪ􎻮̒̐ࠪ􎻮̒̒ࠪ􎻮̒̽ࠪ􎻮̒̾ࠪ􎻮̒̿ࠪ􎻮̒͆ࠪ􎻮̒͊ࠪ􎻮̒͋ࠪ\n", "\r\e[39m\e8"] * Example: fzf --preview=' if file --mime-type {} | grep -qF 'image/'; then # --transfer-mode=memory is the fastest option but if you want fzf to be able # to redraw the image on terminal resize or on 'change-preview-window', # you need to use --transfer-mode=stream. kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} else bat --color=always {} fi '
7 months ago
}
if len(passThroughs) > 0 && len(line) == 0 {
continue
}
var fillRet tui.FillReturn
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
isTrimmed := false
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()
if fillRet == tui.FillNextLine {
continue
} else if fillRet == tui.FillSuspend {
t.previewed.filled = true
break
}
if unchanged && lineNo == 0 {
break
}
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++
}
t.previewed.image = image
t.previewed.wireframe = wireframe
}
func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
height := t.pwindow.Height()
w := t.pborder.Width()
redraw := false
if len(t.previewer.bar) != height {
redraw = true
t.previewer.bar = make([]bool, height)
}
xshift := -1 - t.borderWidth
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() {
continue
}
t.previewer.bar[i] = bar
t.pborder.Move(y, x)
if i >= yoff+barStart && i < yoff+barStart+barLength {
t.pborder.CPrint(tui.ColPreviewScrollbar, t.previewScrollbar)
} else {
t.pborder.CPrint(tui.ColPreviewScrollbar, " ")
}
}
}
func (t *Terminal) printPreview() {
if !t.hasPreviewWindow() || t.pwindow.Height() == 0 {
return
}
numLines := len(t.previewer.lines)
height := t.pwindow.Height()
unchanged := (t.previewed.filled || numLines == t.previewed.numLines) &&
t.previewer.version == t.previewed.version &&
t.previewer.offset == t.previewed.offset
t.previewer.scrollable = t.previewer.offset > t.previewOpts.headerLines || numLines > height
t.renderPreviewArea(unchanged)
t.renderPreviewSpinner()
t.previewed.numLines = numLines
t.previewed.version = t.previewer.version
t.previewed.offset = t.previewer.offset
}
func (t *Terminal) printPreviewDelayed() {
if !t.hasPreviewWindow() || len(t.previewer.lines) > 0 && t.previewed.version == t.previewer.version {
return
}
t.previewer.scrollable = false
t.renderPreviewArea(true)
message := t.trimMessage("Loading ..", t.pwindow.Width())
pos := t.pwindow.Width() - len(message)
t.pwindow.Move(0, pos)
t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), message)
}
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf strings.Builder
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
strbuf.WriteString(strings.Repeat(" ", w))
} else {
w = util.StringWidth(str)
strbuf.WriteString(str)
}
l += w
10 years ago
}
return strbuf.String(), l
10 years ago
}
func (t *Terminal) printAll() {
t.resizeWindows(t.forcePreview)
10 years ago
t.printList()
t.printPrompt()
t.printInfo()
t.printHeader()
t.printPreview()
10 years ago
}
func (t *Terminal) refresh() {
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)
}
10 years ago
}
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)
10 years ago
}
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)
10 years ago
}
func copySlice(slice []rune) []rune {
ret := make([]rune, len(slice))
copy(ret, slice)
return ret
}
func (t *Terminal) rubout(pattern string) {
pcx := t.cx
after := t.input[t.cx:]
t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1
t.yanked = copySlice(t.input[t.cx:pcx])
t.input = append(t.input[:t.cx], after...)
}
func keyMatch(key tui.Event, event tui.Event) bool {
return event.Type == key.Type && event.Char == key.Char ||
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
}
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags := placeholderFlags{}
if match[0] == '\\' {
// Escaped placeholder pattern
return true, match[1:], flags
}
if strings.HasPrefix(match, "{fzf:") {
// {fzf:*} are not determined by the current item
flags.forceUpdate = true
return false, match, flags
}
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.forceUpdate = true
// query flag is not skipped
default:
break
}
}
matchWithoutFlags := "{" + match[skipChars:]
return false, matchWithoutFlags, flags
}
func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
for _, match := range placeholder.FindAllString(template, -1) {
_, _, flags := parsePlaceholder(match)
if flags.plus {
plus = true
}
if flags.forceUpdate {
forceUpdate = true
}
slot = true
}
return
}
func writeTemporaryFile(data []string, printSep string) string {
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{}
}
type replacePlaceholderParams struct {
template string
stripAnsi bool
delimiter Delimiter
printsep string
forcePlus bool
query string
allItems []*Item
lastAction actionType
prompt string
executor *util.Executor
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: t.ansi,
delimiter: t.delimiter,
printsep: t.printsep,
forcePlus: forcePlus,
query: input,
allItems: list,
lastAction: t.lastAction,
prompt: t.promptString,
executor: t.executor,
})
}
func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
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(params replacePlaceholderParams) string {
current := params.allItems[:1]
selected := params.allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
// replace placeholders one by one
return placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
escaped, match, flags := parsePlaceholder(match)
// this function implements the effects a placeholder has on items
var replace func(*Item) string
// placeholder types (escaped, query type, item type, token type)
switch {
case escaped:
return match
case match == "{q}" || match == "{fzf:query}":
return params.executor.QuoteEntry(params.query)
case 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(params.stripAnsi)
default:
return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
}
}
case match == "{fzf:action}":
return params.lastAction.Name()
case match == "{fzf:prompt}":
return params.executor.QuoteEntry(params.prompt)
default:
// token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(rangeExpressions))
for idx, s := range rangeExpressions {
r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
if !ok {
// Invalid expression, just return the original string in the template
return match
}
ranges[idx] = r
}
replace = func(item *Item) string {
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
trans := Transform(tokens, ranges)
str := joinTokens(trans)
// trim the last delimiter
if params.delimiter.str != nil {
str = strings.TrimSuffix(str, *params.delimiter.str)
} else if params.delimiter.regex != nil {
delims := params.delimiter.regex.FindAllStringIndex(str, -1)
// make sure the delimiter is at the very end of the string
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]]
}
}
if !flags.preserveSpace {
str = strings.TrimSpace(str)
}
if !flags.file {
str = params.executor.QuoteEntry(str)
}
return str
}
}
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || params.forcePlus {
items = selected
}
replacements := make([]string, len(items))
for idx, item := range items {
replacements[idx] = replace(item)
}
if flags.file {
return writeTemporaryFile(replacements, params.printsep)
}
return strings.Join(replacements, " ")
})
}
func (t *Terminal) redraw() {
t.tui.Clear()
t.tui.Refresh()
t.printAll()
}
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
line := ""
valid, list := t.buildPlusList(template, forcePlus)
// 'capture' is used for transform-* and we don't want to
// return an empty string in those cases
if !valid && !capture {
return line
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
if !background {
cmd.Stdin = tui.TtyIn()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
t.tui.Pause(true)
cmd.Run()
t.tui.Resume(true, false)
t.redraw()
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()
}
}
t.executing.Set(false)
cleanTemporaryFiles()
return line
}
func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil
}
1 year ago
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)
1 year ago
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, forceUpdate := hasPreviewFlags(template)
if !(!slot || forceUpdate || (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
// 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 {
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)
}
func (t *Terminal) pwindowSize() tui.TermSize {
if t.pwindow == nil {
return tui.TermSize{}
}
size := tui.TermSize{Lines: t.pwindow.Height(), Columns: t.pwindow.Width()}
if t.termSize.PxWidth > 0 {
size.PxWidth = size.Columns * t.termSize.PxWidth / t.termSize.Columns
size.PxHeight = size.Lines * t.termSize.PxHeight / t.termSize.Lines
}
return size
}
func (t *Terminal) currentIndex() int32 {
if currentItem := t.currentItem(); currentItem != nil {
return currentItem.Index()
}
return minItem.Index()
}
9 years ago
// Loop is called to start Terminal I/O
10 years ago
func (t *Terminal) Loop() {
// 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()
1 year ago
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)
})
}
10 years ago
{ // 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)
}
}
}()
contChan := make(chan os.Signal, 1)
notifyOnCont(contChan)
go func() {
for {
<-contChan
t.reqBox.Set(reqReinit, nil)
}
}()
if !t.tui.ShouldEmitResizeEvent() {
resizeChan := make(chan os.Signal, 1)
notifyOnResize(resizeChan) // Non-portable
go func() {
for {
<-resizeChan
t.reqBox.Set(reqResize, nil)
}
}()
}
t.mutex.Lock()
t.initFunc()
t.termSize = t.tui.Size()
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)
}
}
}()
10 years ago
}
if t.hasPreviewer() {
go func() {
var version int64
for {
var items []*Item
var commandTemplate string
var pwindow tui.Window
var pwindowSize tui.TermSize
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
pwindowSize = request.pwindowSize
}
}
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 := t.executor.ExecCommand(command, true)
env := t.environ()
if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
env = append(env, fmt.Sprintf("FZF_PREVIEW_TOP=%d", t.tui.Top()+pwindow.Top()))
env = append(env, fmt.Sprintf("FZF_PREVIEW_LEFT=%d", pwindow.Left()))
}
cmd.Env = env
out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
reader := bufio.NewReader(out)
eofChan := make(chan bool)
finishChan := make(chan bool, 1)
err := cmd.Start()
if err == nil {
reapChan := make(chan bool)
lineChan := make(chan eachLine)
// Goroutine 1 reads process output
go func() {
for {
line, err := reader.ReadString('\n')
lineChan <- eachLine{line, err}
if err != nil {
break
}
}
eofChan <- true
}()
// Goroutine 2 periodically requests rendering
rendered := util.NewAtomicBool(false)
go func(version int64) {
lines := []string{}
spinner := makeSpinner(t.unicode)
spinnerIndex := -1 // Delay initial rendering by an extra tick
ticker := time.NewTicker(previewChunkDelay)
offset := initialOffset
Loop:
for {
select {
case <-ticker.C:
if len(lines) > 0 && len(lines) >= initialOffset {
if spinnerIndex >= 0 {
spin := spinner[spinnerIndex%len(spinner)]
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
rendered.Set(true)
offset = -1
}
spinnerIndex++
}
case eachLine := <-lineChan:
line := eachLine.line
err := eachLine.err
if len(line) > 0 {
clearIndex := strings.Index(line, clearCode)
if clearIndex >= 0 {
lines = []string{}
line = line[clearIndex+len(clearCode):]
version--
offset = 0
}
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) {
1 year ago
if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
}
}
10 years ago
go func() {
var focusedIndex int32 = minItem.Index()
var version int64 = -1
running := true
3 years ago
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()
}
for running {
t.reqBox.Wait(func(events *util.Events) {
10 years ago
defer events.Clear()
t.mutex.Lock()
for req, value := range *events {
10 years ago
switch req {
9 years ago
case reqPrompt:
10 years ago
t.printPrompt()
if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
t.printInfo()
}
9 years ago
case reqInfo:
10 years ago
t.printInfo()
9 years ago
case reqList:
10 years ago
t.printList()
currentIndex := t.currentIndex()
focusChanged := focusedIndex != currentIndex
if focusChanged && t.track == trackCurrent {
t.track = trackDisabled
t.printInfo()
}
if t.hasFocusActions && focusChanged && currentIndex != t.lastFocus {
t.lastFocus = currentIndex
t.eventChan <- tui.Focus.AsEvent()
}
if focusChanged || version != t.version {
version = t.version
focusedIndex = currentIndex
refreshPreview(t.previewOpts.command)
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
}
t.printList()
case reqHeader:
t.printHeader()
9 years ago
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)
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
case reqResize, reqFullRedraw:
if req == reqResize {
t.termSize = t.tui.Size()
}
wasHidden := t.pwindow == nil
t.redraw()
1 year ago
if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command)
}
if req == reqResize && t.hasResizeActions {
t.eventChan <- tui.Resize.AsEvent()
}
9 years ago
case reqClose:
3 years ago
exit(func() int {
if t.output() {
return exitOk
}
return exitNoMatch
})
return
case reqPreviewDisplay:
result := value.(previewResult)
if t.previewer.version != result.version {
t.previewer.version = result.version
t.previewer.following.Force(t.previewOpts.follow)
if t.previewer.following.Enabled() {
t.previewer.offset = 0
}
}
t.previewer.lines = result.lines
t.previewer.spinner = result.spinner
if t.hasPreviewWindow() && t.previewer.following.Enabled() {
t.previewer.offset = util.Max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.previewOpts.headerLines))
} 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:
t.previewer.version = value.(int64)
t.printPreviewDelayed()
case reqPrintQuery:
3 years ago
exit(func() int {
t.printer(string(t.input))
return exitOk
})
return
9 years ago
case reqQuit:
3 years ago
exit(func() int { return exitInterrupt })
return
10 years ago
}
}
t.refresh()
10 years ago
t.mutex.Unlock()
})
}
3 years ago
// prof.Stop()
t.killPreview(code)
10 years ago
}()
looping := true
_, startEvent := t.keymap[tui.Start.AsEvent()]
needBarrier := true
barrier := make(chan bool)
go func() {
for {
<-barrier
t.eventChan <- t.tui.GetChar()
}
}()
previewDraggingPos := -1
barDragging := false
pbarDragging := false
wasDown := false
10 years ago
for looping {
var newCommand *string
var reloadSync bool
changed := false
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:
if t.tui.ShouldEmitResizeEvent() {
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero)
} else {
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero, tui.Resize)
}
case serverActions := <-t.serverInputChan:
event = tui.Invalid.AsEvent()
if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
actions = serverActions
} else {
for _, action := range serverActions {
if !processExecution(action.t) {
actions = append(actions, action)
}
}
}
needBarrier = false
}
}
10 years ago
t.mutex.Lock()
previousInput := t.input
previousCx := t.cx
t.lastKey = event.KeyName()
events := []util.EventType{}
req := func(evts ...util.EventType) {
10 years ago
for _, event := range evts {
events = append(events, event)
9 years ago
if event == reqClose || event == reqQuit {
10 years ago
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) {
9 years ago
req(reqInfo)
return true
}
return false
}
scrollPreviewTo := func(newOffset int) {
if !t.previewer.scrollable {
return
}
numLines := len(t.previewer.lines)
headerLines := t.previewOpts.headerLines
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 {
if keyMatch(key, event) {
t.pressed = ret
t.reqBox.Set(reqClose, nil)
t.mutex.Unlock()
return
}
}
actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()]
}
var doAction func(*action) bool
var doActions func(actions []*action) bool
doActions = func(actions []*action) bool {
for iter := 0; iter <= maxFocusEvents; iter++ {
currentIndex := t.currentIndex()
for _, action := range actions {
if !doAction(action) {
return false
}
}
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents {
if newIndex := t.currentIndex(); newIndex != currentIndex {
t.lastFocus = newIndex
actions = onFocus
continue
}
}
break
}
return true
}
doAction = func(a *action) bool {
switch a.t {
case actIgnore, actStart, actClick:
case actResponse:
t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a))
case actBecome:
valid, list := t.buildPlusList(a.a, false)
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
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().
while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
*/
tui.TtyIn()
t.executor.Become(tui.TtyIn(), t.environ(), command)
}
case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
case actExecuteMulti:
t.executeCommand(a.a, true, false, false, false)
case actInvalid:
t.mutex.Unlock()
return false
case 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)
1 year ago
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.pwindowSize(), t.evaluateScrollOffset(), list})
}
} else {
// Discard the preview content so that it won't accidentally appear
// when preview window is re-enabled and previewDelay is triggered
t.previewer.lines = nil
// Also kill the preview process if it's still running
t.cancelPreview()
}
}
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
t.previewOpts.wrap = !t.previewOpts.wrap
// Reset preview version so that full redraw occurs
t.previewed.version = 0
req(reqPreviewRefresh)
}
case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true, true)
t.promptString = prompt
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)
case actToggleSort:
t.sort = !t.sort
changed = true
case actPreviewTop:
if t.hasPreviewWindow() {
scrollPreviewTo(0)
}
case actPreviewBottom:
if t.hasPreviewWindow() {
scrollPreviewTo(len(t.previewer.lines) - t.pwindow.Height())
}
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-1)
}
case actPreviewDown:
if t.hasPreviewWindow() {
scrollPreviewBy(1)
}
case actPreviewPageUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-t.pwindow.Height())
}
case actPreviewPageDown:
if t.hasPreviewWindow() {
scrollPreviewBy(t.pwindow.Height())
}
case actPreviewHalfPageUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-t.pwindow.Height() / 2)
}
case actPreviewHalfPageDown:
if t.hasPreviewWindow() {
scrollPreviewBy(t.pwindow.Height() / 2)
}
case actBeginningOfLine:
t.cx = 0
t.xoffset = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actPrintQuery:
req(reqPrintQuery)
case actChangeMulti:
multi := t.multi
if a.a == "" {
multi = maxMulti
} else if n, e := strconv.Atoi(a.a); e == nil && n >= 0 {
multi = n
}
if t.multi > 0 && multi != t.multi {
t.selected = make(map[int32]selectedItem)
t.version++
}
t.multi = multi
req(reqList, reqInfo)
case 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:
t.borderLabelOpts.label = a.a
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actChangePreviewLabel:
t.previewLabelOpts.label = a.a
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actTransform:
body := t.executeCommand(a.a, false, true, true, false)
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
return doActions(actions)
case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true)
t.borderLabelOpts.label = label
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actTransformPreviewLabel:
label := t.executeCommand(a.a, false, true, true, true)
t.previewLabelOpts.label = label
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actChangePrompt:
t.promptString = a.a
t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt)
case actPreview:
if !t.hasPreviewWindow() {
updatePreviewWindow(true)
}
refreshPreview(a.a)
case actRefreshPreview:
refreshPreview(t.previewOpts.command)
case actReplaceQuery:
current := t.currentItem()
if current != nil {
t.input = current.text.ToRunes()
t.cx = len(t.input)
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEof:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actBackwardDeleteCharEof:
if len(t.input) == 0 {
req(reqQuit)
} else if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
beof = len(t.input) == 0
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi > 0 {
for i := 0; i < t.merger.Length(); i++ {
if !t.selectItem(t.merger.Get(i).item) {
break
}
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi > 0 {
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
t.deselectItem(t.merger.Get(i).item)
}
req(reqList, reqInfo)
}
case actClose:
if t.hasPreviewWindow() {
t.activePreviewOpts.Toggle()
updatePreviewWindow(false)
} else {
req(reqQuit)
}
case actSelect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
req(reqList, reqInfo)
}
case actDeselect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
req(reqList, reqInfo)
}
case actToggle:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
req(reqList)
}
case actToggleAll:
if t.multi > 0 {
prevIndexes := make(map[int]struct{})
for i := 0; i < t.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)
}
}
for i := 0; i < t.merger.Length(); i++ {
if _, found := prevIndexes[i]; !found {
item := t.merger.Get(i).item
if !t.selectItem(item) {
break
}
}
}
req(reqList, reqInfo)
}
case actToggleIn:
if t.layout != layoutDefault {
return doAction(&action{t: actToggleUp})
}
return doAction(&action{t: actToggleDown})
case actToggleOut:
if t.layout != layoutDefault {
return doAction(&action{t: actToggleDown})
}
return doAction(&action{t: actToggleUp})
case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
req(reqList)
}
case actToggleUp:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(1, true)
req(reqList)
}
case actDown:
t.vmove(-1, true)
9 years ago
req(reqList)
case actUp:
t.vmove(1, true)
9 years ago
req(reqList)
case actAccept:
req(reqClose)
case actAcceptNonEmpty:
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
req(reqClose)
}
case actAcceptOrPrintQuery:
if len(t.selected) > 0 || t.merger.Length() > 0 {
req(reqClose)
} else {
req(reqPrintQuery)
}
case actClearScreen:
req(reqFullRedraw)
case actClearQuery:
t.input = []rune{}
t.cx = 0
case actClearSelection:
if t.multi > 0 {
t.selected = make(map[int32]selectedItem)
t.version++
req(reqList, reqInfo)
}
case actFirst:
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)
case actUnixLineDiscard:
beof = len(t.input) == 0
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
beof = len(t.input) == 0
if t.cx > 0 {
t.rubout("\\s\\S")
10 years ago
}
case actBackwardKillWord:
beof = len(t.input) == 0
if t.cx > 0 {
t.rubout(t.wordRubout)
10 years ago
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems()-1, false)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1), false)
req(reqList)
case actHalfPageUp:
t.vmove(t.maxItems()/2, false)
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2), false)
req(reqList)
case actOffsetUp, actOffsetDown:
diff := 1
if a.t == actOffsetDown {
diff = -1
}
if t.layout == layoutReverse {
diff *= -1
}
t.offset += diff
before := t.offset
t.constrain()
if before != t.offset {
t.offset = before
if t.layout == layoutReverse {
diff *= -1
}
t.vmove(diff, false)
}
req(reqList)
case actJump:
t.jumping = jumpEnabled
req(reqJump)
case actJumpAccept:
t.jumping = jumpAcceptEnabled
req(reqJump)
case actBackwardWord:
t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actChar:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPrevHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = trimQuery(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = trimQuery(t.history.next())
t.cx = len(t.input)
}
case actToggleSearch:
t.paused = !t.paused
changed = !t.paused
req(reqPrompt)
case actToggleTrack:
switch t.track {
case trackEnabled:
t.track = trackDisabled
case trackDisabled:
t.track = trackEnabled
}
req(reqInfo)
case actToggleTrackCurrent:
switch t.track {
case trackCurrent:
t.track = trackDisabled
case trackDisabled:
t.track = trackCurrent
}
req(reqInfo)
case actShowHeader:
t.headerVisible = true
req(reqList, reqInfo, reqPrompt, reqHeader)
case actHideHeader:
t.headerVisible = false
req(reqList, reqInfo, reqPrompt, reqHeader)
case actToggleHeader:
t.headerVisible = !t.headerVisible
req(reqList, reqInfo, reqPrompt, reqHeader)
case actTrackCurrent:
if t.track == trackDisabled {
t.track = trackCurrent
}
req(reqInfo)
case actUntrackCurrent:
if t.track == trackCurrent {
t.track = trackDisabled
}
req(reqInfo)
case actEnableSearch:
t.paused = false
changed = true
req(reqPrompt)
case actDisableSearch:
t.paused = true
req(reqPrompt)
case actSigStop:
p, err := os.FindProcess(os.Getpid())
if err == nil {
t.sigstop = true
t.tui.Clear()
t.tui.Pause(t.fullscreen)
notifyStop(p)
t.mutex.Unlock()
return false
}
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
clicked := !wasDown && me.Down
wasDown = me.Down
if !me.Down {
barDragging = false
pbarDragging = false
previewDraggingPos = -1
}
// Scrolling
if me.S != 0 {
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
evt := tui.ScrollUp
if me.Mod {
evt = tui.SScrollUp
}
if me.S < 0 {
evt = tui.ScrollDown
if me.Mod {
evt = tui.SScrollDown
}
}
return doActions(actionsFor(evt))
} else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
evt := tui.PreviewScrollUp
if me.S < 0 {
evt = tui.PreviewScrollDown
}
return doActions(actionsFor(evt))
10 years ago
}
break
}
// Preview dragging
if me.Down && (previewDraggingPos >= 0 || clicked && t.hasPreviewWindow() && t.pwindow.Enclose(my, mx)) {
if previewDraggingPos > 0 {
scrollPreviewBy(previewDraggingPos - my)
}
previewDraggingPos = my
break
}
// Preview scrollbar dragging
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)
req(reqPreviewRefresh)
}
break
}
// Ignored
if !t.window.Enclose(my, mx) && !barDragging {
break
}
// Translate coordinates
mx -= t.window.Left()
my -= t.window.Top()
min := 2 + t.visibleHeaderLines()
if t.noSeparatorLine() {
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
}
}
// 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)
}
}
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))
}
}
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 {
t.vset(t.offset + my - min)
req(reqList)
evt := tui.RightClick
if me.Mod {
evt = tui.SRightClick
}
if me.Left {
evt = tui.LeftClick
if me.Mod {
evt = tui.SLeftClick
}
}
return doActions(actionsFor(evt))
}
10 years ago
}
case actReload, actReloadSync:
t.failed = nil
valid, list := t.buildPlusList(a.a, false)
if !valid {
// We run the command even when there's no match
// 1. If the template doesn't have any slots
// 2. If the template has {q}
slot, _, forceUpdate := hasPreviewFlags(a.a)
valid = !slot || forceUpdate
}
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
reloadSync = a.t == actReloadSync
t.reading = true
}
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
1 year ago
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) {
// Preview command can be running in the background if the size of
// the preview window is 0 but not 'hidden'
wasHidden := currentPreviewOpts.hidden
updatePreviewWindow(false)
1 year ago
if wasHidden && t.hasPreviewWindow() {
// Restart
refreshPreview(t.previewOpts.command)
} else if t.previewOpts.hidden {
// Cancel
t.cancelPreview()
} else {
// Refresh
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
}
}
}
10 years ago
}
if !processExecution(a.t) {
t.lastAction = a.t
}
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
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList)
}
if len(actions) == 0 {
actions = t.keymap[event.Comparable()]
}
if len(actions) == 0 && event.Type == tui.Rune {
doAction(&action{t: actChar})
} else if !doActions(actions) {
continue
}
t.truncateQuery()
queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
continue
}
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
continue
}
} else {
jumpEvent := tui.JumpCancel
if event.Type == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
jumpEvent = tui.Jump
t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled {
req(reqClose)
}
}
}
t.jumping = jumpDisabled
if acts, prs := t.keymap[jumpEvent.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList)
}
10 years ago
1 year ago
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
_, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
if forceUpdate {
1 year ago
t.version++
}
10 years ago
}
if queryChanged || t.cx != previousCx {
req(reqPrompt)
}
reload := changed || newCommand != nil
var reloadRequest *searchRequest
if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, environ: t.environ(), changed: changed}
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
if reload {
t.eventBox.Set(EvtSearchNew, *reloadRequest)
}
10 years ago
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
height := t.maxItems()
10 years ago
t.cy = util.Constrain(t.cy, 0, util.Max(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
}
}
10 years ago
}
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
}
}
10 years ago
}
t.vset(dest)
}
func (t *Terminal) vset(o int) bool {
t.cy = util.Constrain(o, 0, t.merger.Length()-1)
return t.cy == o
10 years ago
}
func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - t.visibleHeaderLines()
if t.noSeparatorLine() {
9 years ago
max++
}
return util.Max(max, 0)
10 years ago
}
func (t *Terminal) dumpItem(i *Item) StatusItem {
if i == nil {
return StatusItem{}
}
return StatusItem{
Index: int(i.Index()),
Text: i.AsString(t.ansi),
}
}
func (t *Terminal) dumpStatus(params getParams) string {
selectedItems := t.sortSelected()
selected := make([]StatusItem, util.Max(0, util.Min(params.limit, len(selectedItems)-params.offset)))
for i := range selected {
selected[i] = t.dumpItem(selectedItems[i+params.offset].item)
}
matches := make([]StatusItem, util.Max(0, util.Min(params.limit, t.merger.Length()-params.offset)))
for i := range matches {
matches[i] = t.dumpItem(t.merger.Get(i + params.offset).item)
}
var current *StatusItem
currentItem := t.currentItem()
if currentItem != nil {
item := t.dumpItem(currentItem)
current = &item
}
dump := Status{
Reading: t.reading,
Progress: t.progress,
Query: string(t.input),
Position: t.cy,
Sort: t.sort,
TotalCount: t.count,
MatchCount: t.merger.Length(),
Current: current,
Matches: matches,
Selected: selected,
}
bytes, _ := json.Marshal(&dump) // TODO: Errors?
return string(bytes)
}