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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +4 -4
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +283 -108
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/commands/add_section.rb +13 -0
- data/bin/commands/again.rb +99 -0
- data/bin/commands/archive.rb +96 -0
- data/bin/commands/cancel.rb +102 -0
- data/bin/commands/changes.rb +42 -0
- data/bin/commands/choose.rb +9 -0
- data/bin/commands/colors.rb +19 -0
- data/bin/commands/commands.rb +87 -0
- data/bin/commands/commands_accepting.rb +25 -0
- data/bin/commands/completion.rb +24 -0
- data/bin/commands/config.rb +245 -0
- data/bin/commands/done.rb +249 -0
- data/bin/commands/finish.rb +149 -0
- data/bin/commands/flag.rb +126 -0
- data/bin/commands/grep.rb +124 -0
- data/bin/commands/import.rb +101 -0
- data/bin/commands/install_fzf.rb +17 -0
- data/bin/commands/last.rb +114 -0
- data/bin/commands/meanwhile.rb +86 -0
- data/bin/commands/note.rb +130 -0
- data/bin/commands/now.rb +151 -0
- data/bin/commands/on.rb +66 -0
- data/bin/commands/open.rb +53 -0
- data/bin/commands/plugins.rb +23 -0
- data/bin/commands/recent.rb +78 -0
- data/bin/commands/redo.rb +22 -0
- data/bin/commands/reset.rb +106 -0
- data/bin/commands/rotate.rb +73 -0
- data/bin/commands/sections.rb +11 -0
- data/bin/commands/select.rb +123 -0
- data/bin/commands/show.rb +231 -0
- data/bin/commands/since.rb +64 -0
- data/bin/commands/tag.rb +179 -0
- data/bin/commands/tag_dir.rb +29 -0
- data/bin/commands/tags.rb +93 -0
- data/bin/commands/template.rb +61 -0
- data/bin/commands/today.rb +65 -0
- data/bin/commands/undo.rb +49 -0
- data/bin/commands/view.rb +238 -0
- data/bin/commands/views.rb +11 -0
- data/bin/commands/yesterday.rb +73 -0
- data/bin/doing +39 -3641
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +2 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +1 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +46 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +1 -1
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +1 -1
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/FalseClass.html +201 -0
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +1 -1
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +201 -0
- data/docs/doc/_index.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +374 -366
- data/docs/doc/top-level-namespace.html +1 -1
- data/doing.rdoc +15 -5
- data/lib/completion/_doing.zsh +3 -3
- data/lib/completion/doing.fish +1 -1
- data/lib/doing/changelog/changes.rb +1 -1
- data/lib/doing/configuration.rb +1 -0
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/prompt.rb +8 -0
- data/lib/doing/version.rb +1 -1
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/helpers/threaded_tests.rb +25 -19
- 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
|
data/bin/commands/now.rb
ADDED
|
@@ -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
|
data/bin/commands/on.rb
ADDED
|
@@ -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
|