doing 2.1.25 → 2.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +4 -4
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +283 -108
  6. data/Gemfile.lock +1 -1
  7. data/README.md +1 -1
  8. data/bin/commands/add_section.rb +13 -0
  9. data/bin/commands/again.rb +99 -0
  10. data/bin/commands/archive.rb +96 -0
  11. data/bin/commands/cancel.rb +102 -0
  12. data/bin/commands/changes.rb +42 -0
  13. data/bin/commands/choose.rb +9 -0
  14. data/bin/commands/colors.rb +19 -0
  15. data/bin/commands/commands.rb +87 -0
  16. data/bin/commands/commands_accepting.rb +25 -0
  17. data/bin/commands/completion.rb +24 -0
  18. data/bin/commands/config.rb +245 -0
  19. data/bin/commands/done.rb +249 -0
  20. data/bin/commands/finish.rb +149 -0
  21. data/bin/commands/flag.rb +126 -0
  22. data/bin/commands/grep.rb +124 -0
  23. data/bin/commands/import.rb +101 -0
  24. data/bin/commands/install_fzf.rb +17 -0
  25. data/bin/commands/last.rb +114 -0
  26. data/bin/commands/meanwhile.rb +86 -0
  27. data/bin/commands/note.rb +130 -0
  28. data/bin/commands/now.rb +151 -0
  29. data/bin/commands/on.rb +66 -0
  30. data/bin/commands/open.rb +53 -0
  31. data/bin/commands/plugins.rb +23 -0
  32. data/bin/commands/recent.rb +78 -0
  33. data/bin/commands/redo.rb +22 -0
  34. data/bin/commands/reset.rb +106 -0
  35. data/bin/commands/rotate.rb +73 -0
  36. data/bin/commands/sections.rb +11 -0
  37. data/bin/commands/select.rb +123 -0
  38. data/bin/commands/show.rb +231 -0
  39. data/bin/commands/since.rb +64 -0
  40. data/bin/commands/tag.rb +179 -0
  41. data/bin/commands/tag_dir.rb +29 -0
  42. data/bin/commands/tags.rb +93 -0
  43. data/bin/commands/template.rb +61 -0
  44. data/bin/commands/today.rb +65 -0
  45. data/bin/commands/undo.rb +49 -0
  46. data/bin/commands/view.rb +238 -0
  47. data/bin/commands/views.rb +11 -0
  48. data/bin/commands/yesterday.rb +73 -0
  49. data/bin/doing +39 -3641
  50. data/docs/doc/Array.html +1 -1
  51. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  52. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  53. data/docs/doc/BooleanTermParser/Query.html +1 -1
  54. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  55. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  56. data/docs/doc/BooleanTermParser.html +1 -1
  57. data/docs/doc/Doing/Color.html +1 -1
  58. data/docs/doc/Doing/Completion.html +1 -1
  59. data/docs/doc/Doing/Configuration.html +2 -1
  60. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  61. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  62. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  63. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  64. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  65. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  66. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  67. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  68. data/docs/doc/Doing/Errors.html +1 -1
  69. data/docs/doc/Doing/Hooks.html +1 -1
  70. data/docs/doc/Doing/Item.html +1 -1
  71. data/docs/doc/Doing/Items.html +1 -1
  72. data/docs/doc/Doing/LogAdapter.html +1 -1
  73. data/docs/doc/Doing/Note.html +1 -1
  74. data/docs/doc/Doing/Pager.html +1 -1
  75. data/docs/doc/Doing/Plugins.html +1 -1
  76. data/docs/doc/Doing/Prompt.html +46 -1
  77. data/docs/doc/Doing/Section.html +1 -1
  78. data/docs/doc/Doing/TemplateString.html +1 -1
  79. data/docs/doc/Doing/Types.html +1 -1
  80. data/docs/doc/Doing/Util/Backup.html +1 -1
  81. data/docs/doc/Doing/Util.html +1 -1
  82. data/docs/doc/Doing/WWID.html +1 -1
  83. data/docs/doc/Doing.html +2 -2
  84. data/docs/doc/FalseClass.html +201 -0
  85. data/docs/doc/GLI/Commands/Help.html +1 -1
  86. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  87. data/docs/doc/GLI/Commands.html +1 -1
  88. data/docs/doc/GLI.html +1 -1
  89. data/docs/doc/Hash.html +1 -1
  90. data/docs/doc/Numeric.html +1 -1
  91. data/docs/doc/Object.html +203 -0
  92. data/docs/doc/PhraseParser/Operator.html +1 -1
  93. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  94. data/docs/doc/PhraseParser/Query.html +1 -1
  95. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  96. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  97. data/docs/doc/PhraseParser/TermClause.html +1 -1
  98. data/docs/doc/PhraseParser.html +1 -1
  99. data/docs/doc/Status.html +1 -1
  100. data/docs/doc/String.html +1 -1
  101. data/docs/doc/Symbol.html +1 -1
  102. data/docs/doc/Time.html +1 -1
  103. data/docs/doc/TrueClass.html +201 -0
  104. data/docs/doc/_index.html +1 -1
  105. data/docs/doc/file.README.html +2 -2
  106. data/docs/doc/index.html +2 -2
  107. data/docs/doc/method_list.html +374 -366
  108. data/docs/doc/top-level-namespace.html +1 -1
  109. data/doing.rdoc +15 -5
  110. data/lib/completion/_doing.zsh +3 -3
  111. data/lib/completion/doing.fish +1 -1
  112. data/lib/doing/changelog/changes.rb +1 -1
  113. data/lib/doing/configuration.rb +1 -0
  114. data/lib/doing/pager.rb +1 -0
  115. data/lib/doing/prompt.rb +8 -0
  116. data/lib/doing/version.rb +1 -1
  117. data/lib/examples/commands/wiki.rb +6 -7
  118. data/lib/helpers/threaded_tests.rb +25 -19
  119. metadata +45 -1
