diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d51c753c7..c4250b425de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,13 @@ CHANGELOG ``` - `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior. - `kill` completion for bash and zsh were updated to use this feature +- Added `--no-input` option to completely disable and hide the input section + ```sh + # Click header to trigger search + fzf --header '[src] [test]' --no-input --layout reverse \ + --header-border bottom --input-border \ + --bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}' + ``` - Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc. - Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result. ```sh diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ad205920900..3a8a107f613 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -620,6 +620,11 @@ Position of the list label .SS INPUT SECTION +.TP +.B "\-\-no\-input" +Disable and hide the input section. You can no longer type in queries. To +trigger a search, use \fBsearch\fR action. + .TP .BI "\-\-prompt=" "STR" Input prompt (default: '> ') diff --git a/src/options.go b/src/options.go index cc627ee1561..fec9596c433 100644 --- a/src/options.go +++ b/src/options.go @@ -127,6 +127,7 @@ Usage: fzf [options] (default: 0 or center) INPUT SECTION + --no-input Disable and hide the input section --prompt=STR Input prompt (default: '> ') --info=STYLE Finder info style [default|right|hidden|inline[-right][:PREFIX]] @@ -538,6 +539,7 @@ type Options struct { Scheme string Extended bool Phony bool + Inputless bool Case Case Normalize bool Nth []Range @@ -659,6 +661,7 @@ func defaultOptions() *Options { Scheme: "", // Unknown Extended: true, Phony: false, + Inputless: false, Case: CaseSmart, Normalize: true, Nth: make([]Range, 0), @@ -2315,6 +2318,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { opts.Phony = false case "--disabled", "--phony": opts.Phony = true + case "--no-input": + opts.Inputless = true case "--tiebreak": str, err := nextString("sort criterion required") if err != nil { @@ -3064,6 +3069,9 @@ func noSeparatorLine(style infoStyle, separator bool) bool { } func (opts *Options) noSeparatorLine() bool { + if opts.Inputless { + return true + } sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0 return noSeparatorLine(opts.InfoStyle, sep) } @@ -3235,7 +3243,13 @@ func postProcessOptions(opts *Options) error { // Sets --min-height automatically if opts.Height.size > 0 && opts.Height.percent && opts.MinHeight < 0 { - opts.MinHeight = -opts.MinHeight + 1 + borderLines(opts.BorderShape) + borderLines(opts.ListBorderShape) + borderLines(opts.InputBorderShape) + opts.MinHeight = -opts.MinHeight + borderLines(opts.BorderShape) + borderLines(opts.ListBorderShape) + if !opts.Inputless { + opts.MinHeight += 1 + borderLines(opts.InputBorderShape) + if !opts.noSeparatorLine() { + opts.MinHeight++ + } + } if len(opts.Header) > 0 { opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header) } @@ -3246,9 +3260,6 @@ func postProcessOptions(opts *Options) error { } opts.MinHeight += borderLines(borderShape) + opts.HeaderLines } - if !opts.noSeparatorLine() { - opts.MinHeight++ - } if len(opts.Preview.command) > 0 && (opts.Preview.position == posUp || opts.Preview.position == posDown) && opts.Preview.Visible() && opts.Preview.position == posUp { borderShape := opts.Preview.border if opts.Preview.border == tui.BorderLine { diff --git a/src/terminal.go b/src/terminal.go index da3b863c738..b63af45b4c2 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -324,6 +324,7 @@ type Terminal struct { cleanExit bool executor *util.Executor paused bool + inputless bool border tui.Window window tui.Window inputWindow tui.Window @@ -810,6 +811,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor if err != nil { return nil, err } + if opts.Inputless { + renderer.HideCursor() + } wordRubout := "[^\\pL\\pN][\\pL\\pN]" wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)" if opts.FileWord { @@ -887,6 +891,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor cleanExit: opts.ClearOnExit, executor: executor, paused: opts.Phony, + inputless: opts.Inputless, cycle: opts.Cycle, highlightLine: opts.CursorLine, headerVisible: true, @@ -1124,9 +1129,15 @@ func (t *Terminal) visibleHeaderLinesInList() int { // Extra number of lines needed to display fzf func (t *Terminal) extraLines() int { - extra := 1 - if t.inputBorderShape.Visible() { - extra += borderLines(t.inputBorderShape) + extra := 0 + if !t.inputless { + extra++ + if !t.noSeparatorLine() { + extra++ + } + if t.inputBorderShape.Visible() { + extra += borderLines(t.inputBorderShape) + } } if t.listBorderShape.Visible() { extra += borderLines(t.listBorderShape) @@ -1141,9 +1152,6 @@ func (t *Terminal) extraLines() int { } extra += t.headerLines } - if !t.noSeparatorLine() { - extra++ - } return extra } @@ -1265,7 +1273,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) { } func (t *Terminal) noSeparatorLine() bool { - return noSeparatorLine(t.infoStyle, t.separatorLen > 0) + return t.inputless || noSeparatorLine(t.infoStyle, t.separatorLen > 0) } func getScrollbar(perLine int, total int, height int, offset int) (int, int) { @@ -1350,7 +1358,10 @@ func (t *Terminal) Input() (bool, []rune) { t.mutex.Lock() defer t.mutex.Unlock() paused := t.paused - src := t.input + var src []rune + if !t.inputless { + src = t.input + } if t.inputOverride != nil { paused = false src = *t.inputOverride @@ -1635,8 +1646,11 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { minAreaWidth := minWidth minAreaHeight := minHeight + if t.inputless { + minAreaHeight-- + } if t.noSeparatorLine() { - minAreaHeight -= 1 + minAreaHeight-- } if t.needPreviewWindow() { minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts) @@ -1756,7 +1770,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { shrink := 0 hasHeaderWindow := t.hasHeaderWindow() hasHeaderLinesWindow := t.hasHeaderLinesWindow() - hasInputWindow := t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow + hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow) if hasInputWindow { inputWindowHeight := 2 if t.noSeparatorLine() { @@ -1873,6 +1887,9 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) { switch previewOpts.position { case posUp, posDown: minWindowHeight := minHeight + if t.inputless { + minWindowHeight-- + } if t.noSeparatorLine() { minWindowHeight-- } @@ -2227,6 +2244,9 @@ func (t *Terminal) promptLine() int { } func (t *Terminal) placeCursor() { + if t.inputless { + return + } if t.inputWindow != nil { y := t.inputWindow.Height() - 1 if t.layout == layoutReverse { @@ -2239,6 +2259,9 @@ func (t *Terminal) placeCursor() { } func (t *Terminal) printPrompt() { + if t.inputless { + return + } w := t.window if t.inputWindow != nil { w = t.inputWindow @@ -2266,6 +2289,9 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string { } func (t *Terminal) printInfo() { + if t.inputless { + return + } t.withWindow(t.inputWindow, func() { t.printInfoImpl() }) } @@ -2509,7 +2535,7 @@ func (t *Terminal) headerIndent(borderShape tui.BorderShape) int { func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []string) { max := t.window.Height() - if t.inputWindow == nil && window == nil && t.headerFirst { + if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst { max-- if !t.noSeparatorLine() { max-- @@ -2539,7 +2565,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap if needReverse && idx < len(lines1) { line = len(lines1) - idx - 1 } - if t.inputWindow == nil && window == nil && !t.headerFirst { + if !t.inputless && t.inputWindow == nil && window == nil && !t.headerFirst { line++ if !t.noSeparatorLine() { line++ @@ -5681,7 +5707,7 @@ func (t *Terminal) Loop() error { // Header numLines := t.visibleHeaderLinesInList() lineOffset := 0 - if t.inputWindow == nil && !t.headerFirst { + if !t.inputless && t.inputWindow == nil && !t.headerFirst { // offset for info line if t.noSeparatorLine() { lineOffset = 1 @@ -5829,7 +5855,13 @@ func (t *Terminal) Loop() error { } else if !doActions(actions) { continue } - t.truncateQuery() + if t.inputless { + // Always just discard the change + t.input = previousInput + t.cx = len(t.input) + } else { + t.truncateQuery() + } queryChanged = string(previousInput) != string(t.input) if queryChanged { t.inputOverride = nil @@ -6016,6 +6048,9 @@ func (t *Terminal) vset(o int) bool { // Number of prompt lines in the list window func (t *Terminal) promptLines() int { + if t.inputless { + return 0 + } if t.inputWindow != nil { return 0 } diff --git a/src/tui/dummy.go b/src/tui/dummy.go index 1cfb292e59a..8dd584578fc 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -45,6 +45,7 @@ func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false } func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false } func (r *FullscreenRenderer) Bell() {} +func (r *FullscreenRenderer) HideCursor() {} func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Size() TermSize { return TermSize{} } diff --git a/src/tui/light.go b/src/tui/light.go index 54c38c18676..7b40efbb744 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -77,7 +77,13 @@ func (r *LightRenderer) csi(code string) string { func (r *LightRenderer) flush() { if r.queued.Len() > 0 { - r.flushRaw("\x1b[?7l\x1b[?25l" + r.queued.String() + "\x1b[?25h\x1b[?7h") + raw := "\x1b[?7l\x1b[?25l" + r.queued.String() + if r.showCursor { + raw += "\x1b[?25h\x1b[?7h" + } else { + raw += "\x1b[?7h" + } + r.flushRaw(raw) r.queued.Reset() } } @@ -110,6 +116,7 @@ type LightRenderer struct { y int x int maxHeightFunc func(int) int + showCursor bool // Windows only ttyinChannel chan byte @@ -152,7 +159,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse tabstop: tabstop, fullscreen: fullscreen, upOneLine: false, - maxHeightFunc: maxHeightFunc} + maxHeightFunc: maxHeightFunc, + showCursor: true} return &r, nil } @@ -759,6 +767,9 @@ func (r *LightRenderer) Close() { } else if !r.fullscreen { r.csi("u") } + if !r.showCursor { + r.csi("?25h") + } r.disableMouse() r.flush() r.closePlatform() @@ -1214,3 +1225,7 @@ func (w *LightWindow) Erase() { func (w *LightWindow) EraseMaybe() bool { return false } + +func (r *LightRenderer) HideCursor() { + r.showCursor = false +} diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3738214a920..9d6fde80b39 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -52,6 +52,7 @@ type TcellWindow struct { borderStyle BorderStyle uri *string params *string + showCursor bool } func (w *TcellWindow) Top() int { @@ -72,7 +73,9 @@ func (w *TcellWindow) Height() int { func (w *TcellWindow) Refresh() { if w.moveCursor { - _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + if w.showCursor { + _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + } w.moveCursor = false } w.lastX = 0 @@ -104,6 +107,10 @@ func (r *FullscreenRenderer) Bell() { _screen.Beep() } +func (r *FullscreenRenderer) HideCursor() { + r.showCursor = false +} + func (r *FullscreenRenderer) PassThrough(str string) { // No-op // https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846 @@ -168,6 +175,9 @@ func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) { if e != nil { return nil, e } + if !r.showCursor { + s.HideCursor() + } _screen = s } return _screen, nil @@ -590,7 +600,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, width: width, height: height, normal: normal, - borderStyle: borderStyle} + borderStyle: borderStyle, + showCursor: r.showCursor} w.Erase() return w } diff --git a/src/tui/tui.go b/src/tui/tui.go index fe8fc243700..0c778ad6b6e 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -615,6 +615,7 @@ type Renderer interface { NeedScrollbarRedraw() bool ShouldEmitResizeEvent() bool Bell() + HideCursor() GetChar() Event @@ -662,6 +663,7 @@ type FullscreenRenderer struct { forceBlack bool prevDownTime time.Time clicks [][2]int + showCursor bool } func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { @@ -670,7 +672,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende mouse: mouse, forceBlack: forceBlack, prevDownTime: time.Unix(0, 0), - clicks: [][2]int{}} + clicks: [][2]int{}, + showCursor: true} return r } diff --git a/test/test_layout.rb b/test/test_layout.rb index dcfe1d78684..6e844527ef1 100644 --- a/test/test_layout.rb +++ b/test/test_layout.rb @@ -876,4 +876,19 @@ def test_min_height_auto BLOCK tmux.until { assert_block(block, _1) } end + + def test_min_height_auto_no_input + tmux.send_keys %(seq 100 | #{FZF} --style full:sharp --no-input --height 1% --min-height 5+), :Enter + + block = <<~BLOCK + ┌───────── + │ 5 + │ 4 + │ 3 + │ 2 + │ > 1 + └───────── + BLOCK + tmux.until { assert_block(block, _1) } + end end