Module: FzfRuby

Defined in:
lib/fzf/version.rb,
lib/fzf.rb,
lib/fzf/tasks.rb,
lib/fzf/platform.rb,
ext/fzf/extconf.rb

Overview

Gem version constant

Defined Under Namespace

Modules: Installer, Platform, Tasks Classes: FzfNotFoundError

Constant Summary collapse

VENDORED_BINARY =

Binary installed with the gem itself by ext/fzf/extconf.rb.

File.join(__dir__, "fzf", "bin", "fzf").freeze
OPTION_FLAGS =

Maps Ruby keyword options to fzf CLI flags. Organized by category, mirroring the sections of fzf --help (v0.73).

Every flag fzf accepts should have exactly one entry here — this hash is the single source of truth for the supported option surface. Adding a flag means adding a line here, plus listing it in BOOLEAN_OPTIONS or OPTIONAL_VALUE_OPTIONS if it is not a plain required-value flag.

{
  # --- Selection ---
  multi:               "--multi",
  accept_nth:          "--accept-nth",           # fields to print on accept
  id_nth:              "--id-nth",               # item identity fields for reloads

  # --- Search behavior ---
  query:               "--query",                # prefill the query string
  filter:              "--filter",               # non-interactive filter mode
  delimiter:           "--delimiter",
  nth:                 "--nth",                  # fields to match against
  with_nth:            "--with-nth",             # fields to display
  scheme:              "--scheme",               # default | path | history
  tiebreak:            "--tiebreak",             # length | chunk | begin | end | index
  tail:                "--tail",                 # max items to keep in memory
  exact:               "--exact",
  literal:             "--literal",              # do not normalize latin letters
  disabled:            "--disabled",             # do not perform search
  ignore_case:         "--ignore-case",
  no_ignore_case:      "--no-ignore-case",       # force case-sensitive
  smart_case:          "--smart-case",           # fzf's default
  no_extended:         "--no-extended",          # disable extended-search syntax
  no_sort:             "--no-sort",
  tac:                 "--tac",                  # reverse input order
  track:               "--track",                # track selection across updates
  sync:                "--sync",                 # for multi-staged filtering
  cycle:               "--cycle",
  select_1:            "--select-1",             # auto-select if only one match
  exit_0:              "--exit-0",               # exit immediately if no match

  # --- Prompt / input line ---
  prompt:              "--prompt",
  pointer:             "--pointer",
  marker:              "--marker",
  marker_multi_line:   "--marker-multi-line",
  ghost:               "--ghost",                # placeholder shown on empty input
  no_input:            "--no-input",             # hide the input section
  input_border:        "--input-border",
  input_label:         "--input-label",
  input_label_pos:     "--input-label-pos",

  # --- Info line ---
  info:                "--info",                 # default | inline | hidden | inline-right
  info_command:        "--info-command",
  separator:           "--separator",
  no_separator:        "--no-separator",

  # --- Header ---
  header:              "--header",
  header_lines:        "--header-lines",         # treat first N input lines as header
  header_first:        "--header-first",         # print header above the prompt
  header_border:       "--header-border",
  header_lines_border: "--header-lines-border",
  header_label:        "--header-label",
  header_label_pos:    "--header-label-pos",

  # --- Footer ---
  footer:              "--footer",
  footer_border:       "--footer-border",
  footer_label:        "--footer-label",
  footer_label_pos:    "--footer-label-pos",

  # --- Preview ---
  preview:             "--preview",
  preview_window:      "--preview-window",
  preview_border:      "--preview-border",
  preview_label:       "--preview-label",
  preview_label_pos:   "--preview-label-pos",
  preview_wrap_sign:   "--preview-wrap-sign",

  # --- Layout / appearance ---
  height:              "--height",
  min_height:          "--min-height",           # floor when height is a percentage
  layout:              "--layout",               # default | reverse | reverse-list
  style:               "--style",                # default | minimal | full[:BORDER]
  border:              "--border",               # rounded | sharp | bold | double | none | ...
  border_label:        "--border-label",
  border_label_pos:    "--border-label-pos",
  list_border:         "--list-border",
  list_label:          "--list-label",
  list_label_pos:      "--list-label-pos",
  margin:              "--margin",               # TRBL | TB,RL | T,RL,B | T,R,B,L
  padding:             "--padding",              # same forms as margin
  color:               "--color",
  no_color:            "--no-color",
  no_bold:             "--no-bold",
  ansi:                "--ansi",                 # process ANSI color codes in input

  # --- List rendering ---
  highlight_line:      "--highlight-line",
  wrap:                "--wrap",                 # char | word
  wrap_sign:           "--wrap-sign",
  no_multi_line:       "--no-multi-line",        # only meaningful with --read0
  gap:                 "--gap",                  # blank lines between items
  gap_line:            "--gap-line",             # string drawn in each gap
  keep_right:          "--keep-right",
  no_hscroll:          "--no-hscroll",
  hscroll_off:         "--hscroll-off",
  scroll_off:          "--scroll-off",
  ellipsis:            "--ellipsis",
  tabstop:             "--tabstop",
  scrollbar:           "--scrollbar",
  no_scrollbar:        "--no-scrollbar",
  raw:                 "--raw",                  # show non-matching items
  gutter:              "--gutter",
  gutter_raw:          "--gutter-raw",
  freeze_left:         "--freeze-left",          # fields frozen on the left
  freeze_right:        "--freeze-right",

  # --- History ---
  history:             "--history",              # file path, not shell history
  history_size:        "--history-size",

  # --- Keybinds / navigation (bind gets special handling in build_command) ---
  bind:                "--bind",
  jump_labels:         "--jump-labels",
  filepath_word:       "--filepath-word",        # word movements respect path separators

  # --- Process / integration ---
  with_shell:          "--with-shell",           # shell used for child processes
  tmux:                "--tmux",                 # alias of --popup
  popup:               "--popup",                # requires tmux 3.3+ / Zellij 0.44+
  listen:              "--listen",               # HTTP server for remote actions
}.freeze
BOOLEAN_OPTIONS =

