Repository: junegunn/fzf (language: go)

Issue:
Hide search interface in --phony mode
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->

<!-- Check all that apply [x] -->

- [x] I have read through the manual page (`man fzf`)
- [ ] I have the latest version of fzf
- [x] I have searched through the existing issues

## Info

- OS
    - [x] Linux
    - [ ] Mac OS X
    - [ ] Windows
    - [ ] Etc.
- Shell
    - [x] bash
    - [ ] zsh
    - [ ] fish

## Problem / Steps to reproduce

In `--phony` mode, "fzf becomes a simple selector interface" (says manual). The search prompt becomes useless, but it still accepts text. A bit confusing and ugly. 

The hack of `--bind='change:clear-query'` helps a bit but the surely the best UX is just to disable and hide the whole line when in `--phony` mode?

Thanks for considering, and for your work on this amazing tool.

_Version 0.20.0 Ubuntu LTS_


Discussion / hints:
Related: #2159

So the thing is, I added `--phony` (now `--disabled` is the official name) to make `change:reload` bindings usable. Making fzf a simple, non-searchable menu was not a part of the plan.

* https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher
Fair enough. And sorry for missing the existing issue. Will try the `smenu` you mentioned there, maybe it's a better fit

The problem is that the zombie search box makes the feature feel incomplete. If non-searchability is really out of scope, then maybe it would be more coherent to just revert the whole feature.

Anyway, it's your project. Go ahead and mark this as `wontfix` or similar. Thanks.
> If non-searchability is really out of scope, then maybe it would be more coherent to just revert the whole feature.

The option has valid use cases as shown above; search functionality is delegated to an external process started by 
`change:reload` binding.

* See https://github.com/search?q=fzf+disabled+change%3Areload&type=code

And there are creative use cases like this:

```sh
printf 'Definition\nSynonym\nAntonym' |
  fzf --reverse --disabled --preview 'if [ -n {q} ]; then echo {} of {q}: ...; else echo Type in a word; fi'
```

> makes the feature feel incomplete

True, especially when the user is not aware of such use cases. But we can't just change the way it works now. I'm going to leave this open and see if we can think of a nice idea to improve it.
Got round to trying `smenu`. It doesn't offer custom key bindings. :(

Maybe time to rename your project `sfzf` - it's too smart! ;)

FYI I'm using it as a very simple CRUD file manager. Having to go thru the Enter key for everything is a deal-breaker.
@Friptick, you could hide it a bit by adding `--no-info` and setting the prompt to the empty string `--prompt ""`. It will still take up a line, and show the cursor, but it's a bit less conspicuous.

With `--reverse` you can use the `--prompt "Heading "` as a sort of heading. 🙂 

Adding `--padding 0,1,1,1` makes it almost invisible. 😉
Ugh, all so hacky! :laughing:  Except maybe the second `--prompt` idea. That one's my usual approach in this situation - pretend to myself that the ugly side effect is in fact a feature :rofl:

More seriously: nice suggestions, thanks.

In this case BTW I decided in the end to to drop `--phony` mode and just use the tool as God (or @junegunn) intended.
Yeah, "creative solutions" is kind of my speciality! 😅 

In my specific case `--layout reverse-list` works great. It puts the content at the top, but the (empty) prompt and cursor at the bottom. I also incoporated the `--bind='change:clear-query'` parameter you mentioned above. So, thanks! 👍 

Fix patch:
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
 }
 


Test patch:
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