#! /usr/bin/env ruby require 'json' require 'optparse' class FindRecentFiles def initialize @default_fd_filetype = 'f' @fd_allowed_filetypes = ["f", "d", "l", "s", "p", "x", "e"] @output_format = "text" @reverse = false @ignore_file = nil @filetypes = [] @max_results = -1 @changed_within = '7d' get_arguments check_paths end def run files = run_fd_command if @output_format == 'text' text_output(files) else json_output(files) end end private def get_arguments opts = OptionParser.new do |opts| opts.banner = "Usage: #$0 [options]" opts.on('-c', '--changed-within [STRING]', 'changed within 1h, 2d, 5min, etc.; the default is 7d') do |c| @changed_within = c end opts.on('-d', '--dir [STRING]', 'directory to start search from; the default is the current directory') do |d| @directory = d end opts.on('--fd-command [STRING]', 'path to the fd(1) command if not specified in $PATH') do |i| @fd_command = i end opts.on('-h') do puts opts exit end opts.on('-H', '--hidden', 'show hidden files; the default is not to show hidden files') do @show_hidden = true end opts.on('-i', '--ignore-file [STRING]', 'path to the Git-format ignore file for search exclusions, optional') do |i| @ignore_file = i end opts.on('-m', '--max-results [INT]', 'only return MAX_RESULTS items; the default is to return all results') do |m| @max_results = m.to_i end opts.on('-o', '--output-format [text or json]', 'output format, default is text; json is for use by Alfred') do |o| @output_format = o end opts.on('-r', '--reverse', 'reverse the sorting order; the default is newest files first') do @reverse = true end opts.on('-t', '--filetype [STRING]', 'filetype, as supported by fd; the default is \"f\"; the argument may be repeated or combined, so both (1) and (2)\ are allowed: (1) --filetype fd (2) --filetype f --filetype d') do |t| @filetypes << t end end opts.parse!(ARGV) if @filetypes.empty? @filetypes << @default_fd_filetype else @filetypes.each do |f| if not @fd_allowed_filetypes.include? f print "--filetype (or -t) must be one or more of: #{@fd_allowed_filetypes.join(',')}\n\n" puts opts exit 1 end end end # If there's a funky output format, ignore it if @output_format != 'json' @output_format = 'text' end @fd_command = 'fd' if @fd_command.nil? @directory = Dir.pwd if @directory.nil? end def check_paths if not @ignore_file.nil? if File.exist? @ignore_file @ignore_file = File.expand_path(@ignore_file) else print "Error: -i/--ignore-file file does not exist: #{@ignore_file}\n" exit 1 end end end def set_command_line_options options = ['-a'] options << '--changed-within' options << @changed_within @filetypes.each do |f| options << '-t' options << f end if @ignore_file options << '--ignore-file' options << @ignore_file end options << '-H' if @show_hidden return options end def run_fd_command options = set_command_line_options files = {} Dir.chdir(@directory) do IO.popen([@fd_command, *options]) do |subprocess| subprocess.read.split("\n").each do |line| files[line] = File.stat(line).mtime.to_f end end end return files.sort_by {|k, v| v} if @reverse return files.sort_by {|k, v| -v} end def text_output(data) data.each_with_index do |f, index| break if index == @max_results print("#{f[0]}\n") end end def json_output(data) files = [] result_count = 0 data.each do |file| break if result_count == @max_results result_count += 1 filename = file[0] record = { :type => "file", :title => filename, :subtitle => filename, :arg => filename, :icon => { :type => "fileicon", :path => filename } } files.append(record) end items = { :items => files } print JSON.generate(items) end end frf = FindRecentFiles.new frf.run