Module: Booker::Picker

Defined in:
lib/booker/picker.rb

Constant Summary collapse

DEFAULT =

--delimiter/--with-nth hide the id column: it is noise on screen, and hiding it also takes it out of the match, so typing "2_" does not pull up every bookmark from the second source. --no-sort keeps booker's ordering.

deliberately no --multi: under it fzf reads tab as "mark this and move down" - the one key booker's own completion trained everybody to press - so a couple of taps to scroll would mark bookmarks silently and open them all on the next return.

a plain array literal, not %w: neither the real tab in the delimiter nor the prompt's trailing space survives %w

[
  "fzf",
  "--height=40%",
  "--reverse",
  "--no-sort",
  "--delimiter=\t",
  "--with-nth=2..",
  "--prompt=bookmark> "
].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.enabled=(value) ⇒ Object (writeonly)

nil means decide from the environment; true or false forces it. the specs set this, and so does --list - same shape as Colors.enabled=



37
38
39
# File 'lib/booker/picker.rb', line 37

def enabled=(value)
  @enabled = value
end

Class Method Details

.commandObject

the configured picker, or fzf when it is on PATH. nil when neither is installed, which is the signal to stay on booker's pre-picker behavior



63
64
65
66
67
68
# File 'lib/booker/picker.rb', line 63

def command
  return @command if @command_known

  @command_known = true
  @command = resolve_command
end

.enabled?Boolean

Returns:

  • (Boolean)


48
49
50
51
52
53
54
55
# File 'lib/booker/picker.rb', line 48

def enabled?
  return @enabled unless @enabled.nil?

  # tty? first: it is a pair of syscalls, where #command reads and parses
  # ~/.booker.yml, and a run that is not interactive never needs to know
  # whether a finder is installed
  tty? && !command.nil?
end

.executable?(path) ⇒ Boolean

Returns:

  • (Boolean)


84
# File 'lib/booker/picker.rb', line 84

def executable?(path) = File.executable?(path) && !File.directory?(path)

.reset!Object

#enabled? and #select both ask for the command, so an interactive run worked it out twice; both memos outlive an example, so the suite drops them between them



42
43
44
45
46
# File 'lib/booker/picker.rb', line 42

def reset!
  @enabled = nil
  @command = nil
  @command_known = false
end

.select(lines) ⇒ Object

candidates in, the id of what was picked out - or nil for every other outcome: cancelled, nothing matched, no usable finder. one id and not a list, since dropping --multi above only covers the default finder and not one named in ~/.booker.yml



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/booker/picker.rb', line 90

def select(lines)
  argv = command
  return nil if argv.nil?

  io = IO.popen(argv, "r+")
  feed(io, lines)
  chosen = io.read
  io.close

  # fzf: 0 selected, 1 no match, 2 error, 130 interrupted. a selection
  # that came back empty counts as a cancel too - that pairs with the
  # finder exiting 0 the moment it is closed, before it has read anything
  return nil unless Process.last_status&.exitstatus&.zero?

  id = chosen.lines.first.to_s.split("\t", 2).first.to_s.strip
  id unless id.empty?
rescue Errno::ENOENT, Interrupt
  # the binary went away between the PATH check and the spawn, or the
  # user interrupted booker itself rather than the finder
  nil
end

.tty?Boolean

both ends, not just stdout: booker foo | less still writes to a terminal, but there is no keyboard on the other side of the pipe

Returns:

  • (Boolean)


59
# File 'lib/booker/picker.rb', line 59

def tty? = $stdin.tty? && $stdout.tty?

.which(bin) ⇒ Object

no subshell to which: this runs before every interactive invocation, and the answer is a PATH scan booker can do itself. a name carrying a separator is a path, exactly as execvp treats it



73
74
75
76
77
78
79
80
81
82
# File 'lib/booker/picker.rb', line 73

def which(bin)
  return executable?(bin) ? bin : nil if bin.include?(File::SEPARATOR)

  # lazy: the answer is usually in the first few entries, and eagerly
  # joining all forty PATH directories to throw them away is the one
  # thing a hand written which is here to avoid
  ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).lazy
    .map { |dir| File.join(dir, bin) }
    .find { |path| executable?(path) }
end