@@ -0,0 +1,130 @@
1
+ # @@note
2
+ desc 'Add a note to the last entry'
3
+ long_desc %(
4
+ If -r is provided with no other arguments, the last note is removed.
5
+ If new content is specified through arguments or STDIN, any previous
6
+ note will be replaced with the new one.
7
+
8
+ Use -e to load the last entry in a text editor where you can append a note.
9
+ )
10
+ arg_name 'NOTE_TEXT', optional: true
11
+ command :note do |c|
12
+ c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
13
+ c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
14
+ c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
15
+ c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
16
+
17
+ c.desc 'Section'
18
+ c.arg_name 'NAME'
19
+ c.flag %i[s section], default_value: 'All'
20
+
21
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
22
+ c.switch %i[e editor], negatable: false, default_value: false
23
+
24
+ c.desc "Replace/Remove last entry's note (default append)"
25
+ c.switch %i[r remove], negatable: false, default_value: false
26
+
27
+ c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?)'
28
+ c.arg_name 'TAG'
29
+ c.flag [:tag], type: TagArray
30
+
31
+ 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")'
32
+ c.arg_name 'QUERY'
33
+ c.flag [:search]
34
+
35
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
36
+ c.arg_name 'QUERY'
37
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
38
+
39
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
40
+ # c.switch [:fuzzy], default_value: false, negatable: false
41
+
42
+ c.desc 'Force exact search string matching (case sensitive)'
43
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
44
+
45
+ c.desc 'Add note to item that *doesn\'t* match search/tag filters'
46
+ c.switch [:not], default_value: false, negatable: false
47
+
48
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
49
+ c.arg_name 'TYPE'
50
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
51
+
52
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
53
+ c.arg_name 'BOOLEAN'
54
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
55
+
56
+ c.desc 'Select item for new note from a menu of matching entries'
57
+ c.switch %i[i interactive], negatable: false, default_value: false
58
+
59
+ c.desc 'Prompt for note via multi-line input'
60
+ c.switch %i[ask], negatable: false, default_value: false
61
+
62
+ c.action do |_global_options, options, args|
63
+ options[:fuzzy] = false
64
+ if options[:section]
65
+ options[:section] = @wwid.guess_section(options[:section]) || options[:section].cap_first
66
+ end
67
+
68
+ options[:tag_bool] = options[:bool].normalize_bool
69
+
70
+ options[:case] = options[:case].normalize_case
71
+
72
+ if options[:search]
73
+ search = options[:search]
74
+ search.sub!(/^'?/, "'") if options[:exact]
75
+ options[:search] = search
76
+ end
77
+
78
+ last_entry = @wwid.last_entry(options)
79
+ old_entry = last_entry.clone
80
+
81
+ unless last_entry
82
+ Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
83
+ return
84
+ end
85
+
86
+ last_note = last_entry.note || Doing::Note.new
87
+ new_note = Doing::Note.new
88
+
89
+ if $stdin.stat.size.positive?
90
+ new_note.add($stdin.read.strip)
91
+ end
92
+
93
+ unless args.empty?
94
+ new_note.add(args.join(' '))
95
+ end
96
+
97
+ if options[:editor]
98
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
99
+
100
+ if options[:remove]
101
+ input = Doing::Note.new
102
+ else
103
+ input = last_entry.note || Doing::Note.new
104
+ end
105
+
106
+ input.add(new_note)
107
+
108
+ new_note = Doing::Note.new(@wwid.fork_editor(input.strip_lines.join("\n"), message: nil).strip)
109
+ options[:remove] = true
110
+ end
111
+
112
+ if (new_note.empty? && !options[:remove]) || options[:ask]
113
+ $stderr.puts last_note if last_note.good?
114
+ $stderr.puts new_note if new_note.good?
115
+ new_note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
116
+ end
117
+
118
+ raise EmptyInput, 'You must provide content when adding a note' unless options[:remove] || new_note.good?
119
+
120
+ if last_note.equal?(new_note)
121
+ Doing.logger.debug('Skipped:', 'No note change')
122
+ else
123
+ last_note.add(new_note, replace: options[:remove])
124
+ Doing.logger.info('Entry updated:', last_entry.title)
125
+ Doing::Hooks.trigger :post_entry_updated, @wwid, last_entry, old_entry
126
+ end
127
+ # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
128
+ @wwid.write(@wwid.doing_file)
129
+ end
130
+ end
@@ -0,0 +1,151 @@
1
+ # @@now @@next
2
+ desc 'Add an entry'
3
+ long_desc %(Record what you're starting now, or backdate the start time using natural language.
4
+
5
+ A parenthetical at the end of the entry will be converted to a note.
6
+
7
+ Run without arguments to create a new entry interactively.
8
+
9
+ Run with --editor to create a new entry using #{Doing::Util.default_editor}.)
10
+ arg_name 'ENTRY'
11
+ command %i[now next] do |c|
12
+ c.example 'doing now', desc: 'Create a new entry with interactive prompts'
13
+ c.example 'doing now -e', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note"
14
+ c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
15
+ c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
16
+ c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
17
+ c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
18
+
19
+ c.desc 'Section'
20
+ c.arg_name 'NAME'
21
+ c.flag %i[s section]
22
+
23
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
24
+ c.switch %i[e editor], negatable: false, default_value: false
25
+
26
+ c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
27
+ c.arg_name 'DATE_STRING'
28
+ c.flag %i[b back started], type: DateBeginString
29
+
30
+ c.desc %(
31
+ Set a start and optionally end time as a date range ("from 1pm to 2:30pm").
32
+ If an end time is provided, a dated @done tag will be added
33
+ )
34
+ c.arg_name 'TIME_RANGE'
35
+ c.flag [:from], type: DateRangeString
36
+
37
+ c.desc 'Timed entry, marks last entry in section as @done'
38
+ c.switch %i[f finish_last], negatable: false, default_value: false
39
+
40
+ c.desc 'Include a note'
41
+ c.arg_name 'TEXT'
42
+ c.flag %i[n note]
43
+
44
+ c.desc 'Prompt for note via multi-line input'
45
+ c.switch %i[ask], negatable: false, default_value: false
46
+
47
+ # c.desc "Edit entry with specified app"
48
+ # c.arg_name 'editor_app'
49
+ # # c.flag [:a, :app]
50
+
51
+ c.action do |_global_options, options, args|
52
+ raise InvalidArgument, "--back and --from cannot be used together" if options[:back] && options[:from]
53
+
54
+ if options[:back]
55
+ date = options[:back]
56
+ elsif options[:from]
57
+ date, finish_date = options[:from]
58
+ options[:done] = finish_date
59
+ else
60
+ date = Time.now
61
+ end
62
+ raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
63
+
64
+ if options[:section]
65
+ section = @wwid.guess_section(options[:section]) || options[:section].cap_first
66
+ else
67
+ options[:section] = @settings['current_section']
68
+ end
69
+
70
+ ask_note = options[:ask] && !options[:editor] && args.count.positive? ? Doing::Prompt.read_lines(prompt: 'Add a note') : ''
71
+
72
+ if options[:editor]
73
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
74
+
75
+ input = date.strftime('%F %R | ')
76
+ input += args.join(' ') unless args.empty?
77
+ input += " @done(#{options[:done].strftime('%F %R')})" if options[:done]
78
+ input += "\n#{options[:note]}" if options[:note]
79
+ input += "\n#{ask_note}" if ask_note.good?
80
+ input = @wwid.fork_editor(input).strip
81
+
82
+ d, title, note = @wwid.format_input(input)
83
+ raise EmptyInput, 'No content' unless title.good?
84
+
85
+ if ask_note.empty? && options[:ask]
86
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
87
+ note.add(ask_note) if ask_note.good?
88
+ end
89
+
90
+ date = d.nil? ? date : d
91
+ @wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
92
+ @wwid.write(@wwid.doing_file)
93
+ elsif args.length.positive?
94
+ d, title, note = @wwid.format_input(args.join(' '))
95
+ date = d.nil? ? date : d
96
+ note.add(options[:note]) if options[:note]
97
+ note.add(ask_note) if ask_note.good?
98
+ entry = @wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
99
+ if options[:done] && entry.should_finish?
100
+ if entry.should_time?
101
+ entry.tag('done', value: options[:done])
102
+ else
103
+ entry.tag('done')
104
+ end
105
+ end
106
+ @wwid.write(@wwid.doing_file)
107
+ elsif $stdin.stat.size.positive?
108
+ input = $stdin.read.strip
109
+ d, title, note = @wwid.format_input(input)
110
+ unless d.nil?
111
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
112
+ date = d
113
+ end
114
+ note.add(options[:note]) if options[:note]
115
+ if ask_note.empty? && options[:ask]
116
+ ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
117
+ note.add(ask_note) if ask_note.good?
118
+ end
119
+ entry = @wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
120
+ if options[:done] && entry.should_finish?
121
+ if entry.should_time?
122
+ entry.tag('done', value: options[:done])
123
+ else
124
+ entry.tag('done')
125
+ end
126
+ end
127
+ @wwid.write(@wwid.doing_file)
128
+ else
129
+ tags = @wwid.all_tags(@wwid.content)
130
+ $stderr.puts Doing::Color.boldgreen("Add a new entry. Tab will autocomplete known tags. Ctrl-c to cancel.")
131
+ title = Doing::Prompt.read_line(prompt: 'Entry content', completions: tags)
132
+ raise EmptyInput, 'You must provide content when creating a new entry' unless title.good?
133
+
134
+ note = Doing::Note.new
135
+ note.add(options[:note]) if options[:note]
136
+ res = Doing::Prompt.yn('Add a note', default_response: false)
137
+ ask_note = res ? Doing::Prompt.read_lines(prompt: 'Enter note') : []
138
+ note.add(ask_note)
139
+
140
+ entry = @wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
141
+ if options[:done] && entry.should_finish?
142
+ if entry.should_time?
143
+ entry.tag('done', value: options[:done])
144
+ else
145
+ entry.tag('done')
146
+ end
147
+ end
148
+ @wwid.write(@wwid.doing_file)
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,66 @@
1
+ # @@on
2
+ desc 'List entries for a date'
3
+ long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
4
+ and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
5
+ it will create a range.)
6
+ arg_name 'DATE_STRING'
7
+ command :on do |c|
8
+ c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
9
+ c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
10
+ c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
11
+
12
+ c.desc 'Section'
13
+ c.arg_name 'NAME'
14
+ c.flag %i[s section], default_value: 'All'
15
+
16
+ c.desc 'Show time intervals on @done tasks'
17
+ c.switch %i[t times], default_value: true, negatable: true
18
+
19
+ c.desc 'Show elapsed time on entries without @done tag'
20
+ c.switch [:duration]
21
+
22
+ c.desc 'Show time totals at the end of output'
23
+ c.switch [:totals], default_value: false, negatable: false
24
+
25
+ c.desc 'Sort tags by (name|time)'
26
+ default = 'time'
27
+ default = @settings['tag_sort'] || 'name'
28
+ c.arg_name 'KEY'
29
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
30
+
31
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
32
+ c.arg_name 'FORMAT'
33
+ c.flag %i[o output]
34
+
35
+ c.desc "Output using a template from configuration"
36
+ c.arg_name 'TEMPLATE_KEY'
37
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
38
+
39
+ c.desc 'Override output format with a template string containing %placeholders'
40
+ c.arg_name 'TEMPLATE_STRING'
41
+ c.flag [:template]
42
+
43
+ c.action do |_global_options, options, args|
44
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
45
+
46
+ raise MissingArgument, 'Missing date argument' if args.empty?
47
+
48
+ date_string = args.join(' ').strip
49
+ if date_string =~ /^tod(?:ay)?/i
50
+ date_string = 'today to tomorrow 12am'
51
+ end
52
+ start, finish = date_string.split_date_range
53
+
54
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
55
+
56
+ message = "date interpreted as #{start}"
57
+ message += " to #{finish}" if finish
58
+ Doing.logger.debug('Interpreter:', message)
59
+
60
+ options[:times] = true if options[:totals]
61
+ options[:sort_tags] = options[:tag_sort] =~ /^n/i
62
+
63
+ Doing::Pager.page @wwid.list_date([start, finish], options[:section], options[:times], options[:output],
64
+ { template: options[:template], config_template: options[:config_template], duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ # @@open
2
+ desc 'Open the "doing" file in an editor'
3
+ long_desc "`doing open` defaults to using the editors->doing_file setting
4
+ in #{@config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
5
+ command :open do |c|
6
+ c.example 'doing open', desc: 'Open the doing file in the default editor'
7
+ c.desc 'Open with editor command (e.g. vim, mate)'
8
+ c.arg_name 'COMMAND'
9
+ c.flag %i[e editor]
10
+
11
+ if `uname` =~ /Darwin/
12
+ c.desc 'Open with app name'
13
+ c.arg_name 'APP_NAME'
14
+ c.flag %i[a app]
15
+
16
+ c.desc 'Open with app bundle id'
17
+ c.arg_name 'BUNDLE_ID'
18
+ c.flag %i[b bundle_id]
19
+ end
20
+
21
+ c.action do |_global_options, options, _args|
22
+ params = options.clone
23
+ params.delete_if do |k, v|
24
+ k.instance_of?(String) || v.nil? || v == false
25
+ end
26
+
27
+ if options[:editor]
28
+ raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
29
+
30
+ editor = TTY::Which.which(options[:editor])
31
+ system %(#{editor} "#{File.expand_path(@wwid.doing_file)}")
32
+ elsif `uname` =~ /Darwin/
33
+ if options[:app]
34
+ system %(open -a "#{options[:app]}" "#{File.expand_path(@wwid.doing_file)}")
35
+ elsif options[:bundle_id]
36
+ system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(@wwid.doing_file)}")
37
+ elsif Doing::Util.find_default_editor('doing_file')
38
+ editor = Doing::Util.find_default_editor('doing_file')
39
+ if Doing::Util.exec_available(editor.split(/ /).first)
40
+ system %(#{editor} "#{File.expand_path(@wwid.doing_file)}")
41
+ else
42
+ system %(open -a "#{editor}" "#{File.expand_path(@wwid.doing_file)}")
43
+ end
44
+ else
45
+ system %(open "#{File.expand_path(@wwid.doing_file)}")
46
+ end
47
+ else
48
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
49
+
50
+ system %(#{Doing::Util.default_editor} "#{File.expand_path(@wwid.doing_file)}")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ # @@plugins
2
+ desc 'List installed plugins'
3
+ long_desc %(Lists available plugins, including user-installed plugins.
4
+
5
+ Export plugins are available with the `--output` flag on commands that support it.
6
+
7
+ Import plugins are available using `doing import --type PLUGIN`.
8
+ )
9
+ command :plugins do |c|
10
+ c.example 'doing plugins', desc: 'List all plugins'
11
+ c.example 'doing plugins -t import', desc: 'List all import plugins'
12
+
13
+ c.desc 'List plugins of type (import, export)'
14
+ c.arg_name 'TYPE'
15
+ c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
16
+
17
+ c.desc 'List in single column for completion'
18
+ c.switch %i[c column], negatable: false, default_value: false
19
+
20
+ c.action do |_global_options, options, _args|
21
+ Doing::Plugins.list_plugins(options)
22
+ end
23
+ end
@@ -0,0 +1,78 @@
1
+ # @@recent
2
+ desc 'List recent entries'
3
+ default_value 10
4
+ arg_name 'COUNT'
5
+ command :recent do |c|
6
+ c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
7
+ c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
8
+ c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
9
+ c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
10
+
11
+ c.desc 'Section'
12
+ c.arg_name 'NAME'
13
+ c.flag %i[s section], default_value: 'All'
14
+
15
+ c.desc 'Show time intervals on @done tasks'
16
+ c.switch %i[t times], default_value: true, negatable: true
17
+
18
+ c.desc "Output using a template from configuration"
19
+ c.arg_name 'TEMPLATE_KEY'
20
+ c.flag [:config_template], type: TemplateName, default_value: 'recent'
21
+
22
+ c.desc 'Override output format with a template string containing %placeholders'
23
+ c.arg_name 'TEMPLATE_STRING'
24
+ c.flag [:template]
25
+
26
+ c.desc 'Show elapsed time on entries without @done tag'
27
+ c.switch [:duration]
28
+
29
+ c.desc 'Show intervals with totals at the end of output'
30
+ c.switch [:totals], default_value: false, negatable: false
31
+
32
+ c.desc 'Sort tags by (name|time)'
33
+ default = 'time'
34
+ default = @settings['tag_sort'] || 'name'
35
+ c.arg_name 'KEY'
36
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
37
+
38
+ c.desc 'Select from a menu of matching entries to perform additional operations'
39
+ c.switch %i[i interactive], negatable: false, default_value: false
40
+
41
+ c.action do |global_options, options, args|
42
+ section = @wwid.guess_section(options[:section]) || options[:section].cap_first
43
+
44
+ unless global_options[:version]
45
+ if @settings['templates']['recent'].key?('count')
46
+ config_count = @settings['templates']['recent']['count'].to_i
47
+ else
48
+ config_count = 10
49
+ end
50
+
51
+ if options[:interactive]
52
+ count = 0
53
+ else
54
+ count = args.empty? ? config_count : args[0].to_i
55
+ end
56
+
57
+ options[:times] = true if options[:totals]
58
+ options[:sort_tags] = options[:tag_sort] =~ /^n/i
59
+
60
+ template = @settings['templates']['recent'].deep_merge(@settings['templates']['default'])
61
+ tags_color = template.key?('tags_color') ? template['tags_color'] : nil
62
+
63
+ opts = {
64
+ sort_tags: options[:sort_tags],
65
+ tags_color: tags_color,
66
+ times: options[:times],
67
+ totals: options[:totals],
68
+ interactive: options[:interactive],
69
+ duration: options[:duration],
70
+ config_template: options[:config_template],
71
+ template: options[:template]
72
+ }
73
+
74
+ Doing::Pager::page @wwid.recent(count, section.cap_first, opts)
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,22 @@
1
+ # @@redo
2
+ long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo'
3
+ arg_name 'COUNT'
4
+ command :redo do |c|
5
+ c.desc 'Specify alternate doing file'
6
+ c.arg_name 'PATH'
7
+ c.flag %i[f file], default_value: @wwid.doing_file
8
+
9
+ c.desc 'Select from an interactive menu'
10
+ c.switch %i[i interactive]
11
+
12
+ c.action do |_global, options, args|
13
+ file = options[:file] || @wwid.doing_file
14
+ count = args.empty? ? 1 : args[0].to_i
15
+ raise InvalidArgument, "Invalid count specified for redo" unless count&.positive?
16
+ if options[:interactive]
17
+ Doing::Util::Backup.select_redo(file)
18
+ else
19
+ Doing::Util::Backup.redo_backup(file, count: count)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,106 @@
1
+ # @@reset @@begin
2
+ desc 'Reset the start time of an entry'
3
+ long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
4
+ If no argument is provided, the start time will be reset to the current time.
5
+ If a date string is provided as an argument, the start time will be set to the parsed result.'
6
+ arg_name 'DATE_STRING', optional: true
7
+ command %i[reset begin] do |c|
8
+ c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
9
+ c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
10
+ c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
11
+ c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
12
+
13
+ c.desc 'Limit search to section'
14
+ c.arg_name 'NAME'
15
+ c.flag %i[s section], default_value: 'All'
16
+
17
+ c.desc 'Resume entry (remove @done)'
18
+ c.switch %i[r resume], default_value: true
19
+
20
+ c.desc 'Change start date but do not remove @done (shortcut for --no-resume)'
21
+ c.switch [:n]
22
+
23
+ c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?)'
24
+ c.arg_name 'TAG'
25
+ c.flag [:tag]
26
+
27
+ c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
28
+ c.arg_name 'QUERY'
29
+ c.flag [:search]
30
+
31
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
32
+ c.arg_name 'QUERY'
33
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
34
+
35
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
36
+ # c.switch [:fuzzy], default_value: false, negatable: false
37
+
38
+ c.desc 'Force exact search string matching (case sensitive)'
39
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
40
+
41
+ c.desc 'Reset items that *don\'t* match search/tag filters'
42
+ c.switch [:not], default_value: false, negatable: false
43
+
44
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
45
+ c.arg_name 'TYPE'
46
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
47
+
48
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
49
+ c.arg_name 'BOOLEAN'
50
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
51
+
52
+ c.desc 'Select from a menu of matching entries'
53
+ c.switch %i[i interactive], negatable: false, default_value: false
54
+
55
+ c.action do |global_options, options, args|
56
+ if args.count > 0
57
+ reset_date = args.join(' ').chronify(guess: :begin)
58
+ raise InvalidArgument, 'Invalid date string' unless reset_date
59
+ else
60
+ reset_date = Time.now
61
+ end
62
+
63
+ options[:fuzzy] = false
64
+ if options[:section]
65
+ options[:section] = @wwid.guess_section(options[:section]) || options[:section].cap_first
66
+ end
67
+
68
+ options[:bool] = options[:bool].normalize_bool
69
+
70
+ options[:case] = options[:case].normalize_case
71
+
72
+ if options[:search]
73
+ search = options[:search]
74
+ search.sub!(/^'?/, "'") if options[:exact]
75
+ options[:search] = search
76
+ end
77
+
78
+
79
+ items = @wwid.filter_items([], opt: options)
80
+
81
+ if options[:interactive]
82
+ last_entry = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
83
+ menu: true,
84
+ header: '',
85
+ prompt: 'Select an entry to start/reset > ',
86
+ multiple: false,
87
+ sort: false,
88
+ show_if_single: true)
89
+ else
90
+ last_entry = items.reverse.last
91
+ end
92
+
93
+ unless last_entry
94
+ Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
95
+ return
96
+ end
97
+
98
+ old_item = last_entry.clone
99
+
100
+ @wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
101
+ Doing::Hooks.trigger :post_entry_updated, @wwid, last_entry, old_item
102
+ # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
103
+
104
+ @wwid.write(@wwid.doing_file)
105
+ end
106
+ end