#!/usr/bin/env ruby -W1 # frozen_string_literal: true $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') require 'gli' 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 REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i InvalidExportType = Class.new(RuntimeError) MissingConfigFile = Class.new(RuntimeError) colors = Doing::Color wwid = Doing::WWID.new Doing.logger.log_level = :info if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG'] # Quiet always wins if ENV['DOING_QUIET'] && ENV['DOING_QUIET'].truthy? Doing.logger.log_level = :error elsif (ENV['DOING_PLUGIN_DEBUG'] && ENV['DOING_PLUGIN_DEBUG'].truthy?) Doing.logger.log_level = :debug elsif (ENV['DOING_DEBUG'] && ENV['DOING_DEBUG'].truthy?) Doing.logger.log_level = :debug elsif ENV['DOING_LOG_LEVEL'] Doing.logger.log_level = ENV['DOING_LOG_LEVEL'] end end if ENV['DOING_CONFIG'] Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true }) end config = Doing.config settings = config.settings wwid.config = settings if config.settings.dig('plugins', 'command_path') commands_from File.expand_path(config.settings.dig('plugins', 'command_path')) 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 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: settings['paginate'] desc 'Answer yes/no menus with default option' switch [:default], default_value: 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: config.config_file desc 'Specify a different doing_file' flag %i[f doing_file] desc 'Add an entry' long_desc %(Record what you're starting now, or backdate the start time using natural language. A parenthetical at the end of the entry will be converted to a note. Run with no argument to create a new entry using #{Doing::Util.default_editor}.) arg_name 'ENTRY' command %i[now next] do |c| c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note." c.example 'doing now working on a new project', desc: 'Add a new entry at the current time' c.example 'doing now debugging @project2', desc: 'Add an entry with a tag' c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note' c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc "Edit entry with #{Doing::Util.default_editor}" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]' c.arg_name 'DATE_STRING' c.flag %i[b back] c.desc 'Timed entry, marks last entry in section as @done' c.switch %i[f finish_last], negatable: false, default_value: false c.desc 'Include a note' c.arg_name 'TEXT' c.flag %i[n note] # c.desc "Edit entry with specified app" # c.arg_name 'editor_app' # # c.flag [:a, :app] c.action do |_global_options, options, args| if options[:back] date = wwid.chronify(options[:back], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil? else date = Time.now end if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else options[:section] = settings['current_section'] end if options[:e] || (args.empty? && $stdin.stat.size.zero?) raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? input = '' input += args.join(' ') unless args.empty? input = wwid.fork_editor(input).strip raise Doing::Errors::EmptyInput, 'No content' if input.empty? title, note = wwid.format_input(input) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] }) wwid.write(wwid.doing_file) elsif args.length.positive? title, note = wwid.format_input(args.join(' ')) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] }) wwid.write(wwid.doing_file) elsif $stdin.stat.size.positive? input = $stdin.read title, note = wwid.format_input(input) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] }) wwid.write(wwid.doing_file) else raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry' end end end desc 'Reset the start time of an entry' command %i[reset begin] do |c| c.desc 'Set the start date of an item to now' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Resume entry (remove @done)' c.switch %i[r resume], default_value: true c.desc 'Reset last entry matching tag' c.arg_name 'TAG' c.flag [:tag] c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Select from a menu of matching entries' c.switch %i[i interactive] c.action do |global_options, options, args| if options[:section] options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first end options[:tag_bool] = options[:bool].normalize_bool items = wwid.filter_items([], opt: options) if options[:interactive] last_entry = wwid.choose_from_items(items, { menu: true, header: '', prompt: 'Select an entry to start/reset > ', multiple: false, sort: false, show_if_single: true }, include_section: options[:section].nil? ) else last_entry = items.last end unless last_entry Doing.logger.warn('Not found:', 'No entry matching parameters was found.') return end wwid.reset_item(last_entry, resume: options[:resume]) # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note) wwid.write(wwid.doing_file) end end desc 'Add a note to the last entry' long_desc %( If -r is provided with no other arguments, the last note is removed. If new content is specified through arguments or STDIN, any previous note will be replaced with the new one. Use -e to load the last entry in a text editor where you can append a note. ) arg_name 'NOTE_TEXT' command :note do |c| c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc "Edit entry with #{Doing::Util.default_editor}" c.switch %i[e editor], negatable: false, default_value: false c.desc "Replace/Remove last entry's note (default append)" c.switch %i[r remove], negatable: false, default_value: false c.desc 'Add/remove note from last entry matching tag' c.arg_name 'TAG' c.flag [:tag] c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Select item for new note from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, args| if options[:section] options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first end options[:tag_bool] = options[:bool].normalize_bool last_entry = wwid.last_entry(options) unless last_entry Doing.logger.warn('Not found:', 'No entry matching parameters was found.') return end last_note = last_entry.note || Doing::Note.new new_note = Doing::Note.new if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r]) raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? input = !args.empty? ? args.join(' ') : '' if options[:remove] prev_input = Doing::Note.new else prev_input = last_entry.note || Doing::Note.new end input = prev_input.add(input) input = wwid.fork_editor([last_entry.title, '### Edit below this line', input.to_s].join("\n")).strip _title, note = wwid.format_input(input) options[:remove] = true new_note.add(note) elsif !args.empty? new_note.add(args.join(' ')) elsif $stdin.stat.size.positive? new_note.add($stdin.read) else raise Doing::Errors::EmptyInput, 'You must provide content when adding a note' unless options[:remove] end if last_note.equal?(new_note) Doing.logger.debug('Skipped:', 'No note change') else last_note.add(new_note, replace: options[:remove]) Doing.logger.info('Entry updated:', last_entry.title) end # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note) wwid.write(wwid.doing_file) end end desc 'Finish any running @meanwhile tasks and optionally create a new one' arg_name 'ENTRY' command :meanwhile do |c| c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc "Edit entry with #{Doing::Util.default_editor}" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Archive previous @meanwhile entry' c.switch %i[a archive], default_value: false c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]' c.arg_name 'DATE_STRING' c.flag %i[b back] c.desc 'Note' c.arg_name 'TEXT' c.flag %i[n note] c.action do |_global_options, options, args| if options[:back] date = wwid.chronify(options[:back], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil? else date = Time.now end if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = settings['current_section'] end input = '' if options[:e] raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? input += args.join(' ') unless args.empty? input = wwid.fork_editor(input).strip elsif !args.empty? input = args.join(' ') elsif $stdin.stat.size.positive? input = $stdin.read end if input && !input.empty? input, note = wwid.format_input(input) else input = nil note = [] end if options[:n] note.push(options[:n]) elsif note.empty? note = nil end wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:a], note: note }) wwid.write(wwid.doing_file) end end desc 'Output HTML, CSS, and Markdown (ERB) templates for customization' long_desc %( Templates are printed to STDOUT for piping to a file. Save them and use them in the configuration file under html_template. Example `doing template haml > ~/styles/my_doing.haml` ) arg_name 'TYPE', must_match: Doing::Plugins.template_regex command :template do |c| c.desc 'List all available templates' c.switch %i[l list] c.desc 'List in single column for completion' c.switch %i[c] c.action do |_global_options, options, args| if options[:list] || options[:c] if options[:c] $stdout.print Doing::Plugins.plugin_templates.join("\n") else $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}" end return end if args.empty? type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ') else type = args[0] end raise Doing::Errors::InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type $stdout.puts Doing::Plugins.template_for_trigger(type) # case args[0] # when /html|haml/i # $stdout.puts wwid.haml_template # when /css/i # $stdout.puts wwid.css_template # when /markdown|md|erb/i # $stdout.puts wwid.markdown_template # else # exit_now! 'Invalid type specified, must be HAML or CSS' # end end end desc 'Display an interactive menu to perform operations' long_desc 'List all entries and select with typeahead fuzzy matching. Multiple selections are allowed, hit tab to add the highlighted entry to the selection, and use ctrl-a to select all visible items. Return processes the selected entries.' command :select do |c| c.desc 'Select from a specific section' c.arg_name 'SECTION' c.flag %i[s section] c.desc 'Tag selected entries' c.arg_name 'TAG' c.flag %i[t tag] c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)' c.switch %i[r remove], negatable: false # c.desc 'Add @done to selected item(s), using start time of next item as the finish time' # c.switch %i[a auto], negatable: false, default_value: false c.desc 'Archive selected items' c.switch %i[a archive], negatable: false, default_value: false c.desc 'Move selected items to section' c.arg_name 'SECTION' c.flag %i[m move] c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"' c.arg_name 'QUERY' c.flag %i[q query search] c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.' c.switch %i[menu], negatable: true, default_value: true c.desc 'Cancel selected items (add @done without timestamp)' c.switch %i[c cancel], negatable: false, default_value: false c.desc 'Delete selected items' c.switch %i[d delete], negatable: false, default_value: false c.desc 'Edit selected item(s)' c.switch %i[e editor], negatable: false, default_value: false c.desc 'Add @done with current time to selected item(s)' c.switch %i[f finish], negatable: false, default_value: false c.desc 'Add flag to selected item(s)' c.switch %i[flag], negatable: false, default_value: false c.desc 'Perform action without confirmation.' c.switch %i[force], negatable: false, default_value: false c.desc 'Save selected entries to file using --output format' c.arg_name 'FILE' c.flag %i[save_to] c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor." c.switch %i[again resume] c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) raise Doing::Errors::InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query] wwid.interactive(options) end end desc 'Add an item to the Later section' arg_name 'ENTRY' command :later do |c| c.desc "Edit entry with #{Doing::Util.default_editor}" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]' c.arg_name 'DATE_STRING' c.flag %i[b back] c.desc 'Note' c.arg_name 'TEXT' c.flag %i[n note] c.action do |_global_options, options, args| if options[:back] date = wwid.chronify(options[:back], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil? else date = Time.now end if options[:editor] || (args.empty? && $stdin.stat.size.zero?) raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? input = args.empty? ? '' : args.join(' ') input = wwid.fork_editor(input).strip raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty? title, note = wwid.format_input(input) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, 'Later', { note: note, back: date }) wwid.write(wwid.doing_file) elsif !args.empty? title, note = wwid.format_input(args.join(' ')) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, 'Later', { note: note, back: date }) wwid.write(wwid.doing_file) elsif $stdin.stat.size.positive? title, note = wwid.format_input($stdin.read) note.push(options[:n]) if options[:n] wwid.add_item(title.cap_first, 'Later', { note: note, back: date }) wwid.write(wwid.doing_file) else raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry' end end end desc 'Add a completed item with @done(date). No argument finishes last entry.' arg_name 'ENTRY' command %i[done did] do |c| c.desc 'Remove @done tag' c.switch %i[r remove], negatable: false, default_value: false c.desc 'Include date' c.switch [:date], negatable: true, default_value: true c.desc 'Immediately archive the entry' c.switch %i[a archive], negatable: false, default_value: false c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back. Used with --took, backdates start date) c.arg_name 'DATE_STRING' c.flag [:at] c.desc 'Backdate start date by interval [4pm|20m|2h|yesterday noon]' c.arg_name 'DATE_STRING' c.flag %i[b back] c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM). If used without the --back option, the start date will be moved back to allow the completion date to be the current time.) c.arg_name 'INTERVAL' c.flag %i[t took] c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Include a note' c.arg_name 'TEXT' c.flag %i[n note] c.desc 'Finish last entry not already marked @done' c.switch %i[u unfinished], negatable: false, default_value: false # c.desc "Edit entry with specified app" # c.arg_name 'editor_app' # # c.flag [:a, :app] c.action do |_global_options, options, args| took = 0 donedate = nil if options[:took] took = wwid.chronify_qty(options[:took]) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil? end if options[:back] date = wwid.chronify(options[:back], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil? else date = options[:took] ? Time.now - took : Time.now end if options[:at] finish_date = wwid.chronify(options[:at], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil? date = options[:took] ? finish_date - took : finish_date elsif options[:took] finish_date = date + took elsif options[:back] finish_date = date else finish_date = Time.now end if options[:date] donedate = finish_date.strftime('%F %R') end if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = settings['current_section'] end note = Doing::Note.new note.add(options[:note]) if options[:note] if options[:editor] raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? is_new = false if args.empty? last_entry = wwid.filter_items([], opt: {unfinished: options[:unfinished], section: section, count: 1, age: 'new'}).max_by { |item| item.date } unless last_entry Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done') raise Doing::Errors::NoResults, 'No results' end old_entry = last_entry.dup last_entry.note.add(note) input = [last_entry.title, last_entry.note.to_s].join("\n") else is_new = true input = [args.join(' '), note.to_s].join("\n") end input = wwid.fork_editor(input).strip raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty? title, note = wwid.format_input(input) new_entry = Doing::Item.new(date, title, section, note) if new_entry.should_finish? if new_entry.should_time? new_entry.tag('done', value: donedate) else new_entry.tag('done') end end if (is_new) wwid.content[section][:items].push(new_entry) else wwid.update_item(old_entry, new_entry) end if options[:a] wwid.move_item(new_entry, 'Archive', label: true) end wwid.write(wwid.doing_file) elsif args.empty? && $stdin.stat.size.zero? if options[:r] wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true }) else note = options[:note] ? Doing::Note.new(options[:note]) : nil opt = { archive: options[:a], back: finish_date, count: 1, date: options[:date], note: note, section: section, tags: ['done'], took: took == 0 ? nil : took, unfinished: options[:unfinished] } wwid.tag_last(opt) end elsif !args.empty? note = Doing::Note.new(options[:note]) title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n")) title.chomp! section = 'Archive' if options[:a] new_entry = Doing::Item.new(date, title, section, new_note) if new_entry.should_finish? if new_entry.should_time? new_entry.tag('done', value: donedate) else new_entry.tag('done') end end wwid.content[section][:items].push(new_entry) wwid.write(wwid.doing_file) Doing.logger.info('Entry Added:', new_entry.title) elsif $stdin.stat.size.positive? title, note = wwid.format_input($stdin.read) note.add(options[:note]) if options[:note] section = options[:a] ? 'Archive' : section new_entry = Doing::Item.new(date, title, section, note) if new_entry.should_finish? if new_entry.should_time? new_entry.tag('done', value: donedate) else new_entry.tag('done') end end wwid.content[section][:items].push(new_entry) wwid.write(wwid.doing_file) Doing.logger.info('Entry Added:', new_entry.title) else raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry' end end end desc 'End last X entries with no time tracked' long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.' arg_name 'COUNT' command :cancel do |c| c.desc 'Archive entries' c.switch %i[a archive], negatable: false, default_value: false c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)' c.arg_name 'TAG' c.flag [:tag] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Cancel last entry (or entries) not already marked @done' c.switch %i[u unfinished], negatable: false, default_value: false c.desc 'Select item(s) to cancel from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, args| if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = settings['current_section'] end if options[:tag].nil? tags = [] else tags = options[:tag].to_tags end raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1 raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/ if options[:interactive] count = 0 else count = args[0] ? args[0].to_i : 1 end opts = { archive: options[:a], count: count, date: false, section: section, sequential: false, tag: tags, tag_bool: options[:bool].normalize_bool, tags: ['done'], unfinished: options[:unfinished], interactive: options[:interactive] } wwid.tag_last(opts) end end desc 'Mark last X entries as @done' long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.' arg_name 'COUNT' command :finish do |c| c.desc 'Include date' c.switch [:date], negatable: true, default_value: true c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]' c.arg_name 'DATE_STRING' c.flag %i[b back] c.desc 'Set the completed date to the start date plus XX[hmd]' c.arg_name 'INTERVAL' c.flag %i[t took] c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.) c.arg_name 'DATE_STRING' c.flag [:at] c.desc 'Finish the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool' c.arg_name 'TAG' c.flag [:tag] c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Remove done tag' c.switch %i[r remove], negatable: false, default_value: false c.desc 'Finish last entry (or entries) not already marked @done' c.switch %i[u unfinished], negatable: false, default_value: false c.desc %(Auto-generate finish dates from next entry's start time. Automatically generate completion dates 1 minute before next item (in any section) began. --auto overrides the --date and --back parameters.) c.switch [:auto], negatable: false, default_value: false c.desc 'Archive entries' c.switch %i[a archive], negatable: false, default_value: false c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc 'Select item(s) to finish from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, args| unless options[:auto] if options[:took] took = wwid.chronify_qty(options[:took]) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil? end raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took] raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag] if options[:at] finish_date = wwid.chronify(options[:at], guess: :begin) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil? date = options[:took] ? finish_date - took : finish_date elsif options[:back] date = wwid.chronify(options[:back]) raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil? elsif options[:took] date = wwid.chronify_qty(options[:took]) else date = Time.now end end if options[:tag].nil? tags = [] else tags = options[:tag].to_tags end raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1 raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/ if options[:interactive] count = 0 else count = args[0] ? args[0].to_i : 1 end opts = { archive: options[:archive], back: date, count: count, date: options[:date], search: options[:search], section: options[:section], sequential: options[:auto], tag: tags, tag_bool: options[:bool].normalize_bool, tags: ['done'], unfinished: options[:unfinished], remove: options[:remove], interactive: options[:interactive] } wwid.tag_last(opts) end end desc 'Repeat last entry as new entry' command %i[again resume] do |c| c.desc 'Get last entry from a specific section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Add new entry to section (default: same section as repeated entry)' c.arg_name 'SECTION_NAME' c.flag [:in] c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Repeat last entry matching search. Surround with slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean used to combine multiple tags' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Note' c.arg_name 'TEXT' c.flag %i[n note] c.desc 'Select item to resume from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, _args| tags = options[:tag].nil? ? [] : options[:tag].to_tags opts = options opts[:tag] = tags opts[:tag_bool] = options[:bool].normalize_bool opts[:interactive] = options[:interactive] wwid.repeat_last(opts) end end desc 'Add tag(s) to last entry' long_desc 'Add (or remove) tags from the last entry, or from multiple entries (with `--count`), entries matching a search (with `--search`), or entries containing another tag (with `--tag`). When removing tags with `-r`, wildcards are allowed (`*` to match multiple characters, `?` to match a single character). With `--regex`, regular expressions will be interpreted instead of wildcards. For all tag removals the match is case insensitive by default, but if the tag search string contains any uppercase letters, the match will become case sensitive automatically. Tag name arguments do not need to be prefixed with @.' arg_name 'TAG', :multiple command :tag do |c| c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created' c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created' c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries' c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)' c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding' c.desc 'Section' c.arg_name 'SECTION_NAME' c.flag %i[s section], default_value: 'All' c.desc 'How many recent entries to tag (0 for all)' c.arg_name 'COUNT' c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex' c.arg_name 'ORIG_TAG' c.flag %i[rename] c.desc 'Don\'t ask permission to tag all entries when count is 0' c.switch %i[force], negatable: false, default_value: false c.desc 'Include current date/time with tag' c.switch %i[d date], negatable: false, default_value: false c.desc 'Remove given tag(s)' c.switch %i[r remove], negatable: false, default_value: false c.desc 'Interpret tag string as regular expression (with --remove)' c.switch %i[regex], negatable: false, default_value: false c.desc 'Tag last entry (or entries) not marked @done' c.switch %i[u unfinished], negatable: false, default_value: false c.desc 'Autotag entries based on autotag configuration in ~/.doingrc' c.switch %i[a autotag], negatable: false, default_value: false c.desc 'Tag the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Select item(s) to tag from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, args| raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a] raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag] section = 'All' if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first end if options[:tag].nil? search_tags = [] else search_tags = options[:tag].to_tags end if options[:autotag] tags = [] else tags = if args.join('') =~ /,/ args.join('').split(/,/) else args.join(' ').split(' ') # in case tags are quoted as one arg end tags.map! { |tag| tag.sub(/^@/, '').strip } end if options[:interactive] count = 0 options[:force] = true else count = options[:count].to_i end if count.zero? && !options[:force] if options[:search] section_q = ' matching your search terms' elsif options[:tag] section_q = ' matching your tag search' elsif section == 'All' section_q = '' else section_q = " in section #{section}" end question = if options[:a] "Are you sure you want to autotag all records#{section_q}" elsif options[:r] "Are you sure you want to remove #{tags.join(' and ')} from all records#{section_q}" else "Are you sure you want to add #{tags.join(' and ')} to all records#{section_q}" end res = wwid.yn(question, default_response: false) exit_now! 'Cancelled' unless res end options[:count] = count options[:section] = section options[:tag] = search_tags options[:tags] = tags options[:tag_bool] = options[:bool].normalize_bool wwid.tag_last(options) end end # desc 'Autotag last X entries' # arg_name 'COUNT' # command :autotag do |c| # c.action do |global_options, options, args| # options = { # autotag: true, # count: args[0].to_i # } # cmd = commands[:tag] # cmd.action.(global_options, options, []) # end # end desc 'Mark last entry as flagged' command [:mark, :flag] do |c| c.example 'doing flag', desc: 'Add @flagged to the last entry created' c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1' c.example 'doing flag --interactive --search "/(develop|cod)ing/"', desc: 'Find entries matching regular expression and create a menu allowing multiple selections, selected items will be @flagged' c.desc 'Section' c.arg_name 'SECTION_NAME' c.flag %i[s section], default_value: 'All' c.desc 'How many recent entries to tag (0 for all)' c.arg_name 'COUNT' c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer c.desc 'Don\'t ask permission to flag all entries when count is 0' c.switch %i[force], negatable: false, default_value: false c.desc 'Include current date/time with tag' c.switch %i[d date], negatable: false, default_value: false c.desc 'Remove flag' c.switch %i[r remove], negatable: false, default_value: false c.desc 'Flag last entry (or entries) not marked @done' c.switch %i[u unfinished], negatable: false, default_value: false c.desc 'Flag the last entry containing TAG. Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool' c.arg_name 'TAG' c.flag [:tag] c.desc 'Flag the last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Select item(s) to flag from a menu of matching entries' c.switch %i[i interactive] c.action do |_global_options, options, _args| mark = settings['marker_tag'] || 'flagged' raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag] section = 'All' if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first end if options[:tag].nil? search_tags = [] else search_tags = options[:tag].to_tags end if options[:interactive] count = 0 options[:force] = true else count = options[:count].to_i end if count.zero? && !options[:force] if options[:search] section_q = ' matching your search terms' elsif options[:tag] section_q = ' matching your tag search' elsif section == 'All' section_q = '' else section_q = " in section #{section}" end question = if options[:remove] "Are you sure you want to unflag all entries#{section_q}" else "Are you sure you want to flag all records#{section_q}" end res = wwid.yn(question, default_response: false) exit_now! 'Cancelled' unless res end options[:count] = count options[:section] = section options[:tag] = search_tags options[:tags] = [mark] options[:tag_bool] = options[:bool].normalize_bool wwid.tag_last(options) end end desc 'List all entries' long_desc %( The argument can be a section name, @tag(s) or both. "pick" or "choose" as an argument will offer a section menu. ) arg_name '[SECTION|@TAGS]' command :show do |c| c.example 'doing show Currently', desc: 'Show entries in the Currently section' c.example 'doing show @project1', desc: 'Show entries tagged @project1' c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing' c.example 'doing show Ideas --from "mon to fri" --tag doing', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.' c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions' c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag boolean (AND,OR,NOT)' c.arg_name 'BOOLEAN' c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR' c.desc 'Max count to show' c.arg_name 'MAX' c.flag %i[c count], default_value: 0 c.desc 'Age (oldest|newest)' c.arg_name 'AGE' c.flag %i[a age], default_value: 'newest' c.desc 'View entries older than date' c.arg_name 'DATE_STRING' c.flag [:before] c.desc 'View entries newer than date' c.arg_name 'DATE_STRING' c.flag [:after] c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Sort order (asc/desc)' c.arg_name 'ORDER' c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc' c.desc %( Date range to show, or a single day to filter date on. Date range argument should be quoted. Date specifications can be natural language. To specify a range, use "to" or "through": `doing show --from "monday to friday"` ) c.arg_name 'DATE_OR_RANGE' c.flag %i[f from] c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show intervals with totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default c.desc 'Tag sort direction (asc|desc)' c.arg_name 'DIRECTION' c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc' c.desc 'Only show items with recorded time intervals' c.switch [:only_timed], default_value: false, negatable: false c.desc 'Select from a menu of matching entries to perform additional operations' c.switch %i[i interactive] c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) tag_filter = false tags = [] if args.length.positive? case args[0] when /^all$/i section = 'All' args.shift when /^(choose|pick)$/i section = wwid.choose_section args.shift when /^@/ section = 'All' else section = wwid.guess_section(args[0]) raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section args.shift end if args.length.positive? args.each do |arg| arg.split(/,/).each do |tag| tags.push(tag.strip.sub(/^@/, '')) end end end else section = settings['current_section'] end tags.concat(options[:tag].to_tags) if options[:tag] unless tags.empty? tag_filter = { 'tags' => tags, 'bool' => options[:bool].normalize_bool } end if options[:from] date_string = options[:from] if date_string =~ / (to|through|thru|(un)?til|-+) / dates = date_string.split(/ (to|through|thru|(un)?til|-+) /) start = wwid.chronify(dates[0], guess: :begin) finish = wwid.chronify(dates[2], guess: :end) else start = wwid.chronify(date_string, guess: :begin) finish = false end raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start dates = [start, finish] end options[:times] = true if options[:totals] tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil opt = options.dup opt[:sort_tags] = options[:tag_sort] =~ /^n/i opt[:count] = options[:count].to_i opt[:date_filter] = dates opt[:highlight] = true opt[:order] = options[:sort].normalize_order opt[:section] = section opt[:tag] = nil opt[:tag_filter] = tag_filter opt[:tag_order] = options[:tag_order].normalize_order opt[:tags_color] = tags_color Doing::Pager.page wwid.list_section(opt) end end desc 'Search for entries' long_desc <<~'EODESC' Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched. To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'` EODESC arg_name 'SEARCH_PATTERN' command %i[grep search] do |c| c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching' c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)' c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression' c.example 'doing search --before 12/21 "doing wiki"', desc: 'Find entries containing "doing wiki" with entry dates before 12/21 of the current year' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Constrain search to entries older than date' c.arg_name 'DATE_STRING' c.flag [:before] c.desc 'Constrain search to entries newer than date' c.arg_name 'DATE_STRING' c.flag [:after] c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show intervals with totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'Only show items with recorded time intervals' c.switch [:only_timed], default_value: false, negatable: false c.desc 'Display an interactive menu of results to perform further operations' c.switch %i[i interactive], default_value: false, negatable: false c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil section = wwid.guess_section(options[:section]) if options[:section] options[:times] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i options[:highlight] = true options[:search] = args.join(' ') options[:section] = section options[:tags_color] = tags_color Doing::Pager.page wwid.list_section(options) end end desc 'List recent entries' default_value 10 arg_name 'COUNT' command :recent do |c| c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections' c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections' c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section' c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show intervals with totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'Select from a menu of matching entries to perform additional operations' c.switch %i[i interactive] c.action do |global_options, options, args| section = wwid.guess_section(options[:s]) || options[:s].cap_first unless global_options[:version] if settings['templates']['recent'].key?('count') config_count = settings['templates']['recent']['count'].to_i else config_count = 10 end if options[:interactive] count = 0 else count = args.empty? ? config_count : args[0].to_i end options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil opts = { sort_tags: options[:sort_tags], tags_color: tags_color, times: options[:t], totals: options[:totals], interactive: options[:interactive] } Doing::Pager::page wwid.recent(count, section.cap_first, opts) end end end desc 'List entries from today' command :today do |c| c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day' c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section' c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today' c.example 'doing today --output json', desc: 'Output entries from today in JSON format' c.desc 'Specify a section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show time totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)' c.arg_name 'TIME_STRING' c.flag [:before] c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)' c.arg_name 'TIME_STRING' c.flag [:after] c.action do |_global_options, options, _args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i opt = { after: options[:after], before: options[:before], section: options[:section], sort_tags: options[:sort_tags], totals: options[:totals], order: settings.dig('templates', 'today', 'order') } Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp end end desc 'List entries for a date' long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday," and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates, it will create a range.) arg_name 'DATE_STRING' command :on do |c| c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday' c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020' c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show time totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty? date_string = args.join(' ') if date_string =~ / (to|through|thru) / dates = date_string.split(/ (to|through|thru) /) start = wwid.chronify(dates[0], guess: :begin) finish = wwid.chronify(dates[2], guess: :end) else start = wwid.chronify(date_string, guess: :begin) finish = false end raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start message = "Date interpreted as #{start}" message += " to #{finish}" if finish Doing.logger.debug(message) options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output], { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp end end desc 'List entries since a date' long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday," and "2d" would be interpreted as "two days ago.") arg_name 'DATE_STRING' command :since do |c| c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year' c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show time totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty? date_string = args.join(' ') date_string.sub!(/(day) (\d)/, '\1 at \2') date_string.sub!(/(\d+)d( ago)?/, '\1 days ago') start = wwid.chronify(date_string, guess: :begin) finish = Time.now raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start Doing.logger.debug("Date interpreted as #{start} through the current time") options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output], { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp end end desc 'List entries from yesterday' command :yesterday do |c| c.example 'doing yesterday', desc: 'List all entries from the previous day' c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm' c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers' c.desc 'Specify a section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show time totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Sort tags by (name|time)' default = 'time' default = settings['tag_sort'] || 'name' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)' c.arg_name 'TIME_STRING' c.flag [:before] c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)' c.arg_name 'TIME_STRING' c.flag [:after] c.desc 'Tag sort direction (asc|desc)' c.arg_name 'DIRECTION' c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc' c.action do |_global_options, options, _args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) options[:sort_tags] = options[:tag_sort] =~ /^n/i opt = { after: options[:after], before: options[:before], sort_tags: options[:sort_tags], tag_order: options[:tag_order].normalize_order, totals: options[:totals], order: settings.dig('templates', 'today', 'order') } Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp end end desc 'Show the last entry, optionally edit' command :last do |c| c.example 'doing last', desc: 'Show the most recent entry in all sections' c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section' c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work' c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)' c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)' c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications' c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor' c.desc 'Specify a section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc "Edit entry with #{Doing::Util.default_editor}" c.switch %i[e editor], negatable: false, default_value: false c.desc 'Tag filter, combine multiple tags with a comma.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag boolean' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.action do |global_options, options, _args| raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search] if options[:tag].nil? tags = [] else tags = options[:tag].to_tags options[:bool] = case options[:bool] when /(any|or)/i :or when /(not|none)/i :not else :and end end if options[:editor] wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] }) else Doing::Pager::page wwid.last(times: true, section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip end end end desc 'List sections' command :sections do |c| c.desc 'List in single column' c.switch %i[c column], default_value: false c.action do |_global_options, options, _args| joiner = options[:c] ? "\n" : "\t" print wwid.sections.join(joiner) end end desc 'Select a section to display from a menu' command :choose do |c| c.action do |_global_options, _options, _args| section = wwid.choose_section Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section end end desc 'Add a new section to the "doing" file' arg_name 'SECTION_NAME' command :add_section do |c| c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file' c.action do |_global_options, _options, args| raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0]) wwid.add_section(args.join(' ').cap_first) wwid.write(wwid.doing_file) end end desc 'List available color variables for configuration templates and views' command :colors do |c| c.action do |_global_options, _options, _args| bgs = [] fgs = [] colors::attributes.each do |color| if color.to_s =~ /bg/ bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}") else fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}") end end out = [] out << fgs.join("\n") out << bgs.join("\n") Doing::Pager.page out.join("\n") end end desc 'List installed plugins' long_desc %(Lists available plugins, including user-installed plugins. Export plugins are available with the `--output` flag on commands that support it. Import plugins are available using `doing import --type PLUGIN`. ) command :plugins do |c| c.example 'doing plugins', desc: 'List all plugins' c.example 'doing plugins -t import', desc: 'List all import plugins' c.desc 'List plugins of type (import, export)' c.arg_name 'TYPE' c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all' c.desc 'List in single column for completion' c.switch %i[c column], default_value: false c.action do |_global_options, options, _args| Doing::Plugins.list_plugins(options) end end desc 'Display a user-created view' long_desc 'Command line options override view configuration' arg_name 'VIEW_NAME' command :view do |c| c.example 'doing view color', desc: 'Display entries according to config for view "color"' c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings' c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc 'Count to display' c.arg_name 'COUNT' c.flag %i[c count], must_match: /^\d+$/, type: Integer c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})" c.arg_name 'FORMAT' c.flag %i[o output] c.desc 'Show time intervals on @done tasks' c.switch %i[t times], default_value: true, negatable: true c.desc 'Show intervals with totals at the end of output' c.switch [:totals], default_value: false, negatable: false c.desc 'Include colors in output' c.switch [:color], default_value: true, negatable: true c.desc 'Tag filter, combine multiple tags with a comma.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag boolean (AND,OR,NOT)' c.arg_name 'BOOLEAN' c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR' c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Sort tags by (name|time)' c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i c.desc 'Tag sort direction (asc|desc)' c.arg_name 'DIRECTION' c.flag [:tag_order], must_match: REGEX_SORT_ORDER c.desc 'View entries older than date' c.arg_name 'DATE_STRING' c.flag [:before] c.desc 'View entries newer than date' c.arg_name 'DATE_STRING' c.flag [:after] c.desc 'Only show items with recorded time intervals (override view settings)' c.switch [:only_timed], default_value: false, negatable: false c.desc 'Select from a menu of matching entries to perform additional operations' c.switch %i[i interactive] c.action do |_global_options, options, args| raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search] title = if args.empty? wwid.choose_view else wwid.guess_view(args[0]) end if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = settings['current_section'] end view = wwid.get_view(title) if view page_title = view.key?('title') ? view['title'] : title.cap_first only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed] true else false end template = view.key?('template') ? view['template'] : nil date_format = view.key?('date_format') ? view['date_format'] : nil tags_color = view.key?('tags_color') ? view['tags_color'] : nil tag_filter = false if options[:tag] tag_filter = { 'tags' => [], 'bool' => 'OR' } tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip) tag_filter['bool'] = options[:bool].normalize_bool elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?) tag_filter = { 'tags' => [], 'bool' => 'OR' } tag_filter['tags'] = if view['tags'].instance_of?(Array) view['tags'].map(&:strip) else view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip) end tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or end # If the -o/--output flag was specified, override any default in the view template options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template' count = if options[:c] options[:c] else view.key?('count') ? view['count'] : 10 end section = if options[:s] section else view.key?('section') ? view['section'] : settings['current_section'] end order = view.key?('order') ? view['order'].normalize_order : 'asc' totals = if options[:totals] true else view.key?('totals') ? view['totals'] : false end tag_order = if options[:tag_order] options[:tag_order].normalize_order else view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc' end options[:t] = true if totals output_format = options[:output]&.downcase || 'template' options[:sort_tags] = if options[:tag_sort] options[:tag_sort] =~ /^n/i ? true : false elsif view.key?('tag_sort') view['tag_sort'] =~ /^n/i ? true : false else false end if view.key?('after') && !options[:after] options[:after] = view['after'] end if view.key?('before') && !options[:before] options[:before] = view['before'] end if view.key?('from') date_string = view['from'] if date_string =~ / (to|through|thru|(un)?til|-+) / dates = date_string.split(/ (to|through|thru|(un)?til|-+) /) start = wwid.chronify(dates[0], guess: :begin) finish = wwid.chronify(dates[2], guess: :end) else start = wwid.chronify(date_string, guess: :begin) finish = false end raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start dates = [start, finish] end opts = options opts[:output] = output_format opts[:count] = count opts[:format] = date_format opts[:highlight] = options[:color] opts[:only_timed] = only_timed opts[:order] = order opts[:section] = section opts[:tag_filter] = tag_filter opts[:tag_order] = tag_order opts[:tags_color] = tags_color opts[:template] = template opts[:totals] = totals opts[:page_title] = page_title opts[:date_filter] = dates opts[:output] = options[:interactive] ? nil : options[:output] Doing::Pager.page wwid.list_section(opts) elsif title.instance_of?(FalseClass) exit_now! 'Cancelled' else raise Doing::Errors::InvalidView, "View #{title} not found in config" end end end desc 'List available custom views' command :views do |c| c.desc 'List in single column' c.switch %i[c column], default_value: false c.action do |_global_options, options, _args| joiner = options[:c] ? "\n" : "\t" print wwid.views.join(joiner) end end desc 'Move entries between sections' long_desc %(Argument can be a section name to move all entries from a section, or start with an "@" to move entries matching a tag. Default with no argument moves items from the "#{settings['current_section']}" section to Archive.) arg_name 'SECTION_OR_TAG' default_value settings['current_section'] command %i[archive move] do |c| c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section' c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive' c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section' c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)' c.desc 'How many items to keep (ignored if archiving by tag or search)' c.arg_name 'X' c.flag %i[k keep], must_match: /^\d+$/, type: Integer c.desc 'Move entries to' c.arg_name 'SECTION_NAME' c.flag %i[t to], default_value: 'Archive' c.desc 'Label moved items with @from(SECTION_NAME)' c.switch [:label], default_value: true, negatable: true c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag boolean (AND|OR|NOT)' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Search filter' c.arg_name 'QUERY' c.flag [:search] c.desc 'Archive entries older than date (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)' c.arg_name 'DATE_STRING' c.flag [:before] c.action do |_global_options, options, args| if args.empty? section = settings['current_section'] tags = [] elsif args[0] =~ /^all/i section = 'all' elsif args[0] =~ /^@\S+/ section = 'all' tags = args.map { |t| t.sub(/^@/, '').strip } else section = args[0].cap_first tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : [] end raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count] tags.concat(options[:tag].to_tags) if options[:tag] opts = options opts[:bool] = options[:bool].normalize_bool opts[:destination] = options[:to] opts[:tags] = tags wwid.archive(section, opts) end end desc 'Move entries to archive file' command :rotate do |c| c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file' c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries' c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file' c.desc 'How many items to keep in each section (most recent)' c.arg_name 'X' c.flag %i[k keep], must_match: /^\d+$/, type: Integer c.desc 'Section to rotate' c.arg_name 'SECTION_NAME' c.flag %i[s section], default_value: 'All' c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.' c.arg_name 'TAG' c.flag [:tag] c.desc 'Tag boolean (AND|OR|NOT)' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND' c.desc 'Search filter' c.arg_name 'QUERY' c.flag [:search] c.desc 'Rotate entries older than date (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)' c.arg_name 'DATE_STRING' c.flag [:before] c.action do |_global_options, options, args| if options[:section] && options[:section] !~ /^all$/i options[:section] = wwid.guess_section(options[:section]) end options[:bool] = options[:bool].normalize_bool wwid.rotate(options) end end desc 'Open the "doing" file in an editor' long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})." command :open do |c| if `uname` =~ /Darwin/ c.desc 'Open with app name' c.arg_name 'APP_NAME' c.flag %i[a app] c.desc 'Open with app bundle id' c.arg_name 'BUNDLE_ID' c.flag %i[b bundle_id] end c.action do |_global_options, options, _args| params = options.dup params.delete_if do |k, v| k.instance_of?(String) || v.nil? || v == false end if `uname` =~ /Darwin/ if options[:app] system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}") elsif options[:bundle_id] system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}") elsif Doing::Util.find_default_editor('doing_file') editor = Doing::Util.find_default_editor('doing_file') if Doing::Util.exec_available(editor) system %(#{editor} "#{File.expand_path(wwid.doing_file)}") else system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}") end else system %(open "#{File.expand_path(wwid.doing_file)}") end else raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil? system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}") end end end desc 'Edit the configuration file or output a value from it' long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor. If local configurations are found in the path between the current directory and `~/.doingrc`, a menu will allow you to select which to open in the editor. It will use the editor defined in `config_editor_app`, or one specified with `--editor`. Use `doing config -d` to output the configuration to the terminal, and provide a dot-separated key path to get a specific value. Shows the current value including keys/overrides set by local configs.) arg_name 'KEY_PATH' command :config do |c| c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}" c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML' c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON' c.desc 'Editor to use' c.arg_name 'EDITOR' c.flag %i[e editor], default_value: nil c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.' c.switch %i[d dump] c.desc 'Format for --dump (json|yaml|raw)' c.arg_name 'FORMAT' c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/ c.desc 'Update config file with missing configuration options' c.switch %i[u update], default_value: false, negatable: false if `uname` =~ /Darwin/ c.desc 'Application to use' c.arg_name 'APP_NAME' c.flag [:a] c.desc 'Application bundle id to use' c.arg_name 'BUNDLE_ID' c.flag [:b] c.desc "Use the config_editor_app defined in ~/.doingrc (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})" c.switch [:x] end c.action do |_global_options, options, args| if options[:update] config.configure({rewrite: true, ignore_local: true}) # Doing.logger.warn("Config file rewritten: #{config.config_file}") return end if options[:dump] keypath = args.join('.') cfg = config.value_for_key(keypath) if cfg $stdout.puts case options[:output] when /^j/ JSON.pretty_generate(cfg) when /^r/ cfg else # cfg = { last_key => cfg } unless last_key.nil? YAML.dump(cfg) end else Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found") end Doing.logger.output_results return end if config.additional_configs.count.positive? choices = [config.config_file] choices.concat(config.additional_configs) res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ') raise Doing::Errors::UserCancelled, 'Cancelled' unless res config_file = res.strip || config.config_file else config_file = config.config_file end if `uname` =~ /Darwin/ if options[:x] editor = Doing::Util.find_default_editor('config') if editor if Doing::Util.exec_available(editor) system %(#{editor} "#{config_file}") else `open -a "#{editor}" "#{config_file}"` end else raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.' end elsif options[:a] || options[:b] if options[:a] `open -a "#{options[:a]}" "#{config_file}"` elsif options[:b] `open -b #{options[:b]} "#{config_file}"` end else editor = options[:e] || Doing::Util.find_default_editor('config') raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor if Doing::Util.exec_available(editor) system %(#{editor} "#{config_file}") else `open -a "#{editor}" "#{config_file}"` end end else editor = options[:e] || Doing::Util.default_editor raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor) system %(#{editor} "#{config_file}") end end end desc 'Undo the last change to the doing_file' command :undo do |c| c.desc 'Specify alternate doing file' c.arg_name 'PATH' c.flag %i[f file], default_value: wwid.doing_file c.action do |_global_options, options, _args| file = options[:f] || wwid.doing_file wwid.restore_backup(file) end end desc 'Import entries from an external source' long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}" arg_name 'PATH' command :import do |c| c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})" c.arg_name 'TYPE' c.flag :type, default_value: 'doing' c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")' c.arg_name 'QUERY' c.flag [:search] c.desc 'Only import items with recorded time intervals' c.switch [:only_timed], default_value: false, negatable: false c.desc 'Target section' c.arg_name 'NAME' c.flag %i[s section] c.desc 'Tag all imported entries' c.arg_name 'TAGS' c.flag :tag c.desc 'Autotag entries' c.switch :autotag, negatable: true, default_value: true c.desc 'Prefix entries with' c.arg_name 'PREFIX' c.flag :prefix c.desc 'Import entries older than date' c.arg_name 'DATE_STRING' c.flag [:before] c.desc 'Import entries newer than date' c.arg_name 'DATE_STRING' c.flag [:after] c.desc %( Date range to import. Date range argument should be quoted. Date specifications can be natural language. To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`. Has no effect unless the import plugin has implemented date range filtering. ) c.arg_name 'DATE_OR_RANGE' c.flag %i[f from] c.desc 'Allow entries that overlap existing times' c.switch [:overlap], negatable: true c.action do |_global_options, options, args| if options[:section] options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first end if options[:from] date_string = options[:from] if date_string =~ / (to|through|thru|(un)?til|-+) / dates = date_string.split(/ (to|through|thru|(un)?til|-+) /) start = wwid.chronify(dates[0], guess: :begin) finish = wwid.chronify(dates[2], guess: :end) else start = wwid.chronify(date_string, guess: :begin) finish = false end raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start dates = [start, finish] end if options[:type] =~ Doing::Plugins.plugin_regex(type: :import) options[:no_overlap] = !options[:overlap] options[:date_filter] = dates wwid.import(args, options) wwid.write(wwid.doing_file) else raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}" end end end pre do |global, _command, _options, _args| # global[:pager] ||= settings['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?(SystemExit) # false # else # p exception.inspect # Doing.logger.output_results # true # end false 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 end around do |global, command, options, arguments, code| # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}") Doing.logger.adjust_verbosity(global) if global[:stdout] Doing.logger.logdev = $stdout end wwid.default_option = global[:default] if global[:config_file] && global[:config_file] != 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) config.config_file = cf settings = config.configure({ ignore_local: true }) end if global[:doing_file] wwid.init_doing_file(global[:doing_file]) else wwid.init_doing_file end wwid.auto_tag = !global[:noauto] settings[:include_notes] = false unless global[:notes] global[:wwid] = wwid code.call end exit run(ARGV)