class Hiiro
def self.init(*oargs, plugins: [], logging: false, tasks: false, task_scope: nil, **values, &block)
def self.init(*oargs, plugins: [], logging: false, tasks: false, task_scope: nil, **values, &block) load_env if values[:args] args = values[:args] else args ||= oargs args = ARGV if args.empty? end bin_name = values[:bin_name] || $0 new(bin_name, *args, logging: logging, tasks: tasks, task_scope: task_scope, **values).tap do |hiiro| hiiro.load_plugins(plugins) hiiro.add_subcommand(:pry) { |*args| binding.pry } hiiro.add_subcmd(:edit, **values) { |*args| hiiro.edit_files(hiiro.bin) } if hiiro.tasks_enabled? hiiro.add_subcmd(:task) do |*args| tm = TaskManager.new(hiiro, scope: :task) Tasks.build_hiiro(hiiro, tm).run end hiiro.add_subcmd(:subtask) do |*args| tm = TaskManager.new(hiiro, scope: :subtask) Tasks.build_hiiro(hiiro, tm).run end end if block if block.arity == 1 block.call(hiiro) else hiiro.instance_eval(&block) end end end end
def self.load_env
def self.load_env Config.plugin_files.each do |plugin_file| require plugin_file end self end
def self.options(&block)
def self.options(&block) Options.setup(&block) end
def self.run(*args, plugins: [], logging: false, tasks: false, task_scope: nil, **values, &block)
def self.run(*args, plugins: [], logging: false, tasks: false, task_scope: nil, **values, &block) hiiro = init(*args, plugins:, logging:, tasks:, task_scope:, **values, &block) hiiro.run end
def add_default(**values, &handler)
def add_default(**values, &handler) runners.add_default(handler, **global_values, **values) end
def add_subcommand(*names, opts: nil, **values, &handler)
def add_subcommand(*names, opts: nil, **values, &handler) names.each do |name| runners.add_subcommand(name, handler, opts: opts, **global_values, **values) end end
def attach_method(name, &block)
def attach_method(name, &block) define_singleton_method(name.to_sym, &block) end
def auto_var_name(path, existing_vars)
def auto_var_name(path, existing_vars) parts = path.delete_prefix('/').split('/') (1..parts.length).each do |n| candidate = parts.last(n).join('_').upcase.gsub(/[^A-Z0-9]/, '_').squeeze('_').delete_prefix('_').delete_suffix('_') return candidate unless existing_vars.key?(candidate) end "DIR_#{existing_vars.length + 1}" end
def build_location_vars(list)
def build_location_vars(list) paths = list.map { |r| r.location }.compact.map { |loc| loc.sub(/:\d+$/, '') } dirs = paths.map { |p| File.dirname(p) }.uniq ancestors = consolidate_dirs(dirs, min_depth: 4) vars = {} ancestors.sort.each do |ancestor| name = auto_var_name(ancestor, vars) vars[name] = ancestor end vars end
def consolidate_dirs(dirs, min_depth:)
Recursively group directories under their common ancestor when that
def consolidate_dirs(dirs, min_depth:) return dirs if dirs.length <= 1 lca = dirs.reduce { |a, b| path_lca(a, b) } lca_depth = lca.delete_prefix('/').split('/').reject(&:empty?).length if lca_depth >= min_depth [lca] else lca_parts = lca.split('/') subgroups = dirs.group_by { |d| d.split('/').first(lca_parts.length + 1).join('/') } subgroups.flat_map { |_, group| consolidate_dirs(group, min_depth: min_depth) }.uniq end end
def default_subcommand
def default_subcommand Runners::Subcommand.new( bin_name, :DEFAULT, lambda { |*args| help; false }, ) end
def edit_files(*files, max_splits: 3)
def edit_files(*files, max_splits: 3) if editor.match?(/vim/i) if files.count > max_splits.to_i system(editor, '-O' + max_splits.to_i.to_s, *files) else system(editor, '-O', *files) end else system(editor, *files) end end
def editor
def editor editors = Bins.glob(%w[nvim vim vi]).find(&File.method(:executable?)) ENV['EDITOR'] || editors.first end
def environment
def environment @environment ||= Environment.current end
def full_name
def full_name runner&.full_name || [bin_name, subcmd].join(?-) end
def fuzzyfind(lines)
def fuzzyfind(lines) Fuzzyfind.select(lines) end
def fuzzyfind_from_map(mapping)
def fuzzyfind_from_map(mapping) Fuzzyfind.map_select(mapping) end
def get_value(name)
def get_value(name) runner&.values&.[](name) end
def git
def git @git ||= Git.new(self, Dir.pwd) end
def handle_result(result)
def handle_result(result) exit 0 if result.nil? || result exit 1 end
def help(options=nil)
def help(options=nil) ambiguous = runners.ambiguous_matches puts "Current command: #{bin_name}!" if ambiguous.any? puts "Ambiguous subcommand #{subcmd.inspect}!" puts puts "Did you mean one of these?" list_runners(ambiguous) puts else puts "Subcommand required for #{bin_name}" puts puts "Possible subcommands:" list_runners(runners.all_runners) puts end if options puts "Options:" puts options.help_text puts end exit 1 end
def initialize(bin, *all_args, logging: false, tasks: false, task_scope: nil, **values)
def initialize(bin, *all_args, logging: false, tasks: false, task_scope: nil, **values) @bin = bin @bin_name = File.basename(bin) @all_args = all_args @subcmd, *@args = all_args # normally i would never do this @loaded_plugins = [] @logging = logging @tasks_enabled = tasks @task_scope = task_scope @global_values = values @full_command = [ bin_name, *all_args, ].map(&:shellescape).join(' ') end
def list_runners(list)
def list_runners(list) sorted = list.sort_by(&:subcommand_name) vars = build_location_vars(sorted) vars.each { |var_name, path| puts "export #{var_name}=\"#{path}\"" } puts if vars.any? max_name = sorted.map { |r| r.subcommand_name.length }.max || 0 max_type = sorted.map { |r| r.type.to_s.length }.max || 0 max_params = sorted.map { |r| r.params_string.to_s.length }.max || 0 sorted.each do |r| name = r.subcommand_name.ljust(max_name) type = "(#{r.type})".ljust(max_type + 2) params = r.params_string params_col = params ? params.ljust(max_params) : ''.ljust(max_params) location = shorten_location(r.location, vars) puts " #{name} #{params_col} #{type} #{location}" end end
def load_plugin(plugin_const)
def load_plugin(plugin_const) if plugin_const.is_a?(String) || plugin_const.is_a?(Symbol) begin plugin_const = Kernel.const_get(plugin_const.to_sym) rescue => e puts "unable to load plugin: #{plugin_const}" puts "Error message: #{e.message}" return end end return if @loaded_plugins.include?(plugin_const) plugin_const.load(self) @loaded_plugins.push(plugin_const) end
def load_plugins(*plugins)
def load_plugins(*plugins) plugins.flatten.each { |plugin| load_plugin(plugin) } end
def log(message)
def log(message) return unless logging puts "[Hiiro: #{bin_name} #{(runner&.subcommand_name || subcmd).inspect}]: #{message}" end
def make_child(custom_subcmd=nil, custom_args=nil, **kwargs, &block)
def make_child(custom_subcmd=nil, custom_args=nil, **kwargs, &block) child_subcmd = custom_subcmd || subcmd child_args = custom_args || args child_bin_name = [bin, child_subcmd.to_s].join(?-) Hiiro.init(bin_name: child_bin_name, args: child_args, **kwargs, &block) end
def parsed_args
def parsed_args i = Args.new(*args) end
def path_lca(a, b)
def path_lca(a, b) a_parts = a.split('/') b_parts = b.split('/') a_parts.zip(b_parts).take_while { |x, y| x == y }.map(&:first).join('/') end
def pins = @pins ||= Pin.new(self)
def pins = @pins ||= Pin.new(self)
def queue
def queue @queue ||= Queue.current end
def run
def run result = runner.run(*args) handle_result(result) exit 1 rescue => e puts "ERROR: #{e.message}" puts e.backtrace exit 1 end
def run_subcommand(name, *args)
def run_subcommand(name, *args) runners.run_subcommand(name, *args) end
def runnable?
def runnable? runner end
def runner
def runner runners.runner || runners.default_subcommand end
def runners
def runners @runners ||= Runners.new(self) end
def shorten_location(loc, vars)
def shorten_location(loc, vars) return loc if loc.nil? vars.sort_by { |_, path| -path.length }.each do |var_name, path| return loc.sub(path, "$#{var_name}") if loc.start_with?(path + '/') || loc == path end loc end
def start_tmux_session(name, **opts)
def start_tmux_session(name, **opts) tmux_client.open_session(name, **opts) end
def subcommand_names
def subcommand_names runners.subcommand_names end
def task_manager
def task_manager return nil unless tasks_enabled? @task_manager ||= TaskManager.new(self, scope: task_scope || :task) end
def tasks
def tasks task_manager&.tasks end
def tasks_enabled?
def tasks_enabled? @tasks_enabled end
def this
def this self end
def tmux_client
def tmux_client @tmux_client ||= Tmux.client!(self) end
def todo_manager
def todo_manager @todo_manager ||= TodoManager.new end
def vim?
def vim? editor.to_s.match?(/vim/i) end