Bare flags that take no value — emitted only when the option is truthy.

%i[
  multi exact literal disabled ignore_case no_ignore_case smart_case
  no_extended no_sort tac track sync cycle select_1 exit_0 ansi
  no_input no_separator header_first no_color no_bold highlight_line
  no_multi_line keep_right no_hscroll no_scrollbar raw filepath_word
].freeze
OPTIONAL_VALUE_OPTIONS =

Flags whose value is optional (--flag or --flag=VALUE in fzf's help). Passing true emits the bare flag; any other value emits --flag=VALUE.

%i[
  border preview_border list_border input_border header_border
  header_lines_border footer_border wrap gap gap_line scrollbar
  tmux popup listen
].freeze
LIST_RESULT_OPTIONS =

Options that put fzf in a mode where it can emit more than one line: multi-select accepts several items, and filter mode prints every match. Any of these makes fzf() return an Array instead of a single String.

%i[multi filter].freeze
VERSION =
"0.1.1"

Class Method Summary collapse

Class Method Details

.build_command(options) ⇒ Array<String>

Build the fzf command as an array (avoids shell interpolation).

Values are always attached with = (--prompt=Pick> ) rather than as a separate argument. fzf accepts both forms, but the = form is unambiguous for values that begin with a dash (e.g. pointer: "->") and is required for distinguishing a bare optional-value flag from one carrying a value.

Parameters:

  • options (Hash)

    keyword options from the caller

Returns:

  • (Array<String>)

    command suitable for IO.popen



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/fzf.rb', line 282

def build_command(options)
  cmd = [FzfRuby.fzf_binary]

  options.each do |key, value|
    flag = OPTION_FLAGS[key]
    next unless flag # ignore unknown options silently

    if BOOLEAN_OPTIONS.include?(key)
      # Boolean flags are added only when truthy
      cmd << flag if value
    elsif value.nil? || value == false
      # Any option explicitly disabled is simply omitted
      next
    elsif OPTIONAL_VALUE_OPTIONS.include?(key)
      # `true` means "use fzf's default" — emit the flag with no value
      cmd << (value == true ? flag : "#{flag}=#{serialize_value(value)}")
    elsif key == :bind
      # Keybinds accept a string or a hash of { key_combo => action }
      cmd << "#{flag}=#{serialize_bind(value)}"
    else
      cmd << "#{flag}=#{serialize_value(value)}"
    end
  end

  cmd
end

.fzf(*menu_items, **options) ⇒ String, ...

Invoke fzf with the given menu items and options.

Parameters:

  • menu_items (Array<#to_s>)

    items to display in fzf (also accepts a single Array)

  • options (Hash)

    any option from OPTION_FLAGS — see that constant for the full list; unknown keys are ignored. Values follow three shapes:

    - keys in BOOLEAN_OPTIONS take true/false (multi:, exact:, cycle:, ...)
    - keys in OPTIONAL_VALUE_OPTIONS take true (fzf's default) or a value
    (border: true, border: :rounded, wrap: :word, gap: 1, tmux: "80%")
    - everything else takes a value (prompt:, header:, preview:, height:, ...)
    

    Symbol values are hyphenated for fzf (layout: :reverse_list → reverse-list); String values are passed through untouched. Notable options:

    multi:          [Boolean]  enable multi-select
    prompt:         [String]   custom prompt string
    header:         [String]   header text above the list
    preview:        [String]   shell command for preview pane ({} = current item)
    preview_window: [String]   preview pane layout (e.g. "right:50%:wrap")
    height:         [String]   window height (e.g. "40%", "20")
    layout:         [Symbol|String] :reverse, :reverse_list, or :default
    border:         [Boolean|Symbol|String] true, :rounded, :sharp, :none, etc.
    bind:           [String|Hash]  keybinds — hash form: { "ctrl-a" => "select-all" }
    query:          [String]   prefill the search query
    exact:          [Boolean]  exact (non-fuzzy) matching
    cycle:          [Boolean]  wrap around the list
    select_1:       [Boolean]  auto-select if only one match
    exit_0:         [Boolean]  exit immediately when no match

Returns:

  • (String)

    the selected item (single-select mode)

  • (Array<String>)

    selected items (multi:) or all matches (filter:)

  • (nil)

    if the user cancelled (Ctrl-C / Esc), or nothing matched



252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/fzf.rb', line 252

def fzf(*menu_items, **options)
  # Flatten in case caller passes an array instead of splatted args
  items = menu_items.flatten.map(&:to_s)

  cmd    = build_command(options)
  result = run_fzf(cmd, items)

  # Return nil on cancel, an array for the modes that can yield several
  # lines, a single string otherwise
  return nil if result.nil?
  list_result?(options) ? result : result.first
end

.fzf_binaryObject

Resolve the fzf binary path. Priority: FZF_PATH env var → project bin/fzf → gem's own bin → system PATH



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/fzf.rb', line 42

def self.fzf_binary
  # Explicit override via environment variable
  from_env = ENV["FZF_PATH"]
  return from_env if from_env && File.executable?(from_env)

  # Local project binary (installed via `rake fzf:install`)
  local = File.join(project_root, "bin", "fzf")
  return local if File.executable?(local)

  # Binary downloaded when the gem was installed
  return VENDORED_BINARY if File.executable?(VENDORED_BINARY)

  # Fall back to system PATH
  "fzf"
end

.list_result?(options) ⇒ Boolean

Does this option set put fzf in a mode that can return more than one line?

Parameters:

  • options (Hash)

    keyword options from the caller

Returns:

  • (Boolean)


269
270
271
# File 'lib/fzf.rb', line 269

def list_result?(options)
  LIST_RESULT_OPTIONS.any? { |key| options[key] }
end

.project_rootObject

Find the project root — the directory containing the Gemfile. Falls back to the current working directory.



60
61
62
63
64
65
66
# File 'lib/fzf.rb', line 60

def self.project_root
  if defined?(Bundler)
    Bundler.root.to_s
  else
    Dir.pwd
  end
end

.run_fzf(cmd, items) ⇒ Array<String>?

Pipe items into fzf via stdin and capture the selected lines.

fzf reads candidates from stdin and opens /dev/tty for interactive user input, so IO.popen works without additional TTY wiring.

Parameters:

  • cmd (Array<String>)

    the fzf command array

  • items (Array<String>)

    candidate lines

Returns:

  • (Array<String>, nil)

    selected lines, or nil on cancel/error



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/fzf.rb', line 346

def run_fzf(cmd, items)
  input = items.join("\n")

  output = IO.popen(cmd, "r+") do |io|
    io.write(input)
    io.close_write
    io.read
  end

  # fzf exit codes: 0 = ok, 1 = no match, 2 = error, 130 = cancelled
  return nil unless $?.success?

  selections = output.split("\n").reject(&:empty?)
  selections.empty? ? nil : selections
rescue Errno::ENOENT
  raise FzfNotFoundError,
    "fzf is not installed or not found in PATH. Install it: https://github.com/junegunn/fzf#installation"
end

.serialize_bind(value) ⇒ String

Serialize bind option to fzf's comma-separated key:action format.

Parameters:

  • value (String, Hash)

    either a raw bind string or a hash mapping key combos to actions (e.g. { "ctrl-a" => "select-all" })

Returns:

  • (String)

    fzf-compatible bind string



327
328
329
330
331
332
333
334
335
336
# File 'lib/fzf.rb', line 327

def serialize_bind(value)
  case value
  when Hash
    # Convert { "ctrl-a" => "select-all", "ctrl-d" => "deselect-all" }
    # into "ctrl-a:select-all,ctrl-d:deselect-all"
    value.map { |k, v| "#{k}:#{v}" }.join(",")
  else
    value.to_s
  end
end

.serialize_value(value) ⇒ String

Serialize an option value for the command line.

Symbols are treated as Ruby-flavoured spellings of fzf's hyphenated enum values (layout: :reverse_list → "reverse-list"). Every other type is passed through verbatim — strings are user data (labels, paths, preview commands) and must never be rewritten.

Parameters:

  • value (Object)

    the raw option value

Returns:

  • (String)

    the value as fzf expects it



318
319
320
# File 'lib/fzf.rb', line 318

def serialize_value(value)
  value.is_a?(Symbol) ? value.to_s.tr("_", "-") : value.to_s
end