lib/markdown_exec.rb in markdown_exec-2.0.4 vs lib/markdown_exec.rb in markdown_exec-2.0.5

- old
+ new

@@ -7,10 +7,11 @@ require 'clipboard' require 'fileutils' require 'open3' require 'optparse' require 'shellwords' +require 'time' require 'tmpdir' require 'tty-prompt' require 'yaml' require_relative 'ansi_formatter' @@ -101,34 +102,111 @@ # execute markdown documents # module MarkdownExec include Exceptions + class FileInMenu + # Prepends the age of the file in days to the file name for display in a menu. + # @param filename [String] the name of the file + # @return [String] modified file name with age prepended + def self.for_menu(filename) + file_age = (Time.now - File.mtime(filename)) / (60 * 60 * 24 * 30) + + " #{Histogram.display(file_age, 0, 11, 12, inverse: false)}: #{filename}" + end + + # Removes the age from the string to retrieve the original file name. + # @param filename_with_age [String] the modified file name with age + # @return [String] the original file name + def self.from_menu(filename_with_age) + filename_with_age.split(': ', 2).last + end + end + + # A class that generates a histogram bar in terminal using xterm-256 color codes. + class Histogram + # Generates and prints a histogram bar for a given value within a specified range and width, with an option for inverse display. + # @param integer_value [Integer] the value to represent in the histogram + # @param min [Integer] the minimum value of the range + # @param max [Integer] the maximum value of the range + # @param width [Integer] the total width of the histogram in characters + # @param inverse [Boolean] whether the histogram is displayed in inverse order (right to left) + def self.display(integer_value, min, max, width, inverse: false) + return if max <= min # Ensure the range is valid + + # Normalize the value within the range 0 to 1 + normalized_value = [0, [(integer_value - min).to_f / (max - min), 1].min].max + + # Calculate how many characters should be filled + filled_length = (normalized_value * width).round + + # # Generate the histogram bar using xterm-256 colors (color code 42 is green) + # filled_bar = "\e[48;5;42m" + ' ' * filled_length + "\e[0m" + filled_bar = ('¤' * filled_length).fg_rgbh_AF_AF_00 + empty_bar = ' ' * (width - filled_length) + + # Determine the order of filled and empty parts based on the inverse flag + inverse ? (empty_bar + filled_bar) : (filled_bar + empty_bar) + end + end + + class MenuBuilder + def initialize + @chrome_color = :cyan + @o_color = :red + end + + def build_menu(file_names, directory_names, found_in_block_names, files_in_directories, vbn) + choices = [] + + # Adding section title and data for file names + choices << { disabled: '', name: "in #{file_names[:section_title]}".send(@chrome_color) } + choices += file_names[:data].map { |str| FileInMenu.for_menu(str) } + + # Conditionally add directory names if data is present + unless directory_names[:data].count.zero? + choices << { disabled: '', name: "in #{directory_names[:section_title]}".send(@chrome_color) } + choices += files_in_directories + end + + # Adding found in block names + choices << { disabled: '', name: "in #{found_in_block_names[:section_title]}".send(@chrome_color) } + + choices += vbn + + choices + end + end + class SearchResultsReport < DirectorySearcher def directory_names(search_options, highlight_value) matched_directories = find_directory_names { section_title: 'directory names', data: matched_directories, formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(matched_directories, highlight: [highlight_value]) }] } end - def file_contents(search_options, highlight_value) - matched_contents = find_file_contents.map.with_index do |(file, contents), index| - [file, contents.map { |detail| format('=%4.d: %s', detail.index, detail.line) }, index] + def found_in_block_names(search_options, highlight_value, formspec: '=%<index>4.d: %<line>s') + matched_contents = (find_file_contents do |line| + read_block_name(line, search_options[:fenced_start_and_end_regex], search_options[:block_name_match], search_options[:block_name_nick_match]) + end).map.with_index do |(file, contents), index| + # [file, contents.map { |detail| format(formspec, detail.index, detail.line) }, index] + [file, contents.map { |detail| format(formspec, { index: detail.index, line: detail.line }) }, index] end { - section_title: 'file contents', + section_title: 'block names', data: matched_contents.map(&:first), formatted_text: matched_contents.map do |(file, details, index)| - { header: format('- %3.d: %s', index + 1, file), - content: AnsiFormatter.new(search_options).format_and_highlight_array( - details, - highlight: [highlight_value] - ) } - end + { header: format('- %3.d: %s', index + 1, file), + content: AnsiFormatter.new(search_options).format_and_highlight_array( + details, + highlight: [highlight_value] + ) } + end, + matched_contents: matched_contents } end def file_names(search_options, highlight_value) matched_files = find_file_names @@ -138,10 +216,25 @@ formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array( matched_files, highlight: [highlight_value] ).join("\n") }] } end + + def read_block_name(line, fenced_start_and_end_regex, block_name_match, block_name_nick_match) + return unless line.match(fenced_start_and_end_regex) + + bm = extract_named_captures_from_option(line, block_name_match) + return if bm.nil? + + name = bm[:title] + + if block_name_nick_match.present? && line =~ Regexp.new(block_name_nick_match) + $~[0] + else + bm && bm[1] ? bm[:title] : name + end + end end ## # # :reek:DuplicateMethodCall { allow_calls: ['block', 'item', 'lm', 'opts', 'option', '@options', 'required_blocks'] } @@ -351,40 +444,53 @@ @fout.fout 'Searching in: ' \ "#{HashDelegator.new(@options).string_send_color(find_path, :menu_chrome_color)}" searcher = SearchResultsReport.new(value, [find_path]) file_names = searcher.file_names(options, value) - file_contents = searcher.file_contents(options, value) + found_in_block_names = searcher.found_in_block_names(options, value, formspec: '%<line>s') directory_names = searcher.directory_names(options, value) ### search in file contents (block names, chrome, or text) - [file_contents, + [found_in_block_names, directory_names, file_names].each do |data| - @fout.fout "In #{data[:section_title]}" if data[:section_title] + next if data[:data].count.zero? next unless data[:formatted_text] + @fout.fout "In #{data[:section_title]}" if data[:section_title] data[:formatted_text].each do |fi| @fout.fout fi[:header] if fi[:header] @fout.fout fi[:content] if fi[:content] end end return { exit: true } unless execute_chosen_found ## pick a document to open # - files = directory_names[:data].map do |dn| + files_in_directories = directory_names[:data].map do |dn| find_files('*', [dn], exclude_dirs: true) - end.flatten(1) - choices = \ - [{ disabled: '', name: "in #{file_names[:section_title]}".cyan }] \ - + file_names[:data] \ - + [{ disabled: '', name: "in #{directory_names[:section_title]}".cyan }] \ - + files \ - + [{ disabled: '', name: "in #{file_contents[:section_title]}".cyan }] \ - + file_contents[:data] - @options[:filename] = select_document_if_multiple(choices) + end.flatten(1).map { |str| FileInMenu.for_menu(str) } + + return { exit: true } unless file_names[:data]&.count.positive? || files_in_directories&.count.positive? || found_in_block_names[:data]&.count.positive? + + vbn = found_in_block_names[:matched_contents].map do |matched_contents| + filename, details, = matched_contents + nexo = AnsiFormatter.new(@options).format_and_highlight_array( + details, + highlight: [value] + ) + [FileInMenu.for_menu(filename)] + nexo.map { |str| { disabled: '', name: (' ' * 20) + str } } + end.flatten + + choices = MenuBuilder.new.build_menu(file_names, directory_names, found_in_block_names, files_in_directories, vbn) + + @options[:filename] = FileInMenu.from_menu( + select_document_if_multiple( + choices, + prompt: options[:prompt_select_md].to_s + ' ¤ Age in months'.fg_rgbh_AF_AF_00 + ) + ) { exit: false } end ## Sets up the options and returns the parsed arguments # @@ -616,16 +722,16 @@ @options[:block_name] = mf[:block] @options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern], @options[:saved_filename_replacement]) end - def select_document_if_multiple(files = list_markdown_files_in_path) + def select_document_if_multiple(files = list_markdown_files_in_path, prompt: options[:prompt_select_md].to_s) return files[0] if (count = files.count) == 1 return unless count >= 2 opts = options.dup - select_option_or_exit(HashDelegator.new(@options).string_send_color(opts[:prompt_select_md].to_s, :prompt_color_after_script_execution), + select_option_or_exit(HashDelegator.new(@options).string_send_color(prompt, :prompt_color_after_script_execution), files, opts.merge(per_page: opts[:select_page_height])) end # Presents a TTY prompt to select an option or exit, returns selected option or nil