#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') require 'gli' require 'doing/help_monkey_patch' require 'doing/add_options' require 'doing' require 'tempfile' require 'pp' def class_exists?(class_name) klass = Module.const_get(class_name) klass.is_a?(Class) rescue NameError false end if class_exists? 'Encoding' Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') end include GLI::App include Doing::Errors version Doing::VERSION hide_commands_without_desc true autocomplete_commands true wrap_help_text :one_line unless $stdout.isatty include Doing::Types @colors = Doing::Color @wwid = Doing::WWID.new Doing.logger.log_level = :info env_log_level = nil if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG'] env_log_level = true # Quiet always wins if ENV['DOING_QUIET']&.truthy? Doing.logger.log_level = :error elsif ENV['DOING_PLUGIN_DEBUG']&.truthy? Doing.logger.log_level = :debug elsif ENV['DOING_DEBUG']&.truthy? Doing.logger.log_level = :debug elsif ENV['DOING_LOG_LEVEL'] Doing.logger.log_level = ENV['DOING_LOG_LEVEL'] end end Doing.logger.benchmark(:total, :start) Doing.logger.benchmark(:configure, :start) Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true }) if ENV['DOING_CONFIG'] Doing.logger.benchmark(:configure, :finish) Doing.set('backup_dir', ENV['DOING_BACKUP_DIR']) if ENV['DOING_BACKUP_DIR'] # Set up class vars for backwards compatibility @settings = Doing.settings accept BooleanSymbol do |value| value.normalize_bool(:pattern) end accept CaseSymbol do |value| value.normalize_case(Doing.config.fetch('search', 'case', :smart)) end accept AgeSymbol do |value| value.normalize_age(:newest) end accept OrderSymbol do |value| value.normalize_order(:asc) end accept MatchingSymbol do |value| value.normalize_matching(:pattern) end accept TagSortSymbol do |value| value.normalize_tag_sort(Doing.config.fetch('tag_sort', :name)) end accept TemplateName do |value| res = Doing.setting('templates').keys.select { |k| k =~ value.to_rx(distance: 2) } raise InvalidArgument, "Unknown template: #{value}" unless res.good? res.group_by(&:length).min.last[0] end accept DateBeginString do |value| if value =~ REGEX_TIME res = value else res = value.chronify(guess: :begin, future: false) end raise InvalidTimeExpression, 'Invalid start date' unless res res end accept DateEndString do |value| if value =~ REGEX_TIME res = value else res = value.chronify(guess: :end, future: false) end raise InvalidTimeExpression, 'Invalid end date' unless res res end accept DateRangeString do |value| start, finish = value.split_date_range raise InvalidTimeExpression, 'Invalid range' unless start finish ||= Time.now [start, finish] end accept DateRangeOptionalString do |value| start, finish = value.split_date_range raise InvalidTimeExpression, 'Invalid range' unless start [start, finish] end accept DateIntervalString do |value| res = value.chronify_qty raise InvalidTimeExpression, 'Invalid time quantity' unless res res end accept TagArray do |value| value.gsub(/[, ]+/, ' ').split(' ').map { |tag| tag.sub(/^@/, '') }.map(&:strip) end program_desc 'A CLI for a What Was I Doing system' program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.) default_command :recent # sort_help :manually ## Global options desc 'Output notes if included in the template' switch [:notes], default_value: true, negatable: true desc 'Send results report to STDOUT instead of STDERR' switch [:stdout], default_value: false, negatable: false desc 'Use a pager when output is longer than screen' switch %i[p pager], default_value: Doing.setting('paginate') desc 'Answer yes/no menus with default option' switch [:default], default_value: false, negatable: false desc 'Answer all yes/no menus with yes' switch [:yes], negatable: false desc 'Answer all yes/no menus with no' switch [:no], negatable: false desc 'Exclude auto tags and default tags' switch %i[x noauto], default_value: false, negatable: false desc 'Colored output' switch %i[color], default_value: true desc 'Silence info messages' switch %i[q quiet], default_value: false, negatable: false desc 'Verbose output' switch %i[debug], default_value: false, negatable: false desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead' flag [:config_file], default_value: Doing.config.config_file desc 'Specify a different doing_file' flag %i[f doing_file] def add_commands(commands) commands = [commands] unless commands.is_a?(Array) hidden = Doing.setting('disabled_commands') hidden = hidden.set_type('array') if hidden.good? && !hidden.is_a?(Array) commands.delete_if { |c| hidden.include?(c) } commands.each { |cmd| require_relative "commands/#{cmd}" } end ## Add/modify commands add_commands(%w[now done finish note select tag]) ## View commands add_commands(%w[grep last recent show on view]) ## Utility commands add_commands(%w[config open commands]) ## File handling/batch modification commands add_commands(%w[archive import rotate]) ## History commands add_commands(%w[undo redo]) ## Hidden commands add_commands(%w[commands_accepting install_fzf]) ## Optional commands add_commands(%w[again cancel flag meanwhile reset tags today yesterday since tag_dir colors completion plugins sections template views changes]) pre do |global, _command, _options, _args| # global[:pager] ||= Doing.setting('paginate') Doing::Pager.paginate = global[:pager] $stdout.puts "doing v#{Doing::VERSION}" if global[:version] unless $stdout.isatty Doing::Color.coloring = global[:pager] ? global[:color] : false else Doing::Color.coloring = global[:color] end # Return true to proceed; false to abort and not call the # chosen command # Use skips_pre before a command to skip this block # on that command only true end on_error do |exception| if exception.kind_of?(GLI::UnknownCommand) && ARGV.count > 1 exit run(['now'].concat(ARGV.unshift(@command))) elsif exception.kind_of?(SystemExit) false else # Doing.logger.error('Fatal:', exception) Doing.logger.output_results true end end post do |global, _command, _options, _args| # Use skips_post before a command to skip this # block on that command only Doing.logger.output_results Doing.logger.benchmark(:total, :finish) Doing.logger.log_benchmarks end around do |global, command, options, arguments, code| Doing.logger.benchmark("command_#{command.name}".to_sym, :start) # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{Doing.setting('paginate')}, Pager: #{Doing::Pager.paginate}") if env_log_level.nil? Doing.logger.adjust_verbosity(global) end if global[:stdout] Doing.logger.logdev = $stdout end if global[:yes] Doing::Prompt.force_answer = :yes Doing.config.force_answer = true elsif global[:no] Doing::Prompt.force_answer = :no Doing.config.force_answer = false else Doing::Prompt.default_answer = if $stdout.isatty global[:default] else true end Doing.config.force_answer = global[:default] ? true : false end if global[:config_file] && global[:config_file] != Doing.config.config_file Doing.logger.warn(format('%sWARNING:%s %sThe use of --config_file is deprecated, please set the environment variable DOING_CONFIG instead.', @colors.flamingo, @colors.default, @colors.boldred)) Doing.logger.warn(format('%sTo set it just for the current command, use: %sDOING_CONFIG=/path/to/doingrc doing [command]%s', @colors.red, @colors.boldwhite, @colors.default)) cf = File.expand_path(global[:config_file]) raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf) Doing.config.config_file = cf Doing.config_with(cf, { ignore_local: true }) end Doing.logger.benchmark(:init, :start) if global[:doing_file] @wwid.init_doing_file(global[:doing_file]) else @wwid.init_doing_file end Doing.logger.benchmark(:init, :finish) Doing.auto_tag = !global[:noauto] Doing.set('include_notes', false) unless global[:notes] global[:wwid] = @wwid code.call Doing.logger.benchmark("command_#{command.name.to_s}".to_sym, :finish) end commands_from File.expand_path(Doing.setting('plugins.command_path')) if Doing.setting('plugins.command_path') @command = ARGV[0] exit run(ARGV)