Add --no-input to hide the input section (#4210)

Close #2890
Close #1396
 
You can't type in queries in this mode, and the only way to trigger an
fzf search is to use `search(...)` action.

  # 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}'
This commit is contained in:
Junegunn Choi
2025-01-30 00:50:46 +09:00
committed by GitHub
parent 6b5d461411
commit 6c0ca4a64a
9 changed files with 126 additions and 23 deletions

View File

@@ -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. - `$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 - `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. - 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. - 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 ```sh

View File

@@ -620,6 +620,11 @@ Position of the list label
.SS INPUT SECTION .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 .TP
.BI "\-\-prompt=" "STR" .BI "\-\-prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')

View File

@@ -127,6 +127,7 @@ Usage: fzf [options]
(default: 0 or center) (default: 0 or center)
INPUT SECTION INPUT SECTION
--no-input Disable and hide the input section
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--info=STYLE Finder info style --info=STYLE Finder info style
[default|right|hidden|inline[-right][:PREFIX]] [default|right|hidden|inline[-right][:PREFIX]]
@@ -538,6 +539,7 @@ type Options struct {
Scheme string Scheme string
Extended bool Extended bool
Phony bool Phony bool
Inputless bool
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
@@ -659,6 +661,7 @@ func defaultOptions() *Options {
Scheme: "", // Unknown Scheme: "", // Unknown
Extended: true, Extended: true,
Phony: false, Phony: false,
Inputless: false,
Case: CaseSmart, Case: CaseSmart,
Normalize: true, Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
@@ -2315,6 +2318,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Phony = false opts.Phony = false
case "--disabled", "--phony": case "--disabled", "--phony":
opts.Phony = true opts.Phony = true
case "--no-input":
opts.Inputless = true
case "--tiebreak": case "--tiebreak":
str, err := nextString("sort criterion required") str, err := nextString("sort criterion required")
if err != nil { if err != nil {
@@ -3064,6 +3069,9 @@ func noSeparatorLine(style infoStyle, separator bool) bool {
} }
func (opts *Options) noSeparatorLine() 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 sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
return noSeparatorLine(opts.InfoStyle, sep) return noSeparatorLine(opts.InfoStyle, sep)
} }
@@ -3235,7 +3243,13 @@ func postProcessOptions(opts *Options) error {
// Sets --min-height automatically // Sets --min-height automatically
if opts.Height.size > 0 && opts.Height.percent && opts.MinHeight < 0 { 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 { if len(opts.Header) > 0 {
opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header) opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header)
} }
@@ -3246,9 +3260,6 @@ func postProcessOptions(opts *Options) error {
} }
opts.MinHeight += borderLines(borderShape) + opts.HeaderLines 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 { 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 borderShape := opts.Preview.border
if opts.Preview.border == tui.BorderLine { if opts.Preview.border == tui.BorderLine {

View File

@@ -324,6 +324,7 @@ type Terminal struct {
cleanExit bool cleanExit bool
executor *util.Executor executor *util.Executor
paused bool paused bool
inputless bool
border tui.Window border tui.Window
window tui.Window window tui.Window
inputWindow tui.Window inputWindow tui.Window
@@ -810,6 +811,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
if err != nil { if err != nil {
return nil, err return nil, err
} }
if opts.Inputless {
renderer.HideCursor()
}
wordRubout := "[^\\pL\\pN][\\pL\\pN]" wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)" wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
if opts.FileWord { if opts.FileWord {
@@ -887,6 +891,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
executor: executor, executor: executor,
paused: opts.Phony, paused: opts.Phony,
inputless: opts.Inputless,
cycle: opts.Cycle, cycle: opts.Cycle,
highlightLine: opts.CursorLine, highlightLine: opts.CursorLine,
headerVisible: true, headerVisible: true,
@@ -1124,9 +1129,15 @@ func (t *Terminal) visibleHeaderLinesInList() int {
// Extra number of lines needed to display fzf // Extra number of lines needed to display fzf
func (t *Terminal) extraLines() int { func (t *Terminal) extraLines() int {
extra := 1 extra := 0
if t.inputBorderShape.Visible() { if !t.inputless {
extra += borderLines(t.inputBorderShape) extra++
if !t.noSeparatorLine() {
extra++
}
if t.inputBorderShape.Visible() {
extra += borderLines(t.inputBorderShape)
}
} }
if t.listBorderShape.Visible() { if t.listBorderShape.Visible() {
extra += borderLines(t.listBorderShape) extra += borderLines(t.listBorderShape)
@@ -1141,9 +1152,6 @@ func (t *Terminal) extraLines() int {
} }
extra += t.headerLines extra += t.headerLines
} }
if !t.noSeparatorLine() {
extra++
}
return extra return extra
} }
@@ -1265,7 +1273,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
} }
func (t *Terminal) noSeparatorLine() bool { 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) { 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() t.mutex.Lock()
defer t.mutex.Unlock() defer t.mutex.Unlock()
paused := t.paused paused := t.paused
src := t.input var src []rune
if !t.inputless {
src = t.input
}
if t.inputOverride != nil { if t.inputOverride != nil {
paused = false paused = false
src = *t.inputOverride src = *t.inputOverride
@@ -1635,8 +1646,11 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
minAreaWidth := minWidth minAreaWidth := minWidth
minAreaHeight := minHeight minAreaHeight := minHeight
if t.inputless {
minAreaHeight--
}
if t.noSeparatorLine() { if t.noSeparatorLine() {
minAreaHeight -= 1 minAreaHeight--
} }
if t.needPreviewWindow() { if t.needPreviewWindow() {
minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts) minPreviewWidth, minPreviewHeight := t.minPreviewSize(t.activePreviewOpts)
@@ -1756,7 +1770,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
shrink := 0 shrink := 0
hasHeaderWindow := t.hasHeaderWindow() hasHeaderWindow := t.hasHeaderWindow()
hasHeaderLinesWindow := t.hasHeaderLinesWindow() hasHeaderLinesWindow := t.hasHeaderLinesWindow()
hasInputWindow := t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow hasInputWindow := !t.inputless && (t.inputBorderShape.Visible() || hasHeaderWindow || hasHeaderLinesWindow)
if hasInputWindow { if hasInputWindow {
inputWindowHeight := 2 inputWindowHeight := 2
if t.noSeparatorLine() { if t.noSeparatorLine() {
@@ -1873,6 +1887,9 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
switch previewOpts.position { switch previewOpts.position {
case posUp, posDown: case posUp, posDown:
minWindowHeight := minHeight minWindowHeight := minHeight
if t.inputless {
minWindowHeight--
}
if t.noSeparatorLine() { if t.noSeparatorLine() {
minWindowHeight-- minWindowHeight--
} }
@@ -2227,6 +2244,9 @@ func (t *Terminal) promptLine() int {
} }
func (t *Terminal) placeCursor() { func (t *Terminal) placeCursor() {
if t.inputless {
return
}
if t.inputWindow != nil { if t.inputWindow != nil {
y := t.inputWindow.Height() - 1 y := t.inputWindow.Height() - 1
if t.layout == layoutReverse { if t.layout == layoutReverse {
@@ -2239,6 +2259,9 @@ func (t *Terminal) placeCursor() {
} }
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
if t.inputless {
return
}
w := t.window w := t.window
if t.inputWindow != nil { if t.inputWindow != nil {
w = t.inputWindow w = t.inputWindow
@@ -2266,6 +2289,9 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inputless {
return
}
t.withWindow(t.inputWindow, func() { t.printInfoImpl() }) 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) { func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []string) {
max := t.window.Height() max := t.window.Height()
if t.inputWindow == nil && window == nil && t.headerFirst { if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst {
max-- max--
if !t.noSeparatorLine() { if !t.noSeparatorLine() {
max-- max--
@@ -2539,7 +2565,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
if needReverse && idx < len(lines1) { if needReverse && idx < len(lines1) {
line = len(lines1) - idx - 1 line = len(lines1) - idx - 1
} }
if t.inputWindow == nil && window == nil && !t.headerFirst { if !t.inputless && t.inputWindow == nil && window == nil && !t.headerFirst {
line++ line++
if !t.noSeparatorLine() { if !t.noSeparatorLine() {
line++ line++
@@ -5681,7 +5707,7 @@ func (t *Terminal) Loop() error {
// Header // Header
numLines := t.visibleHeaderLinesInList() numLines := t.visibleHeaderLinesInList()
lineOffset := 0 lineOffset := 0
if t.inputWindow == nil && !t.headerFirst { if !t.inputless && t.inputWindow == nil && !t.headerFirst {
// offset for info line // offset for info line
if t.noSeparatorLine() { if t.noSeparatorLine() {
lineOffset = 1 lineOffset = 1
@@ -5829,7 +5855,13 @@ func (t *Terminal) Loop() error {
} else if !doActions(actions) { } else if !doActions(actions) {
continue 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) queryChanged = string(previousInput) != string(t.input)
if queryChanged { if queryChanged {
t.inputOverride = nil t.inputOverride = nil
@@ -6016,6 +6048,9 @@ func (t *Terminal) vset(o int) bool {
// Number of prompt lines in the list window // Number of prompt lines in the list window
func (t *Terminal) promptLines() int { func (t *Terminal) promptLines() int {
if t.inputless {
return 0
}
if t.inputWindow != nil { if t.inputWindow != nil {
return 0 return 0
} }

View File

@@ -45,6 +45,7 @@ func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false } func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false } func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Bell() {} func (r *FullscreenRenderer) Bell() {}
func (r *FullscreenRenderer) HideCursor() {}
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} } func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }

View File

@@ -77,7 +77,13 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { 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() r.queued.Reset()
} }
} }
@@ -110,6 +116,7 @@ type LightRenderer struct {
y int y int
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
showCursor bool
// Windows only // Windows only
ttyinChannel chan byte ttyinChannel chan byte
@@ -152,7 +159,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc,
showCursor: true}
return &r, nil return &r, nil
} }
@@ -759,6 +767,9 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen { } else if !r.fullscreen {
r.csi("u") r.csi("u")
} }
if !r.showCursor {
r.csi("?25h")
}
r.disableMouse() r.disableMouse()
r.flush() r.flush()
r.closePlatform() r.closePlatform()
@@ -1214,3 +1225,7 @@ func (w *LightWindow) Erase() {
func (w *LightWindow) EraseMaybe() bool { func (w *LightWindow) EraseMaybe() bool {
return false return false
} }
func (r *LightRenderer) HideCursor() {
r.showCursor = false
}

View File

@@ -52,6 +52,7 @@ type TcellWindow struct {
borderStyle BorderStyle borderStyle BorderStyle
uri *string uri *string
params *string params *string
showCursor bool
} }
func (w *TcellWindow) Top() int { func (w *TcellWindow) Top() int {
@@ -72,7 +73,9 @@ func (w *TcellWindow) Height() int {
func (w *TcellWindow) Refresh() { func (w *TcellWindow) Refresh() {
if w.moveCursor { 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.moveCursor = false
} }
w.lastX = 0 w.lastX = 0
@@ -104,6 +107,10 @@ func (r *FullscreenRenderer) Bell() {
_screen.Beep() _screen.Beep()
} }
func (r *FullscreenRenderer) HideCursor() {
r.showCursor = false
}
func (r *FullscreenRenderer) PassThrough(str string) { func (r *FullscreenRenderer) PassThrough(str string) {
// No-op // No-op
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846 // https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
@@ -168,6 +175,9 @@ func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
if e != nil { if e != nil {
return nil, e return nil, e
} }
if !r.showCursor {
s.HideCursor()
}
_screen = s _screen = s
} }
return _screen, nil return _screen, nil
@@ -590,7 +600,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
width: width, width: width,
height: height, height: height,
normal: normal, normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle,
showCursor: r.showCursor}
w.Erase() w.Erase()
return w return w
} }

View File

@@ -615,6 +615,7 @@ type Renderer interface {
NeedScrollbarRedraw() bool NeedScrollbarRedraw() bool
ShouldEmitResizeEvent() bool ShouldEmitResizeEvent() bool
Bell() Bell()
HideCursor()
GetChar() Event GetChar() Event
@@ -662,6 +663,7 @@ type FullscreenRenderer struct {
forceBlack bool forceBlack bool
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clicks [][2]int
showCursor bool
} }
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@@ -670,7 +672,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse, mouse: mouse,
forceBlack: forceBlack, forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0), prevDownTime: time.Unix(0, 0),
clicks: [][2]int{}} clicks: [][2]int{},
showCursor: true}
return r return r
} }

View File

@@ -876,4 +876,19 @@ class TestLayout < TestInteractive
BLOCK BLOCK
tmux.until { assert_block(block, _1) } tmux.until { assert_block(block, _1) }
end 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 end