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,126 @@
1
+ # @@mark @@flag
2
+ desc 'Mark last entry as flagged'
3
+ command %i[mark flag] do |c|
4
+ c.example 'doing flag', desc: 'Add @flagged to the last entry created'
5
+ c.example 'doing mark', desc: 'mark is an alias for flag'
6
+ c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
7
+ 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'
8
+
9
+ c.desc 'Section'
10
+ c.arg_name 'SECTION_NAME'
11
+ c.flag %i[s section], default_value: 'All'
12
+
13
+ c.desc 'How many recent entries to tag (0 for all)'
14
+ c.arg_name 'COUNT'
15
+ c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
16
+
17
+ c.desc 'Don\'t ask permission to flag all entries when count is 0'
18
+ c.switch %i[force], negatable: false, default_value: false
19
+
20
+ c.desc 'Include current date/time with tag'
21
+ c.switch %i[d date], negatable: false, default_value: false
22
+
23
+ c.desc 'Remove flag'
24
+ c.switch %i[r remove], negatable: false, default_value: false
25
+
26
+ c.desc 'Flag last entry (or entries) not marked @done'
27
+ c.switch %i[u unfinished], negatable: false, default_value: false
28
+
29
+ c.desc 'Flag the last entry containing TAG.
30
+ Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
31
+ c.arg_name 'TAG'
32
+ c.flag [:tag], type: TagArray
33
+
34
+ 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")'
35
+ c.arg_name 'QUERY'
36
+ c.flag [:search]
37
+
38
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
39
+ c.arg_name 'QUERY'
40
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
41
+
42
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
43
+ # c.switch [:fuzzy], default_value: false, negatable: false
44
+
45
+ c.desc 'Force exact search string matching (case sensitive)'
46
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
47
+
48
+ c.desc 'Flag items that *don\'t* match search/tag/date filters'
49
+ c.switch [:not], default_value: false, negatable: false
50
+
51
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
52
+ c.arg_name 'TYPE'
53
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
54
+
55
+ c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
56
+ c.arg_name 'BOOLEAN'
57
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
58
+
59
+ c.desc 'Select item(s) to flag from a menu of matching entries'
60
+ c.switch %i[i interactive], negatable: false, default_value: false
61
+
62
+ c.action do |_global_options, options, _args|
63
+ options[:fuzzy] = false
64
+ mark = @settings['marker_tag'] || 'flagged'
65
+
66
+ raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
67
+
68
+ section = 'All'
69
+
70
+ if options[:section]
71
+ section = @wwid.guess_section(options[:section]) || options[:section].cap_first
72
+ end
73
+
74
+ if options[:tag].nil?
75
+ search_tags = []
76
+ else
77
+ search_tags = options[:tag]
78
+ end
79
+
80
+ if options[:interactive]
81
+ count = 0
82
+ options[:force] = true
83
+ else
84
+ count = options[:count].to_i
85
+ end
86
+
87
+ options[:case] = options[:case].normalize_case
88
+
89
+ if options[:search]
90
+ search = options[:search]
91
+ search.sub!(/^'?/, "'") if options[:exact]
92
+ options[:search] = search
93
+ end
94
+
95
+ if count.zero? && !options[:force]
96
+ if options[:search]
97
+ section_q = ' matching your search terms'
98
+ elsif options[:tag]
99
+ section_q = ' matching your tag search'
100
+ elsif section == 'All'
101
+ section_q = ''
102
+ else
103
+ section_q = " in section #{section}"
104
+ end
105
+
106
+
107
+ question = if options[:remove]
108
+ "Are you sure you want to unflag all entries#{section_q}"
109
+ else
110
+ "Are you sure you want to flag all records#{section_q}"
111
+ end
112
+
113
+ res = Doing::Prompt.yn(question, default_response: false)
114
+
115
+ exit_now! 'Cancelled' unless res
116
+ end
117
+
118
+ options[:count] = count
119
+ options[:section] = section
120
+ options[:tag] = search_tags
121
+ options[:tags] = [mark]
122
+ options[:tag_bool] = options[:bool].normalize_bool
123
+
124
+ @wwid.tag_last(options)
125
+ end
126
+ end
@@ -0,0 +1,124 @@
1
+ # @@grep @@search
2
+ desc 'Search for entries'
3
+ long_desc %(
4
+ Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
5
+
6
+ To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
7
+ )
8
+ arg_name 'SEARCH_PATTERN'
9
+ command %i[grep search] do |c|
10
+ c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
11
+ c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
12
+ c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
13
+ 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'
14
+
15
+ c.desc 'Section'
16
+ c.arg_name 'NAME'
17
+ c.flag %i[s section], default_value: 'All'
18
+
19
+ c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
20
+ c.arg_name 'DATE_STRING'
21
+ c.flag [:before], type: DateBeginString
22
+
23
+ c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day'
24
+ c.arg_name 'DATE_STRING'
25
+ c.flag [:after], type: DateEndString
26
+
27
+ c.desc %(
28
+ Date range to show, or a single day to filter date on.
29
+ Date range argument should be quoted. Date specifications can be natural language.
30
+ To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
31
+
32
+ If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
33
+ by time of day.
34
+ )
35
+ c.arg_name 'DATE_OR_RANGE'
36
+ c.flag [:from], type: DateRangeString
37
+
38
+ c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
39
+ c.arg_name 'FORMAT'
40
+ c.flag %i[o output]
41
+
42
+ c.desc "Output using a template from configuration"
43
+ c.arg_name 'TEMPLATE_KEY'
44
+ c.flag [:config_template], type: TemplateName, default_value: 'default'
45
+
46
+ c.desc 'Override output format with a template string containing %placeholders'
47
+ c.arg_name 'TEMPLATE_STRING'
48
+ c.flag [:template]
49
+
50
+ c.desc 'Show time intervals on @done tasks'
51
+ c.switch %i[t times], default_value: true, negatable: true
52
+
53
+ c.desc 'Show elapsed time on entries without @done tag'
54
+ c.switch [:duration]
55
+
56
+ c.desc 'Show intervals with totals at the end of output'
57
+ c.switch [:totals], default_value: false, negatable: false
58
+
59
+ c.desc 'Sort tags by (name|time)'
60
+ default = 'time'
61
+ default = @settings['tag_sort'] || 'name'
62
+ c.arg_name 'KEY'
63
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
64
+
65
+ c.desc 'Only show items with recorded time intervals'
66
+ c.switch [:only_timed], default_value: false, negatable: false
67
+
68
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
69
+ # c.switch [:fuzzy], default_value: false, negatable: false
70
+
71
+ c.desc 'Force exact string matching (case sensitive)'
72
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
73
+
74
+ c.desc 'Show items that *don\'t* match search string'
75
+ c.switch [:not], default_value: false, negatable: false
76
+
77
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
78
+ c.arg_name 'TYPE'
79
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
80
+
81
+ c.desc "Highlight search matches in output. Only affects command line output"
82
+ c.switch %i[h hilite], default_value: @settings.dig('search', 'highlight')
83
+
84
+ c.desc "Edit matching entries with #{Doing::Util.default_editor}"
85
+ c.switch %i[e editor], negatable: false, default_value: false
86
+
87
+ c.desc "Delete matching entries"
88
+ c.switch %i[d delete], negatable: false, default_value: false
89
+
90
+ c.desc 'Display an interactive menu of results to perform further operations'
91
+ c.switch %i[i interactive], default_value: false, negatable: false
92
+
93
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
94
+ c.arg_name 'QUERY'
95
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
96
+
97
+ c.desc 'Combine multiple tags or value queries using AND, OR, or NOT'
98
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
99
+
100
+ c.action do |_global_options, options, args|
101
+ options[:fuzzy] = false
102
+ raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
103
+
104
+ template = @settings['templates'][options[:config_template]].deep_merge(@settings)
105
+ tags_color = template.key?('tags_color') ? template['tags_color'] : nil
106
+
107
+ section = @wwid.guess_section(options[:section]) if options[:section]
108
+
109
+ options[:case] = options[:case].normalize_case
110
+ options[:bool] = options[:bool].normalize_bool
111
+
112
+ search = args.join(' ')
113
+ search.sub!(/^'?/, "'") if options[:exact]
114
+
115
+ options[:times] = true if options[:totals]
116
+ options[:sort_tags] = options[:tag_sort] =~ /^n/i
117
+ options[:highlight] = true
118
+ options[:search] = search
119
+ options[:section] = section
120
+ options[:tags_color] = tags_color
121
+
122
+ Doing::Pager.page @wwid.list_section(options)
123
+ end
124
+ end
@@ -0,0 +1,101 @@
1
+ # @@import
2
+ desc 'Import entries from an external source'
3
+ long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
4
+ arg_name 'PATH'
5
+ command :import do |c|
6
+ c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
7
+ c.example 'doing import --type doing --tag imported --no-autotag ~/doing_backup.md', desc: 'Import an Doing archive, tag all entries with @imported, skip autotagging'
8
+ c.example 'doing import --type doing --from "10/1 to 10/15" ~/doing_backup.md', desc: 'Import a Doing archive, only importing entries between two dates'
9
+
10
+ c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
11
+ c.arg_name 'TYPE'
12
+ c.flag :type, default_value: 'doing'
13
+
14
+ c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
15
+ c.arg_name 'QUERY'
16
+ c.flag [:search]
17
+
18
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
19
+ # c.switch [:fuzzy], default_value: false, negatable: false
20
+
21
+ c.desc 'Force exact search string matching (case sensitive)'
22
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
23
+
24
+ c.desc 'Import items that *don\'t* match search/tag/date filters'
25
+ c.switch [:not], default_value: false, negatable: false
26
+
27
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
28
+ c.arg_name 'TYPE'
29
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
30
+
31
+ c.desc 'Only import items with recorded time intervals'
32
+ c.switch [:only_timed], default_value: false, negatable: false
33
+
34
+ c.desc 'Target section'
35
+ c.arg_name 'NAME'
36
+ c.flag %i[s section]
37
+
38
+ c.desc 'Tag all imported entries'
39
+ c.arg_name 'TAGS'
40
+ c.flag %i[t tag]
41
+
42
+ c.desc 'Autotag entries'
43
+ c.switch :autotag, negatable: true, default_value: true
44
+
45
+ c.desc 'Prefix entries with'
46
+ c.arg_name 'PREFIX'
47
+ c.flag :prefix
48
+
49
+ # TODO: Allow time range filtering
50
+ c.desc 'Import entries older than date'
51
+ c.arg_name 'DATE_STRING'
52
+ c.flag [:before], type: DateBeginString
53
+
54
+ c.desc 'Import entries newer than date'
55
+ c.arg_name 'DATE_STRING'
56
+ c.flag [:after], type: DateEndString
57
+
58
+ c.desc %(
59
+ Date range to import. Date range argument should be quoted. Date specifications can be natural language.
60
+ To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
61
+ Has no effect unless the import plugin has implemented date range filtering.
62
+ )
63
+ c.arg_name 'DATE_OR_RANGE'
64
+ c.flag %i[f from], type: DateRangeString
65
+
66
+ c.desc 'Allow entries that overlap existing times'
67
+ c.switch [:overlap], negatable: true
68
+
69
+ c.action do |_global_options, options, args|
70
+ options[:fuzzy] = false
71
+ if options[:section]
72
+ options[:section] = @wwid.guess_section(options[:section]) || options[:section].cap_first
73
+ end
74
+
75
+ if options[:search]
76
+ search = options[:search]
77
+ search.sub!(/^'?/, "'") if options[:exact]
78
+ options[:search] = search
79
+ end
80
+
81
+ if options[:from]
82
+ options[:date_filter] = options[:from]
83
+
84
+ raise InvalidTimeExpression, 'Unrecognized date string' unless options[:date_filter][0]
85
+ elsif options[:before] || options[:after]
86
+ options[:date_filter] = [nil, nil]
87
+ options[:date_filter][1] = options[:before] || Time.now + (1 << 64)
88
+ options[:date_filter][0] = options[:after] || Time.now - (1 << 64)
89
+ end
90
+
91
+ options[:case] = options[:case].normalize_case
92
+
93
+ if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
94
+ options[:no_overlap] = !options[:overlap]
95
+ @wwid.import(args, options)
96
+ @wwid.write(@wwid.doing_file)
97
+ else
98
+ raise InvalidPluginType, "Invalid import type: #{options[:type]}"
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,17 @@
1
+ # @@install_fzf
2
+ command :install_fzf do |c|
3
+ c.desc 'Force reinstall'
4
+ c.switch %i[r reinstall], default_value: false
5
+
6
+ c.desc 'Uninstall'
7
+ c.switch %i[u uninstall], default_value: false, negatable: false
8
+
9
+ c.action do |g, o, a|
10
+ if o[:uninstall]
11
+ Doing::Prompt.uninstall_fzf
12
+ else
13
+ Doing.logger.warn('fzf:', 'force reinstall') if o[:reinstall]
14
+ res = Doing::Prompt.install_fzf(force: o[:reinstall])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,114 @@
1
+ # @@last
2
+ desc 'Show the last entry, optionally edit'
3
+ long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
4
+ allowing `doing last` to target historical entries.'
5
+ command :last do |c|
6
+ c.example 'doing last', desc: 'Show the most recent entry in all sections'
7
+ c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
8
+ c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
9
+ c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
10
+ c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
11
+ c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
12
+ c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
13
+
14
+ c.desc 'Specify a section'
15
+ c.arg_name 'NAME'
16
+ c.flag %i[s section], default_value: 'All'
17
+
18
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
19
+ c.switch %i[e editor], negatable: false, default_value: false
20
+
21
+ c.desc "Delete the last entry"
22
+ c.switch %i[d delete], negatable: false, default_value: false
23
+
24
+ c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?)'
25
+ c.arg_name 'TAG'
26
+ c.flag [:tag], type: TagArray
27
+
28
+ c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans'
29
+ c.arg_name 'BOOLEAN'
30
+ c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
31
+
32
+ c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
33
+ c.arg_name 'QUERY'
34
+ c.flag [:search]
35
+
36
+ c.desc "Output using a template from configuration"
37
+ c.arg_name 'TEMPLATE_KEY'
38
+ c.flag [:config_template], type: TemplateName, default_value: 'last'
39
+
40
+ c.desc 'Override output format with a template string containing %placeholders'
41
+ c.arg_name 'TEMPLATE_STRING'
42
+ c.flag [:template]
43
+
44
+ c.desc "Highlight search matches in output. Only affects command line output"
45
+ c.switch %i[h hilite], default_value: @settings.dig('search', 'highlight')
46
+
47
+ c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
48
+ c.arg_name 'QUERY'
49
+ c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
50
+
51
+ c.desc 'Show elapsed time if entry is not tagged @done'
52
+ c.switch [:duration]
53
+
54
+ # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
55
+ # c.switch [:fuzzy], default_value: false, negatable: false
56
+
57
+ c.desc 'Force exact search string matching (case sensitive)'
58
+ c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
59
+
60
+ c.desc 'Show items that *don\'t* match search string or tag filter'
61
+ c.switch [:not], default_value: false, negatable: false
62
+
63
+ c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
64
+ c.arg_name 'TYPE'
65
+ c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
66
+
67
+ c.action do |global_options, options, _args|
68
+ options[:fuzzy] = false
69
+ raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
70
+
71
+ if options[:tag].nil?
72
+ options[:tag] = []
73
+ else
74
+ options[:tag] = options[:tag]
75
+ options[:bool] = options[:bool].normalize_bool
76
+ end
77
+
78
+ options[:case] = options[:case].normalize_case
79
+
80
+ options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
81
+
82
+ if options[:editor]
83
+ @wwid.edit_last(section: options[:section],
84
+ options: {
85
+ search: options[:search],
86
+ fuzzy: options[:fuzzy],
87
+ case: options[:case],
88
+ tag: options[:tag],
89
+ tag_bool: options[:bool],
90
+ not: options[:not],
91
+ val: options[:val],
92
+ bool: options[:bool]
93
+ })
94
+ else
95
+ last = @wwid.last(times: true, section: options[:section],
96
+ options: {
97
+ config_template: options[:config_template],
98
+ template: options[:template],
99
+ duration: options[:duration],
100
+ search: options[:search],
101
+ fuzzy: options[:fuzzy],
102
+ case: options[:case],
103
+ hilite: options[:hilite],
104
+ negate: options[:not],
105
+ tag: options[:tag],
106
+ tag_bool: options[:bool],
107
+ delete: options[:delete],
108
+ bool: options[:bool],
109
+ val: options[:val]
110
+ })
111
+ Doing::Pager::page last.strip if last
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,86 @@
1
+ # @@meanwhile
2
+ desc 'Finish any running @meanwhile tasks and optionally create a new one'
3
+ long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
4
+ This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
5
+ big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
6
+ itself to mark the entry as @done.'
7
+ arg_name 'ENTRY', optional: true
8
+ command :meanwhile do |c|
9
+ c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
10
+ c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
11
+ c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
12
+ c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
13
+
14
+ c.desc 'Section'
15
+ c.arg_name 'NAME'
16
+ c.flag %i[s section]
17
+
18
+ c.desc "Edit entry with #{Doing::Util.default_editor}"
19
+ c.switch %i[e editor], negatable: false, default_value: false
20
+
21
+ c.desc 'Archive previous @meanwhile entry'
22
+ c.switch %i[a archive], negatable: false, default_value: false
23
+
24
+ c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
25
+ c.arg_name 'DATE_STRING'
26
+ c.flag %i[b back started], type: DateBeginString
27
+
28
+ c.desc 'Note'
29
+ c.arg_name 'TEXT'
30
+ c.flag %i[n note]
31
+
32
+ c.desc 'Prompt for note via multi-line input'
33
+ c.switch %i[ask], negatable: false, default_value: false
34
+
35
+ c.action do |_global_options, options, args|
36
+ if options[:back]
37
+ date = options[:back]
38
+
39
+ raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
40
+ else
41
+ date = Time.now
42
+ end
43
+
44
+ if options[:section]
45
+ section = @wwid.guess_section(options[:section]) || options[:section].cap_first
46
+ else
47
+ section = @settings['current_section']
48
+ end
49
+ input = ''
50
+
51
+ ask_note = options[:ask] ? Doing::Prompt.read_lines(prompt: 'Add a note') : []
52
+
53
+ if options[:editor]
54
+ raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
55
+ input += date.strftime('%F %R | ')
56
+ input += args.join(' ') unless args.empty?
57
+ input += "\n#{options[:note]}" if options[:note]
58
+ input += "\n#{ask_note}" unless ask_note.good?
59
+
60
+ input = @wwid.fork_editor(input).strip
61
+ elsif !args.empty?
62
+ input = args.join(' ')
63
+ elsif $stdin.stat.size.positive?
64
+ input = $stdin.read.strip
65
+ end
66
+
67
+ if input.good?
68
+ d, input, note = @wwid.format_input(input)
69
+ unless d.nil?
70
+ Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
71
+ date = d
72
+ end
73
+ else
74
+ input = nil
75
+ note = []
76
+ end
77
+
78
+ unless options[:editor]
79
+ note.add(options[:note]) if options[:note]
80
+ note.add(ask_note) if ask_note.good?
81
+ end
82
+
83
+ @wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
84
+ @wwid.write(@wwid.doing_file)
85
+ end
86
+ end