# frozen_string_literal: true module Doing class WWID # Use fzf to filter an Items object with a search query. # Faster than {#filter_items} when all you need is a # text search of the title and note # # @param items [Items] an Items object # @param query [String] The search query # @param case_type [Symbol] The case type (:smart, :sensitive, :ignore) # # @return [Items] Filtered Items array # def fuzzy_filter_items(items, query, case_type: :smart) scannable = items.map.with_index { |item, idx| "#{item.title} #{item.note.join(' ')}".gsub(/[|*?!]/, '') + "|#{idx}" }.join("\n") res = `echo #{Shellwords.escape(scannable)}|#{Prompt.fzf} #{fuzzy_filter_args(query, case_type).join(' ')}` selected = Items.new res.split(/\n/).each do |item| idx = item.match(/\|(\d+)$/)[1].to_i selected.push(items[idx]) end selected end def fuzzy_filter_args(query, case_type) fzf_args = ['--multi', %(--filter="#{query.sub(/^'?/, "'")}"), '--no-sort', '-d "\|"', '--nth=1'] fzf_args << case case_type.normalize_case when :smart query =~ /[A-Z]/ ? '+i' : '-i' when :sensitive '+i' when :ignore '-i' end fzf_args end ## ## Filter items based on search criteria ## ## @param items [Array] The items to filter (if empty, filters all items) ## @param opt [Hash] The filter parameters ## ## @option opt [String] :section ('all') ## @option opt [Boolean] :unfinished (false) ## @option opt [Array or String] :tag ([]) Array or comma-separated string ## @option opt [Symbol] :tag_bool (:and) :and, :or, :not ## @option opt [String] :search ('') string, optional regex with `/string/` ## @option opt [Array] :date_filter (nil) [[Time]start, [Time]end] ## @option opt [Boolean] :only_timed (false) ## @option opt [String] :before (nil) Date/Time string, unparsed ## @option opt [String] :after (nil) Date/Time string, unparsed ## @option opt [Boolean] :today (false) limit to entries from today ## @option opt [Boolean] :yesterday (false) limit to entries from yesterday ## @option opt [Number] :count (0) max entries to return ## @option opt [String] :age (new) 'old' or 'new' ## @option opt [Array] :val (nil) Array of tag value queries ## def filter_items(items = Items.new, opt: {}) logger.benchmark(:filter_items, :start) time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/i if items.nil? || items.empty? section = opt[:section] ? guess_section(opt[:section]) : 'All' items = section =~ /^all$/i ? @content.clone : @content.in_section(section) end unless opt[:time_filter] opt[:time_filter] = [nil, nil] if opt[:from] && !opt[:date_filter] if opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx opt[:time_filter] = opt[:from] elsif opt[:from][0].is_a?(Time) opt[:date_filter] = opt[:from] end end end if opt[:before].is_a?(String) && opt[:before] =~ time_rx opt[:time_filter][1] = opt[:before] opt[:before] = nil end if opt[:after].is_a?(String) && opt[:after] =~ time_rx opt[:time_filter][0] = opt[:after] opt[:after] = nil end items.sort_by! { |item| [item.date, item.title.downcase] }.reverse filtered_items = items.select { |item| item.keep_item?(opt) } count = opt[:count].to_i&.positive? ? opt[:count].to_i : filtered_items.count output = Items.new if opt[:age] && opt[:age].normalize_age == :oldest output.concat(filtered_items.slice(0, count).reverse) else output.concat(filtered_items.reverse.slice(0, count)) end logger.benchmark(:filter_items, :finish) output end end end