Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
philc committed Apr 11, 2010
0 parents commit ebaebaa
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 0 deletions.
118 changes: 118 additions & 0 deletions fuzzycd.rb
@@ -0,0 +1,118 @@
#!/usr/bin/env ruby
# Returns a string representing a color-coded menu which presents each match as a choice.
# This uses flexible width columns, because fixed-width columns turn out to not look so good.
# Example output: 1.notes.git 2.projects.git
def menu_with_options(options)
columns = `tput cols`.to_i
output = []
current_line = ""
options.each_with_index do |option, i|
option = option.sub(ENV["HOME"], "~")
option_text = "#{i + 1}.#{colorize_blue(option)}"
if current_line.size + (option.size + i.to_s.size) >= columns - 1
output.push(current_line)
current_line = option_text
else
current_line += (current_line.empty? ? "#{option_text}" : " #{option_text}")
end
end
output.push(current_line)
output.join("\n") + " "
end

# Inserts bash color escape codes to render the given text in blue.
def colorize_blue(text)
"\e[34m" + text + "\e[0m"
end

# Presents all of the given options in a menu and collects input over STDIN. Returns the chosen option,
# or nil if the user's input was invalid or they hit CTRL+C.
def present_menu_with_options(options)
begin
original_terminal_state = `stty -g`
print menu_with_options(options)
# Put the terminal in raw mode so we can capture one keypress at a time instead of waiting for enter.
`stty raw -echo`
input = STDIN.getc.chr

ctrl_c = "\003"
return nil if input == ctrl_c

# We may require two characters for lists with many choices. If the second character is "enter" (10),
# ignore it.
if options.length > 9
char = STDIN.getc.chr
input += char unless (char == 10)
end

# we require numeric input.
return nil unless /^\d+$/ =~ input

choice = input.to_i
return nil unless (choice >= 1 && choice <= options.length)

return options[choice - 1]
ensure
system `stty #{original_terminal_state}`
print "\n"
end
end

# Returns an array of all matches for a given path. Each part of the path is a globed (fuzzy) match.
# For example:
# "p" matches "places/" and "suspects/"
# "p/h" matches "places/home" and "suspects/harry"
def matches_for_path(path)
# Build up a glob string for each component of the path to make something like: "*p*/*h*".
# Avoid adding asterisks around each piece of HOME if the path starts with ~, like: /home/philc/*p*/*h*
root = ""
if (path.index(ENV["HOME"]) == 0)
root = ENV["HOME"] + "/"
path.sub!(root, "")
else
# Ignore the initial ../ if the path is rooted with ../, as well as a few other special cases that we
# do not wish to include in the glob expression.
special_roots = ["./", "../", "/"]
special_roots.each do |special_root|
next unless path.index(special_root) == 0
root = special_root
path.sub!(root, "")
break
end
end

glob_expression = "*" + path.gsub("/", "*/*") + "*"
Dir.glob(root + glob_expression, File::FNM_CASEFOLD).select { |file| File.directory?(file) }
end


# Communicate with the shell wrapper using a temp file instead of STDOUT, since we want to be able to
# show our own interactive menu over STDOUT without confusing the shell wrapper with that output.
@out = File.open("/tmp/fuzzycd.rb.out", "w")
path = ARGV.join(" ")

# When no path is provided, just invoke 'cd' directly without arguments, which usually navigates to ~.
if path.nil?
@out.puts "@passthrough"
exit
end

# When the path ends in "/" and for other special-case paths, just let cd handle it directly.
if path == "." || path == ".." || path == "/" || path.rindex("/") == path.size - 1 || path == ENV["HOME"]
@out.puts "@passthrough"
exit
end

matches = matches_for_path(path)

if matches.size == 1
@out.puts matches.first
elsif matches.size == 0
@out.puts "@nomatches"
elsif matches.size >= 100
puts "There are more than 100 matches; be more specific."
@out.puts "@exit"
else
choice = present_menu_with_options(matches)
@out.puts(choice.nil? ? "@exit" : choice)
end
17 changes: 17 additions & 0 deletions fuzzycd_bash_wrapper.sh
@@ -0,0 +1,17 @@
# This overrides the normal bash "cd" command and invokes the fuzzycd command instead.
# This displaces the normal bash "cd" builtin command. cd needs to be redefined this way because it's not
# a normal binary, but rather a Bash builtin.
cd() {
# This assumes that fuzzycd.rb is available somewhere in your PATH.
fuzzycd.rb $*
# fuzzycd.rb communicates to its bash wrapper through a temp file, because it uses STDOUT for other purposes.
output=`cat /tmp/fuzzycd.rb.out`
`rm /tmp/fuzzycd.rb.out`
if [ "$output" = "@nomatches" ]; then
echo "No files match \"$*\""
elif [ "$output" = "@passthrough" ]; then
builtin cd $*
elif [ "$output" != "@exit" ]; then
builtin cd "$output"
fi
}

0 comments on commit ebaebaa

Please sign in to comment.