# frozen_string_literal: true # Requirements # ======================================================================= # Stdlib # ----------------------------------------------------------------------- require 'shellwords' # Deps # ----------------------------------------------------------------------- require 'thor' require 'nrser/labs/i8' require 'rouge' # Project / Package # ----------------------------------------------------------------------- # Refinements # ======================================================================= require 'nrser/refinements/types' using NRSER::Types # Namespace # ======================================================================== module Locd module CLI module Command # Definitions # ======================================================================= # Abstract base for CLI interface commands using the `thor` gem. # # @abstract # # @see http://whatisthor.com/ # class Base < Thor # Instance Methods # ======================================================================== protected # @!group Helper Instance Methods # ======================================================================== # Swap `$stdout` for a {StringIO}, call `block`, swap original `$stdout` # back in and return the string contents. # # 'Cause we got that damn threaded logging going on, we want to write # output all as one string using `$stdout.write`, but some stuff like # {Thor::Shell::Basic#print_table} just write to {#stdout} (which resolves # to `$stdout`) and don't offer an option to returned a formatted string # instead. # # This seems like the simplest way to handle it, though it may still run # into trouble with the threads, we shall see... # # @param [Proc] &block # Block to run that writes to `$stdout` # # @return [String] # def capture_stdout &block io = StringIO.new stdout = $stdout $stdout = io block.call $stdout = stdout io.string end def agent_file agent agent.path.to_s.sub( /\A#{ Regexp.escape( ENV['HOME'] ) }/, '~' ) end def render_table table string = capture_stdout do print_table table.to_a end width = string.lines.map( &:length ).max return ( string.lines[0] + "-" * width + "\n" + string.lines[1..-1].join + "---\n" + table.footer + "\n\n" ) string.lines.each_with_index.map { |line, index| if index == 0 '# ' + line else ' ' + line end }.join end def render_text object # Does it look like an array/list? if NRSER.array_like? object # It does! Convert it to an actual array to make life easier array = object.to_a return "(EMPTY)" if array.empty? # Is it a list of agents? if array.all? { |entry| entry.is_a? Locd::Agent } # Ok, we want to display them. What mode are we in accoring to the # options? if options[:long] # We're in long-mode, render a table render_table agent_table( array ) else # We're in regular mode, render each agent on it's own line by # recurring array.map( &method( __method__ ) ).join( "\n" ) end # Is it a list of arrays? elsif array.all? { |entry| entry.is_a? Array } # It is, let's render that as a table render_table message else # It's not, let's just render each entry on it's own line by # recurring message.map( &method( __method__ ) ).join( "\n" ) end else # It's not array-ish. Special-case {Locd::Agent} instances and render # the rest as `#to_s` case object when Locd::Agent::Site # TODO Want to add options for this, but for now just render agent # URLs 'cause they have the label in them and are # `cmd+click`-able in iTerm2 to open, which is most useful object.url when Locd::Agent object.label else object.to_s end end end # #render_text def render_color? if options.key? :color options[:color] else $stdout.isatty end end def render_color src, lexer_name formatter = Rouge::Formatters::Terminal256.new lexer = Rouge::Lexers.const_get( lexer_name ).new formatter.format( lexer.lex( src ) ) end def respond message, title: nil formatted = if options[:json] json = begin JSON.pretty_generate message rescue JSON::GeneratorError => error logger.debug "Error generating 'pretty' JSON, falling back to dump", error: error JSON.dump message end if render_color? render_color json, :JSON else json end elsif options[:yaml] || NRSER.hash_like?( message ) # TODO This sucks, but it's the easiest way to get "nice" YAML :/ yaml = YAML.dump JSON.load( JSON.dump message ) if render_color? # formatter = Rouge::Formatters::Terminal256.new # lexer = Rouge::Lexers::YAML.new # formatter.format( lexer.lex( yaml ) ) render_color yaml, :YAML else yaml end else render_text message end if title formatted = ( "# #{ title }\n#{ '#' * 78}\n#\n" + formatted ) end # Be puts-like unless formatted[-1] == "\n" formatted = formatted + "\n" end $stdout.write formatted end # #respond # Hook called from {Thor::Command#run} when an error occurs. Errors are # not recoverable from my understanding, so this method should provide # necessary feedback to the user and exit with an error code for errors # it expects and re-raise those that it doesn't. # def on_run_error error, command, args case error when NRSER::CountError if error.count == 0 logger.error "No results" else logger.error "Too many results:\n\n#{ render_text error.value }\n" end # If the command supports `--all`, let them know that they can use it if command.options[:all] logger.info "You can use `--all` to to operate on ALL matches." logger.info "See `locd help #{ command.name }`\n" end exit 1 else raise error end end # Find exactly one {Locd::Agent} for a `pattern`, using the any `:pattern` # shared options provided, and raising if there are no matches or more # than one. # # @param pattern (see Locd::Agent.find_only!) # # @return [Locd::Agent] # Matched agent. # # @raise If more or less than one agent is matched. # def find_only! pattern Locd::Agent.find_only! pattern, **option_kwds( groups: :pattern ) end def find_multi! pattern # Behavior depend on the `:all` option... if options[:all] # `:all` is set, so we find all the agents for the pattern, raising # if we don't find any Locd::Agent.find_all!( pattern, **option_kwds( groups: :pattern ) ).values else # `:all` is not set, so we need to find exactly one or error [find_only!( pattern )] end end # @!endgroup Helper Instance Methods # *********************************** public # end protected ***************************************************** end # class Base # /Namespace # ======================================================================== end # module Command end # module CLI end # module Locd