#!/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 version Doing::VERSION wwid = WWID.new wwid.user_home = if Dir.respond_to?('home') Dir.home else File.expand_path('~') end wwid.configure program_desc 'A CLI for a What Was I Doing system' 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 'Exclude auto tags and default tags' switch %i[x noauto], default_value: false desc 'Use a specific configuration file' flag [:config_file], default_value: wwid.config_file desc 'Specify a different doing_file' flag %i[f doing_file] desc 'Add an entry' arg_name 'ENTRY' command %i[now next] do |c| c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc "Edit entry with #{ENV['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 '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]) exit_now! '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] = wwid.config['current_section'] end if options[:e] || (args.empty? && $stdin.stat.size.zero?) exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? input = '' input += args.join(' ') unless args.empty? input = wwid.fork_editor(input).strip exit_now! '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 exit_now! 'You must provide content when creating a new entry' end 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 #{ENV['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.action do |_global_options, options, args| if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first end if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r]) exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? input = !args.empty? ? args.join(' ') : '' prev_input = wwid.last_note(section) || '' prev_input = prev_input.join("\n") if prev_input.instance_of?(Array) input = prev_input + input input = wwid.fork_editor(input).strip exit_now! 'No content, cancelled' unless input _title, note = wwid.format_input(input) exit_now! 'No note content' unless note wwid.note_last(section, note, replace: true) elsif !args.empty? title, note = wwid.format_input(args.join(' ')) note.insert(0, title) wwid.note_last(section, note, replace: options[:r]) elsif $stdin.stat.size.positive? title, note = wwid.format_input($stdin.read) note.insert(0, title) wwid.note_last(section, note, replace: options[:r]) elsif options[:r] wwid.note_last(section, [], replace: true) else exit_now! 'You must provide content when adding a note' end 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 #{ENV['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]) exit_now! '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 = wwid.config['current_section'] end input = '' if options[:e] exit_now! 'No EDITOR variable defined in environment' if ENV['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 and CSS 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: /^(?:html|haml|css)/i command :template do |c| c.action do |_global_options, options, args| exit_now! 'No type specified, use `doing template [HAML|CSS]`' if args.empty? case args[0] when /html|haml/i $stdout.puts wwid.haml_template when /css/i $stdout.puts wwid.css_template else exit_now! 'Invalid type specified, must be HAML or CSS' end end end desc 'Display an interactive menu to perform operations (requires fzf)' 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. 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' c.arg_name 'QUERY' c.flag %i[q query] 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|taskpaper|csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i c.action do |_global_options, options, args| 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 #{ENV['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]) exit_now! 'Unable to parse date string' if date.nil? else date = Time.now end if options[:editor] || (args.empty? && $stdin.stat.size.zero?) exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? input = args.empty? ? '' : args.join(' ') input = wwid.fork_editor(input).strip exit_now! '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 exit_now! '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 #{ENV['EDITOR']}" c.switch %i[e editor], 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 if options[:took] took = wwid.chronify_qty(options[:took]) exit_now! 'Unable to parse date string for --took' if took.nil? end if options[:back] date = wwid.chronify(options[:back]) exit_now! '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]) exit_now! '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 finish_date donedate = options[:date] ? "(#{finish_date.strftime('%F %R')})" : '' end if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = wwid.config['current_section'] end if options[:editor] exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? input = '' input += args.join(' ') unless args.empty? input = wwid.fork_editor(input).strip exit_now! 'No content' unless input && !input.empty? title, note = wwid.format_input(input) title += " @done#{donedate}" section = 'Archive' if options[:a] wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date }) 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 options = { tags: ['done'], archive: options[:a], back: finish_date, count: 1, date: options[:date], section: section, took: took == 0 ? nil : took } wwid.tag_last(options) end elsif !args.empty? title, note = wwid.format_input(args.join(' ')) title.chomp! title += " @done#{donedate}" section = 'Archive' if options[:a] wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date }) wwid.write(wwid.doing_file) elsif $stdin.stat.size.positive? title, note = wwid.format_input($stdin.read) title += " @done#{donedate}" section = options[:a] ? 'Archive' : section wwid.add_item(title.cap_first, section.cap_first, { note: note, back: date }) wwid.write(wwid.doing_file) else exit_now! '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: /(?:and|all|any|or|not|none)/i, 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.action do |_global_options, options, args| if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = wwid.config['current_section'] end if options[:tag].nil? tags = [] else tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') } options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end end exit_now! 'Only one argument allowed' if args.length > 1 exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/ count = args[0] ? args[0].to_i : 1 opts = { archive: options[:a], count: count, date: false, section: section, sequential: false, tag: tags, tag_bool: options[:bool], tags: ['done'], unfinished: options[:unfinished] } 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.*/")' 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: /(?:and|all|any|or|not|none)/i, default_value: 'AND' 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 start date. --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.action do |_global_options, options, args| if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = wwid.config['current_section'] end unless options[:auto] if options[:took] took = wwid.chronify_qty(options[:took]) exit_now! 'Unable to parse date string for --took' if took.nil? end exit_now! '--back and --took cannot be used together' if options[:back] && options[:took] exit_now! '--search and --tag cannot be used together' if options[:search] && options[:tag] if options[:at] finish_date = wwid.chronify(options[:at]) exit_now! '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]) exit_now! '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].split(/ *, */).map { |t| t.strip.sub(/^@/, '') } options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end end exit_now! 'Only one argument allowed' if args.length > 1 exit_now! 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/ count = args[0] ? args[0].to_i : 1 opts = { archive: options[:a], back: date, count: count, date: options[:date], search: options[:search], section: section, sequential: options[:auto], tag: tags, tag_bool: options[:bool], tags: ['done'], unfinished: options[:unfinished] } wwid.tag_last(opts) end end desc 'Repeat last entry as new entry' command [:again, :resume] do |c| c.desc '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/").' c.arg_name 'QUERY' c.flag [:search] c.desc 'Boolean used to combine multiple tags' c.arg_name 'BOOLEAN' c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND' c.desc 'Note' c.arg_name 'TEXT' c.flag %i[n note] c.action do |_global_options, options, _args| tags = options[:tag].nil? ? [] : options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip } options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end opts = { in: options[:in], note: options[:n], search: options[:search], section: options[:s], tag: tags, tag_bool: options[:bool] } wwid.restart_last(opts) end end desc 'Add tag(s) to last entry' arg_name 'TAG', :multiple command :tag do |c| 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 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 '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.*/")' 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: /(?:and|all|any|or|not|none)/i, default_value: 'AND' c.action do |_global_options, options, args| exit_now! 'You must specify at least one tag' if args.empty? && !options[:a] exit_now! '--search and --tag cannot 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].split(/ *, */).map { |t| t.strip.sub(/^@/, '') } options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end 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 count = options[:count].to_i 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 opts = { autotag: options[:a], count: count, date: options[:date], remove: options[:r], search: options[:search], section: section, tag: search_tags, tag_bool: options[:bool], tags: tags, unfinished: options[:unfinished] } wwid.tag_last(opts) end end desc 'Mark last entry as highlighted' command [:mark, :flag] do |c| c.desc 'Section' c.arg_name 'NAME' c.flag %i[s section] c.desc 'Remove mark' c.switch %i[r remove], negatable: false, default_value: false c.desc 'Mark last entry not marked @done' c.switch %i[u unfinished], negatable: false, default_value: false c.action do |_global_options, options, _args| mark = wwid.config['marker_tag'] || 'flagged' wwid.tag_last({ remove: options[:r], section: options[:s], tags: [mark], unfinished: options[:unfinished] }) 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.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: /(?:and|all|any|or|not|none)/i, 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/)' c.arg_name 'QUERY' c.flag [:search] c.desc 'Sort order (asc/desc)' c.arg_name 'ORDER' c.flag %i[s sort], must_match: /^[ad].*/i, 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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') 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: /^(?:a(?:sc)?|d(?:esc)?)$/i c.desc 'Only show items with recorded time intervals' c.switch [:only_timed], default_value: false, negatable: false c.desc 'Output to export format (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i c.action do |_global_options, options, args| 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]) exit_now! "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 = wwid.current_section end tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag] options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end unless tags.empty? tag_filter = { 'tags' => tags, 'bool' => options[: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]) finish = wwid.chronify(dates[2]) else start = wwid.chronify(date_string) finish = false end exit_now! 'Unrecognized date string' unless start dates = [start, finish] end options[:times] = true if options[:totals] tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil options[:sort_tags] = options[:tag_sort] =~ /^n/i tag_order = if options[:tag_order] options[:tag_order] =~ /^d/i ? 'desc' : 'asc' else 'asc' end opts = { after: options[:after], age: options[:age], before: options[:before], count: options[:c].to_i, date_filter: dates, highlight: true, only_timed: options[:only_timed], order: options[:s], output: options[:output], search: options[:search], section: section, sort_tags: options[:sort_tags], tag_filter: tag_filter, tag_order: tag_order, tags_color: tags_color, times: options[:t], totals: options[:totals] } puts wwid.list_section(opts) 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 [:grep, :search] do |c| 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 (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i 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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') 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.action do |_global_options, options, args| tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil section = wwid.guess_section(options[:s]) if options[:s] options[:times] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i opts = { after: options[:after], before: options[:before], highlight: true, only_timed: options[:only_timed], output: options[:output], search: args.join(' '), section: section, sort_tags: options[:sort_tags], tags_color: tags_color, times: options[:times], totals: options[:totals] } puts wwid.list_section(opts) end end desc 'List recent entries' default_value 10 arg_name 'COUNT' command :recent do |c| 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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.action do |global_options, options, args| section = wwid.guess_section(options[:s]) || options[:s].cap_first unless global_options[:version] if wwid.config['templates']['recent'].key?('count') config_count = wwid.config['templates']['recent']['count'].to_i else config_count = 10 end count = args.empty? ? config_count : args[0].to_i options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i tags_color = wwid.config.key?('tags_color') ? wwid.config['tags_color'] : nil opts = { sort_tags: options[:sort_tags], tags_color: tags_color, times: options[:t], totals: options[:totals] } puts wwid.recent(count, section.cap_first, opts) end end end desc 'List entries from today' command :today do |c| 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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'Output to export format (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i 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| 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] } puts 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.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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'Output to export format (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i c.action do |_global_options, options, args| exit_now! '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]) finish = wwid.chronify(dates[2]) else start = wwid.chronify(date_string) finish = false end exit_now! 'Unrecognized date string' unless start message = "Date interpreted as #{start}" message += " to #{finish}" if finish wwid.results.push(message) options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i puts 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.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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') c.arg_name 'KEY' c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default c.desc 'Output to export format (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i c.action do |_global_options, options, args| exit_now! 'Missing date argument' if args.empty? date_string = args.join(' ') date_string += ' at midnight' unless date_string =~ /(\d:|\d *[ap]m?|midnight|noon)/i date_string.sub!(/(day) (\d)/, '\1 at \2') if date_string =~ /day \d/ start = wwid.chronify(date_string) finish = Time.now exit_now! 'Unrecognized date string' unless start message = "Date interpreted as #{start} through the current time" wwid.results.push(message) options[:t] = true if options[:totals] options[:sort_tags] = options[:tag_sort] =~ /^n/i puts 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.desc 'Specify a section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc 'Output to export format (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i 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 = wwid.config['tag_sort'] if wwid.config.key?('tag_sort') 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: /^(?:a(?:sc)?|d(?:esc)?)$/i c.action do |_global_options, options, _args| tag_order = if options[:tag_order] options[:tag_order] =~ /^d/i ? 'desc' : 'asc' else 'asc' end 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], totals: options[:totals] } puts wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp end end desc 'Show the last entry, optionally edit' command :last do |c| c.desc 'Specify a section' c.arg_name 'NAME' c.flag %i[s section], default_value: 'All' c.desc "Edit entry with #{ENV['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: /(?:and|all|any|or|not|none)/i, default_value: 'AND' c.desc 'Search filter, surround with slashes for regex (/query/)' c.arg_name 'QUERY' c.flag [:search] c.action do |_global_options, options, _args| exit_now! '--tag and --search cannot be used together' if options[:tag] && options[:search] if options[:tag].nil? tags = [] else tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') } 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 puts 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 puts 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.action do |_global_options, _options, args| exit_now! "Section #{args[0]} already exists" if wwid.sections.include?(args[0]) wwid.add_section(args[0].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| clrs = wwid.colors bgs = [] fgs = [] clrs.each do |k, v| if k =~ /bg/ bgs.push("#{v} #{clrs['default']} <-- #{k}") else fgs.push("#{v}XXXX#{clrs['default']} <-- #{k}") end end puts fgs.join("\n") puts bgs.join("\n") 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.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 (csv|html|json|template|timeline)' c.arg_name 'FORMAT' c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i 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: /(?:and|all|any|or|not|none)/i, default_value: 'OR' c.desc 'Search filter, surround with slashes for regex (/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: /^(?:a(?:sc)?|d(?:esc)?)$/i 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.action do |_global_options, options, args| exit_now! '--tag and --search cannot 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 = wwid.config['current_section'] end view = wwid.get_view(title) if view only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed] true else false end template = view.key?('template') ? view['template'] : nil 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[:o] ||= 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'] : wwid.current_section end order = view.key?('order') ? view['order'] : 'asc' totals = if options[:totals] true else view.key?('totals') ? view['totals'] : false end options[:t] = true if totals options[:output]&.downcase! 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 tag_order = if options[:tag_order] options[:tag_order] =~ /^d/i ? 'desc' : 'asc' elsif view.key?('tag_order') view['tag_order'] =~ /^d/i ? 'desc' : 'asc' else 'asc' end opts = { after: options[:after], before: options[:before], count: count, format: format, highlight: options[:color], only_timed: only_timed, order: order, output: options[:output], search: options[:search], section: section, sort_tags: options[:sort_tags], tag_filter: tag_filter, tag_order: tag_order, tags_color: tags_color, template: template, times: options[:t], totals: totals } puts wwid.list_section(opts) elsif title.instance_of?(FalseClass) exit_now! 'Cancelled' else exit_now! "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' arg_name 'SECTION_NAME' default_value wwid.current_section command :archive do |c| 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: /(?:and|all|any|or|not|none)/i, 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 = wwid.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 exit_now! '--keep and --count can\'t be used together' if options[:keep] && options[:count] tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag] options[:bool] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end opts = { before: options[:before], bool: options[:bool], destination: options[:to], keep: options[:keep], search: options[:search], tags: tags } wwid.archive(section, opts) end end desc 'Move entries to archive file' command :rotate do |c| 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: /(?:and|all|any|or|not|none)/i, 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] = case options[:bool] when /(and|all)/i 'AND' when /(any|or)/i 'OR' when /(not|none)/i 'NOT' else 'AND' end 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 #{wwid.config_file} (#{wwid.config.key?('editor_app') ? wwid.config['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.desc "Open with $EDITOR (#{ENV['EDITOR']})" c.switch %i[e editor], negatable: false, default_value: false 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 options[:editor] exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? system %($EDITOR "#{File.expand_path(wwid.doing_file)}") elsif wwid.config.key?('editor_app') && !wwid.config['editor_app'].nil? system %(open -a "#{wwid.config['editor_app']}" "#{File.expand_path(wwid.doing_file)}") else system %(open "#{File.expand_path(wwid.doing_file)}") end else exit_now! 'No EDITOR variable defined in environment' if ENV['EDITOR'].nil? system %($EDITOR "#{File.expand_path(wwid.doing_file)}") end end end desc 'Edit the configuration file' command :config do |c| c.desc 'Editor to use' c.arg_name 'EDITOR' c.flag %i[e editor], default_value: nil 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 (#{wwid.config.key?('config_editor_app') ? wwid.config['config_editor_app'] : 'config_editor_app not set'})" c.switch [:x] end c.action do |_global_options, options, _args| if `uname` =~ /Darwin/ if options[:x] `open -a "#{wwid.config['config_editor_app']}" "#{wwid.config_file}"` elsif options[:a] || options[:b] if options[:a] `open -a "#{options[:a]}" "#{wwid.config_file}"` elsif options[:b] `open -b #{options[:b]} "#{wwid.config_file}"` end else exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil? editor = options[:e].nil? ? ENV['EDITOR'] : options[:e] system %(#{editor} "#{wwid.config_file}") end else exit_now! 'No EDITOR variable defined in environment' if options[:e].nil? && ENV['EDITOR'].nil? editor = options[:e].nil? ? ENV['EDITOR'] : options[:e] system %(#{editor} "#{wwid.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. Currently only handles JSON reports exported from Timing.app.' arg_name 'PATH' command :import do |c| c.desc 'Import type' c.arg_name 'TYPE' c.flag :type, default_value: 'timing' 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 'Allow entries that overlap existing times' c.switch [:overlap], negatable: true c.action do |_global_options, options, args| if options[:section] section = wwid.guess_section(options[:section]) || options[:section].cap_first else section = wwid.config['current_section'] end if options[:type] =~ /^tim/i args.each do |path| options = { autotag: options[:autotag], no_overlap: !options[:overlap], prefix: options[:prefix], section: section, tag: options[:tag] } wwid.import_timing(path, options) wwid.write(wwid.doing_file) end else exit_now! 'Invalid import type' end end end pre do |global, _command, _options, _args| if global[:config_file] && global[:config_file] != wwid.config_file wwid.config_file = global[:config_file] wwid.configure({ ignore_local: true }) # wwid.results.push("Override config file #{wwid.config_file}") end if global[:doing_file] wwid.init_doing_file(global[:doing_file]) else wwid.init_doing_file end wwid.auto_tag = !global[:noauto] wwid.config[:include_notes] = false unless global[:notes] $stdout.puts "doing v#{Doing::VERSION}" if global[:version] # 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 post do |global, _command, _options, _args| # Use skips_post before a command to skip this # block on that command only if global[:stdout] $stdout.print wwid.results.join("\n") else warn wwid.results.join("\n") end end on_error do |_exception| # puts exception.message true end exit run(ARGV)