doing 2.1.2pre → 2.1.6pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +19 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +62 -14
- data/Gemfile.lock +25 -1
- data/README.md +5 -1
- data/Rakefile +2 -0
- data/bin/doing +429 -142
- data/docs/_config.yml +1 -0
- data/{doc → docs/doc}/Array.html +63 -1
- data/docs/doc/BooleanTermParser/Clause.html +293 -0
- data/docs/doc/BooleanTermParser/Operator.html +172 -0
- data/docs/doc/BooleanTermParser/Query.html +417 -0
- data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
- data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
- data/docs/doc/BooleanTermParser.html +115 -0
- data/docs/doc/Doing/CLIFormat.html +131 -0
- data/{doc → docs/doc}/Doing/Color.html +2 -2
- data/{doc → docs/doc}/Doing/Completion.html +1 -1
- data/{doc → docs/doc}/Doing/Configuration.html +163 -69
- data/{doc → docs/doc}/Doing/Content.html +0 -0
- data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
- data/{doc → docs/doc}/Doing/Errors.html +1 -1
- data/{doc → docs/doc}/Doing/Hooks.html +1 -1
- data/{doc → docs/doc}/Doing/Item.html +135 -89
- data/{doc → docs/doc}/Doing/Items.html +36 -2
- data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
- data/{doc → docs/doc}/Doing/Note.html +5 -134
- data/{doc → docs/doc}/Doing/Pager.html +1 -1
- data/{doc → docs/doc}/Doing/Plugins.html +431 -35
- data/{doc → docs/doc}/Doing/Prompt.html +70 -18
- data/{doc → docs/doc}/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +713 -0
- data/docs/doc/Doing/Util/Backup.html +686 -0
- data/{doc → docs/doc}/Doing/Util.html +16 -4
- data/{doc → docs/doc}/Doing/WWID.html +133 -73
- data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
- data/{doc → docs/doc}/Doing.html +4 -4
- data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/{doc → docs/doc}/GLI/Commands.html +1 -1
- data/{doc → docs/doc}/GLI.html +1 -1
- data/{doc → docs/doc}/Hash.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +172 -0
- data/docs/doc/PhraseParser/PhraseClause.html +303 -0
- data/docs/doc/PhraseParser/Query.html +495 -0
- data/docs/doc/PhraseParser/QueryParser.html +136 -0
- data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
- data/docs/doc/PhraseParser/TermClause.html +293 -0
- data/docs/doc/PhraseParser.html +115 -0
- data/{doc → docs/doc}/Status.html +1 -1
- data/{doc → docs/doc}/String.html +319 -13
- data/{doc → docs/doc}/Symbol.html +35 -1
- data/{doc → docs/doc}/Time.html +70 -2
- data/{doc → docs/doc}/_index.html +132 -4
- data/docs/doc/class_list.html +51 -0
- data/{doc → docs/doc}/css/common.css +0 -0
- data/{doc → docs/doc}/css/full_list.css +0 -0
- data/{doc → docs/doc}/css/style.css +0 -0
- data/{doc → docs/doc}/file.README.html +6 -2
- data/{doc → docs/doc}/file_list.html +0 -0
- data/{doc → docs/doc}/frames.html +0 -0
- data/{doc → docs/doc}/index.html +6 -2
- data/{doc → docs/doc}/js/app.js +0 -0
- data/{doc → docs/doc}/js/full_list.js +0 -0
- data/{doc → docs/doc}/js/jquery.js +0 -0
- data/{doc → docs/doc}/method_list.html +684 -196
- data/{doc → docs/doc}/top-level-namespace.html +2 -2
- data/docs/index.md +60 -0
- data/doing.gemspec +3 -0
- data/doing.rdoc +222 -74
- data/example_plugin.rb +3 -1
- data/lib/completion/_doing.zsh +53 -41
- data/lib/completion/doing.bash +17 -6
- data/lib/completion/doing.fish +321 -2
- data/lib/doing/array.rb +9 -0
- data/lib/doing/boolean_term_parser.rb +86 -0
- data/lib/doing/completion/fish_completion.rb +46 -3
- data/lib/doing/completion/zsh_completion.rb +1 -1
- data/lib/doing/configuration.rb +48 -21
- data/lib/doing/item.rb +105 -10
- data/lib/doing/items.rb +6 -0
- data/lib/doing/log_adapter.rb +28 -0
- data/lib/doing/note.rb +31 -30
- data/lib/doing/phrase_parser.rb +124 -0
- data/lib/doing/plugin_manager.rb +84 -21
- data/lib/doing/plugins/export/dayone_export.rb +209 -0
- data/lib/doing/plugins/export/html_export.rb +2 -2
- data/lib/doing/plugins/export/json_export.rb +1 -0
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +94 -86
- data/lib/doing/prompt.rb +26 -15
- data/lib/doing/string.rb +114 -29
- data/lib/doing/string_chronify.rb +5 -1
- data/lib/doing/symbol.rb +4 -0
- data/lib/doing/template_string.rb +197 -0
- data/lib/doing/time.rb +32 -0
- data/lib/doing/util.rb +6 -7
- data/lib/doing/util_backup.rb +287 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +105 -41
- data/lib/doing.rb +9 -0
- data/lib/examples/plugins/say_export.rb +1 -1
- data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
- data/lib/templates/doing-dayone-entry.erb +6 -0
- data/lib/templates/doing-dayone.erb +5 -0
- metadata +136 -51
- data/doc/class_list.html +0 -51
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Doing
|
5
|
+
module Util
|
6
|
+
## Backup utils
|
7
|
+
module Backup
|
8
|
+
extend self
|
9
|
+
include Util
|
10
|
+
|
11
|
+
##
|
12
|
+
## Delete all but most recent 5 backups
|
13
|
+
##
|
14
|
+
## @param limit Maximum number of backups to retain
|
15
|
+
##
|
16
|
+
def prune_backups(filename, limit = 10)
|
17
|
+
backups = get_backups(filename)
|
18
|
+
return unless backups.count > limit
|
19
|
+
|
20
|
+
backups[limit..-1].each do |file|
|
21
|
+
FileUtils.rm(File.join(backup_dir, file))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
## Restore the most recent backup. If a filename is
|
27
|
+
## provided, only backups of that filename will be used.
|
28
|
+
##
|
29
|
+
## @param filename The filename to restore, if
|
30
|
+
## different from default
|
31
|
+
##
|
32
|
+
def restore_last_backup(filename = nil, count: 1)
|
33
|
+
Doing.logger.benchmark(:restore_backup, :start)
|
34
|
+
filename ||= Doing.config.settings['doing_file']
|
35
|
+
|
36
|
+
result = get_backups(filename).slice(count - 1)
|
37
|
+
raise DoingRuntimeError, 'End of undo history' if result.nil?
|
38
|
+
|
39
|
+
backup_file = File.join(backup_dir, result)
|
40
|
+
|
41
|
+
save_undone(filename)
|
42
|
+
FileUtils.mv(backup_file, filename)
|
43
|
+
prune_backups_after(File.basename(backup_file))
|
44
|
+
Doing.logger.warn('File update:', "restored from #{result}")
|
45
|
+
Doing.logger.benchmark(:restore_backup, :finish)
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
## Undo last undo
|
50
|
+
##
|
51
|
+
## @param filename The filename
|
52
|
+
##
|
53
|
+
def redo_backup(filename = nil, count: 1)
|
54
|
+
filename ||= Doing.config.settings['doing_file']
|
55
|
+
# redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
|
56
|
+
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
|
57
|
+
total = undones.count
|
58
|
+
count = total if count > total
|
59
|
+
|
60
|
+
skipped = undones.slice!(0, count)
|
61
|
+
undone = skipped.pop
|
62
|
+
|
63
|
+
raise DoingRuntimeError, 'End of redo history' if undone.nil?
|
64
|
+
|
65
|
+
redo_file = File.join(backup_dir, undone)
|
66
|
+
|
67
|
+
FileUtils.move(redo_file, filename)
|
68
|
+
|
69
|
+
skipped.each do |f|
|
70
|
+
FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
|
71
|
+
end
|
72
|
+
|
73
|
+
Doing.logger.warn('File update:', "restored undo step #{count}/#{total}")
|
74
|
+
Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
|
75
|
+
end
|
76
|
+
|
77
|
+
def clear_undone(filename = nil)
|
78
|
+
filename ||= Doing.config.settings['doing_file']
|
79
|
+
# redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
|
80
|
+
Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
|
81
|
+
FileUtils.rm(File.join(backup_dir, f))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
## Select from recent undos. If a filename is
|
87
|
+
## provided, only backups of that filename will be used.
|
88
|
+
##
|
89
|
+
## @param filename The filename to restore
|
90
|
+
##
|
91
|
+
def select_redo(filename = nil)
|
92
|
+
filename ||= Doing.config.settings['doing_file']
|
93
|
+
|
94
|
+
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
|
95
|
+
|
96
|
+
raise DoingRuntimeError, 'End of redo history' if undones.empty?
|
97
|
+
|
98
|
+
total = undones.count
|
99
|
+
options = undones.each_with_object([]) do |file, arr|
|
100
|
+
d, _base = date_of_backup(file)
|
101
|
+
next if d.nil?
|
102
|
+
|
103
|
+
arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
|
104
|
+
end
|
105
|
+
|
106
|
+
backup_file = show_menu(options, filename)
|
107
|
+
idx = undones.index(File.basename(backup_file))
|
108
|
+
skipped = undones.slice!(idx, undones.count - idx)
|
109
|
+
undone = skipped.shift
|
110
|
+
|
111
|
+
redo_file = File.join(backup_dir, undone)
|
112
|
+
|
113
|
+
FileUtils.move(redo_file, filename)
|
114
|
+
|
115
|
+
skipped.each do |f|
|
116
|
+
FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
|
117
|
+
end
|
118
|
+
|
119
|
+
Doing.logger.warn('File update:', "restored undo step #{idx}/#{total}")
|
120
|
+
Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
## Select from recent backups. If a filename is
|
125
|
+
## provided, only backups of that filename will be used.
|
126
|
+
##
|
127
|
+
## @param filename The filename to restore
|
128
|
+
##
|
129
|
+
def select_backup(filename = nil)
|
130
|
+
filename ||= Doing.config.settings['doing_file']
|
131
|
+
|
132
|
+
options = get_backups(filename).each_with_object([]) do |file, arr|
|
133
|
+
d, _base = date_of_backup(file)
|
134
|
+
next if d.nil?
|
135
|
+
arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
|
136
|
+
end
|
137
|
+
|
138
|
+
backup_file = show_menu(options, filename)
|
139
|
+
write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
|
140
|
+
FileUtils.mv(backup_file, filename)
|
141
|
+
prune_backups_after(File.basename(backup_file))
|
142
|
+
Doing.logger.warn('File update:', "restored from #{backup_file}")
|
143
|
+
end
|
144
|
+
|
145
|
+
def show_menu(options, filename)
|
146
|
+
if TTY::Which.which('colordiff')
|
147
|
+
preview = 'colordiff -U 1'
|
148
|
+
pipe = '| awk "(NR>2)"'
|
149
|
+
elsif TTY::Which.which('git')
|
150
|
+
preview = 'git --no-pager diff -U1 --color=always --minimal --word-diff'
|
151
|
+
pipe = ' | awk "(NR>4)"'
|
152
|
+
else
|
153
|
+
preview = 'diff -U 1'
|
154
|
+
pipe = if TTY::Which.which('delta')
|
155
|
+
' | delta --no-gitconfig --syntax-theme=1337'
|
156
|
+
elsif TTY::Which.which('diff-so-fancy')
|
157
|
+
' | diff-so-fancy'
|
158
|
+
elsif TTY::Which.which('ydiff')
|
159
|
+
' | ydiff -c always --wrap < /dev/tty'
|
160
|
+
else
|
161
|
+
cmd = 'sed -e "s/^-/`echo -e "\033[31m"`-/;s/^+/`echo -e "\033[32m"`+/;s/^@/`echo -e "\033[34m"`@/;s/\$/`echo -e "\033[0m"`/"'
|
162
|
+
"| bash -c #{Shellwords.escape(cmd)}"
|
163
|
+
end
|
164
|
+
pipe += ' | awk "(NR>2)"'
|
165
|
+
end
|
166
|
+
|
167
|
+
result = Doing::Prompt.choose_from(options,
|
168
|
+
prompt: 'Select a backup to restore',
|
169
|
+
sorted: false,
|
170
|
+
fzf_args: [
|
171
|
+
'--delimiter="\t"',
|
172
|
+
'--with-nth=1',
|
173
|
+
%(--preview='#{preview} "#{filename}" {2} #{pipe}'),
|
174
|
+
'--disabled',
|
175
|
+
'--height=10',
|
176
|
+
'--preview-window="right,70%,nowrap,follow"',
|
177
|
+
'--header="Select a revision to restore"'
|
178
|
+
])
|
179
|
+
raise UserCancelled unless result
|
180
|
+
|
181
|
+
result.strip.split(/\t/).last
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
## Writes a copy of the content to a dated backup file
|
186
|
+
## in a hidden directory
|
187
|
+
##
|
188
|
+
## @param content The data to back up
|
189
|
+
##
|
190
|
+
def write_backup(filename = nil)
|
191
|
+
Doing.logger.benchmark(:_write_backup, :start)
|
192
|
+
filename ||= Doing.config.settings['doing_file']
|
193
|
+
|
194
|
+
unless File.exist?(filename)
|
195
|
+
Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
199
|
+
backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
|
200
|
+
# compressed = Zlib::Deflate.deflate(content)
|
201
|
+
# Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
|
202
|
+
# gz.write(IO.read(filename))
|
203
|
+
# end
|
204
|
+
|
205
|
+
FileUtils.cp(filename, backup_file)
|
206
|
+
|
207
|
+
prune_backups(filename, 15)
|
208
|
+
clear_undone(filename)
|
209
|
+
Doing.logger.benchmark(:_write_backup, :finish)
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def timestamp_filename
|
215
|
+
Time.now.strftime('%Y-%m-%d_%H.%M.%S')
|
216
|
+
end
|
217
|
+
|
218
|
+
def get_backups(filename = nil)
|
219
|
+
filename ||= Doing.config.settings['doing_file']
|
220
|
+
backups = Dir.glob("*___#{File.basename(filename)}", base: backup_dir).sort.reverse
|
221
|
+
backups.delete_if { |f| f =~ /^undone/ }
|
222
|
+
end
|
223
|
+
|
224
|
+
def save_undone(filename = nil)
|
225
|
+
filename ||= Doing.config.settings['doing_file']
|
226
|
+
undone_file = File.join(backup_dir, "undone#{timestamp_filename}___#{File.basename(filename)}")
|
227
|
+
FileUtils.cp(filename, undone_file)
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
## Retrieve date from backup filename
|
232
|
+
##
|
233
|
+
## @param filename The filename
|
234
|
+
##
|
235
|
+
def date_of_backup(filename)
|
236
|
+
m = filename.match(/^(?:undone)?(?<date>\d{4}-\d{2}-\d{2})_(?<time>\d{2}\.\d{2}\.\d{2})___(?<file>.*?)$/)
|
237
|
+
return nil if m.nil?
|
238
|
+
|
239
|
+
[Time.parse("#{m['date']} #{m['time'].gsub(/\./, ':')}"), m['file']]
|
240
|
+
end
|
241
|
+
|
242
|
+
##
|
243
|
+
## Return a location for storing backups, creating if needed
|
244
|
+
##
|
245
|
+
## @return Path to backup directory
|
246
|
+
##
|
247
|
+
def backup_dir
|
248
|
+
@backup_dir ||= create_backup_dir
|
249
|
+
end
|
250
|
+
|
251
|
+
def create_backup_dir
|
252
|
+
dir = File.expand_path(Doing.config.settings['backup_dir']) || File.join(user_home, '.doing_backup')
|
253
|
+
if File.exist?(dir) && !File.directory?(dir)
|
254
|
+
raise DoingRuntimeError, "Backup error: #{dir} is not a directory"
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
unless File.exist?(dir)
|
259
|
+
FileUtils.mkdir_p(dir)
|
260
|
+
Doing.logger.warn('Backup:', "backup directory created at #{dir}")
|
261
|
+
end
|
262
|
+
|
263
|
+
dir
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
## Delete backups newer than selected filename
|
268
|
+
##
|
269
|
+
## @param filename The filename
|
270
|
+
##
|
271
|
+
def prune_backups_after(filename)
|
272
|
+
target_date, base = date_of_backup(filename)
|
273
|
+
return if target_date.nil?
|
274
|
+
|
275
|
+
counter = 0
|
276
|
+
get_backups(base).each do |file|
|
277
|
+
date, _base = date_of_backup(file)
|
278
|
+
if date && target_date < date
|
279
|
+
FileUtils.mv(File.join(backup_dir, file), File.join(backup_dir, "undone#{file}"))
|
280
|
+
counter += 1
|
281
|
+
end
|
282
|
+
end
|
283
|
+
Doing.logger.debug('Backup:', "deleted #{counter} files newer than restored backup")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
data/lib/doing/version.rb
CHANGED
data/lib/doing/wwid.rb
CHANGED
@@ -183,7 +183,25 @@ module Doing
|
|
183
183
|
|
184
184
|
date = nil
|
185
185
|
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
186
|
-
|
186
|
+
watch_tags = [
|
187
|
+
'start(?:ed)?',
|
188
|
+
'beg[ia]n',
|
189
|
+
'done',
|
190
|
+
'finished',
|
191
|
+
'completed?',
|
192
|
+
'waiting',
|
193
|
+
'defer(?:red)?'
|
194
|
+
]
|
195
|
+
if @config['date_tags']
|
196
|
+
date_tags = @config['date_tags']
|
197
|
+
date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
|
198
|
+
date_tags.map! do |tag|
|
199
|
+
tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
|
200
|
+
end
|
201
|
+
watch_tags.concat(date_tags).uniq!
|
202
|
+
end
|
203
|
+
|
204
|
+
done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
|
187
205
|
date_rx = /^(?:\s*- )?(?<date>.*?) \| (?=\S)/
|
188
206
|
|
189
207
|
title.gsub!(done_rx) do
|
@@ -244,7 +262,7 @@ module Doing
|
|
244
262
|
return frag.cap_first if @content.section?(frag)
|
245
263
|
|
246
264
|
section = nil
|
247
|
-
re = frag.
|
265
|
+
re = frag.to_rx(distance: 2, case_type: :ignore)
|
248
266
|
sections.each do |sect|
|
249
267
|
next unless sect =~ /#{re}/i
|
250
268
|
|
@@ -286,7 +304,7 @@ module Doing
|
|
286
304
|
def guess_view(frag, guessed: false, suggest: false)
|
287
305
|
views.each { |view| return view if frag.downcase == view.downcase }
|
288
306
|
view = false
|
289
|
-
re = frag.
|
307
|
+
re = frag.to_rx(distance: 2, case_type: :ignore)
|
290
308
|
views.each do |v|
|
291
309
|
next unless v =~ /#{re}/i
|
292
310
|
|
@@ -423,8 +441,9 @@ module Doing
|
|
423
441
|
# @param item [Item] the item to reset/resume
|
424
442
|
# @param resume [Boolean] removing @done tag if true
|
425
443
|
#
|
426
|
-
def reset_item(item, resume: false)
|
427
|
-
|
444
|
+
def reset_item(item, date: nil, resume: false)
|
445
|
+
date ||= Time.now
|
446
|
+
item.date = date
|
428
447
|
item.tag('done', remove: true) if resume
|
429
448
|
logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
|
430
449
|
item
|
@@ -528,10 +547,25 @@ module Doing
|
|
528
547
|
last_entry
|
529
548
|
end
|
530
549
|
|
531
|
-
def all_tags(items, opt: {})
|
532
|
-
|
533
|
-
|
534
|
-
|
550
|
+
def all_tags(items, opt: {}, counts: false)
|
551
|
+
if counts
|
552
|
+
all_tags = {}
|
553
|
+
items.each do |item|
|
554
|
+
item.tags.each do |tag|
|
555
|
+
if all_tags.key?(tag.downcase)
|
556
|
+
all_tags[tag.downcase] += 1
|
557
|
+
else
|
558
|
+
all_tags[tag.downcase] = 1
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
all_tags.sort_by { |tag, count| count }
|
564
|
+
else
|
565
|
+
all_tags = []
|
566
|
+
items.each { |item| all_tags.concat(item.tags.map(&:downcase)).uniq! }
|
567
|
+
all_tags.sort
|
568
|
+
end
|
535
569
|
end
|
536
570
|
|
537
571
|
def tag_groups(items, opt: {})
|
@@ -656,6 +690,7 @@ module Doing
|
|
656
690
|
end
|
657
691
|
|
658
692
|
if keep && opt[:tag]
|
693
|
+
opt[:tag_bool] = opt[:bool].normalize_bool if opt[:bool]
|
659
694
|
opt[:tag_bool] ||= :and
|
660
695
|
tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.tags?(opt[:tag], opt[:tag_bool])
|
661
696
|
keep = false unless tag_match
|
@@ -666,7 +701,7 @@ module Doing
|
|
666
701
|
search_match = if opt[:search].nil? || opt[:search].empty?
|
667
702
|
true
|
668
703
|
else
|
669
|
-
item.search(opt[:search], case_type: opt[:case].normalize_case
|
704
|
+
item.search(opt[:search], case_type: opt[:case].normalize_case)
|
670
705
|
end
|
671
706
|
|
672
707
|
keep = false unless search_match
|
@@ -708,7 +743,7 @@ module Doing
|
|
708
743
|
|
709
744
|
keep = false if keep && opt[:only_timed] && !item.interval
|
710
745
|
|
711
|
-
if keep && opt[:tag_filter]
|
746
|
+
if keep && opt[:tag_filter]
|
712
747
|
keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
|
713
748
|
keep = opt[:not] ? !keep : keep
|
714
749
|
end
|
@@ -745,7 +780,7 @@ module Doing
|
|
745
780
|
|
746
781
|
keep
|
747
782
|
end
|
748
|
-
count = opt[:count]&.positive? ? opt[:count] : filtered_items.
|
783
|
+
count = opt[:count].to_i&.positive? ? opt[:count].to_i : filtered_items.count
|
749
784
|
|
750
785
|
output = Items.new
|
751
786
|
|
@@ -914,12 +949,19 @@ module Doing
|
|
914
949
|
if opt[:resume] && !opt[:reset]
|
915
950
|
repeat_item(item, { editor: opt[:editor] })
|
916
951
|
elsif opt[:reset]
|
952
|
+
res = Prompt.enter_text('Start date (blank for current time)', default_response: '')
|
953
|
+
if res =~ /^ *$/
|
954
|
+
date = Time.now
|
955
|
+
else
|
956
|
+
date = res.chronify(guess: :begin)
|
957
|
+
end
|
958
|
+
|
917
959
|
res = if item.tags?('done', :and) && !opt[:resume]
|
918
960
|
opt[:force] ? true : Prompt.yn('Remove @done tag?', default_response: 'y')
|
919
961
|
else
|
920
962
|
opt[:resume]
|
921
963
|
end
|
922
|
-
@content.update_item(item, reset_item(item, resume: res))
|
964
|
+
@content.update_item(item, reset_item(item, date: date, resume: res))
|
923
965
|
end
|
924
966
|
write(@doing_file)
|
925
967
|
|
@@ -1309,20 +1351,6 @@ module Doing
|
|
1309
1351
|
end
|
1310
1352
|
end
|
1311
1353
|
|
1312
|
-
##
|
1313
|
-
## Restore a backed up version of a file
|
1314
|
-
##
|
1315
|
-
## @param file [String] The filepath to restore
|
1316
|
-
##
|
1317
|
-
def restore_backup(file)
|
1318
|
-
if File.exist?("#{file}~")
|
1319
|
-
FileUtils.cp("#{file}~", file)
|
1320
|
-
logger.warn('File update:', "Restored #{file.sub(/^#{Util.user_home}/, '~')}")
|
1321
|
-
else
|
1322
|
-
logger.error('Restore error:', 'No backup file found')
|
1323
|
-
end
|
1324
|
-
end
|
1325
|
-
|
1326
1354
|
##
|
1327
1355
|
## Rename doing file with date and start fresh one
|
1328
1356
|
##
|
@@ -1387,8 +1415,35 @@ module Doing
|
|
1387
1415
|
##
|
1388
1416
|
## @return [String] The selected section name
|
1389
1417
|
##
|
1390
|
-
def choose_section
|
1391
|
-
|
1418
|
+
def choose_section(include_all: false)
|
1419
|
+
options = @content.section_titles.sort
|
1420
|
+
options.unshift('All') if include_all
|
1421
|
+
choice = Prompt.choose_from(options, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
|
1422
|
+
choice ? choice.strip : choice
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
##
|
1426
|
+
## Generate a menu of tags and allow user selection
|
1427
|
+
##
|
1428
|
+
## @return [String] The selected tag name
|
1429
|
+
##
|
1430
|
+
def choose_tag(section = 'All', items: nil, include_all: false)
|
1431
|
+
items ||= @content.in_section(section)
|
1432
|
+
tags = all_tags(items, counts: true).map { |t, c| "@#{t} (#{c})" }
|
1433
|
+
tags.unshift('No tag filter') if include_all
|
1434
|
+
choice = Prompt.choose_from(tags, sorted: false, multiple: true, prompt: 'Choose tag(s) > ', fzf_args: ['--height=60%'])
|
1435
|
+
choice ? choice.split(/\n/).map { |t| t.strip.sub(/ \(.*?\)$/, '')}.join(' ') : choice
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
##
|
1439
|
+
## Generate a menu of sections and tags and allow user selection
|
1440
|
+
##
|
1441
|
+
## @return [String] The selected section or tag name
|
1442
|
+
##
|
1443
|
+
def choose_section_tag
|
1444
|
+
options = @content.section_titles.sort
|
1445
|
+
options.concat(@content.all_tags.sort.map { |t| "@#{t}" })
|
1446
|
+
choice = Prompt.choose_from(options, prompt: 'Choose a section or tag > ', fzf_args: ['--height=60%'])
|
1392
1447
|
choice ? choice.strip : choice
|
1393
1448
|
end
|
1394
1449
|
|
@@ -1427,17 +1482,25 @@ module Doing
|
|
1427
1482
|
##
|
1428
1483
|
## @param opt [Hash] Additional Options
|
1429
1484
|
##
|
1430
|
-
def list_section(opt = {})
|
1485
|
+
def list_section(opt = {}, items: Items.new)
|
1431
1486
|
opt[:config_template] ||= 'default'
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1487
|
+
|
1488
|
+
tpl_cfg = @config.dig('templates', opt[:config_template])
|
1489
|
+
|
1490
|
+
cfg = if opt[:view_template]
|
1491
|
+
@config.dig('views', opt[:view_template]).deep_merge(tpl_cfg)
|
1492
|
+
else
|
1493
|
+
tpl_cfg
|
1494
|
+
end
|
1495
|
+
|
1496
|
+
cfg.deep_merge({
|
1497
|
+
'wrap_width' => @config['wrap_width'] || 0,
|
1498
|
+
'date_format' => @config['default_date_format'],
|
1499
|
+
'order' => @config['order'] || 'asc',
|
1500
|
+
'tags_color' => @config['tags_color'],
|
1501
|
+
'duration' => @config['duration'],
|
1502
|
+
'interval_format' => @config['interval_format']
|
1503
|
+
})
|
1441
1504
|
opt[:duration] ||= cfg['duration'] || false
|
1442
1505
|
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
1443
1506
|
opt[:count] ||= 0
|
@@ -1468,9 +1531,9 @@ module Doing
|
|
1468
1531
|
end
|
1469
1532
|
end
|
1470
1533
|
|
1471
|
-
items = filter_items(
|
1534
|
+
items = filter_items(items, opt: opt)
|
1472
1535
|
|
1473
|
-
items.reverse!
|
1536
|
+
items.reverse! unless opt[:order] =~ /^d/i
|
1474
1537
|
|
1475
1538
|
if opt[:interactive]
|
1476
1539
|
opt[:menu] = !opt[:force]
|
@@ -1869,6 +1932,7 @@ EOS
|
|
1869
1932
|
output + tail
|
1870
1933
|
when :markdown
|
1871
1934
|
pad = sorted_tags_data.map {|k, v| k }.group_by(&:size).max.last[0].length
|
1935
|
+
pad = 7 if pad < 7
|
1872
1936
|
output = <<~EOS
|
1873
1937
|
| #{' ' * (pad - 7) }project | time |
|
1874
1938
|
| #{'-' * (pad - 1)}: | :------- |
|
data/lib/doing.rb
CHANGED
@@ -6,9 +6,14 @@ require 'yaml'
|
|
6
6
|
require 'pp'
|
7
7
|
require 'csv'
|
8
8
|
require 'tempfile'
|
9
|
+
require 'zlib'
|
10
|
+
require 'base64'
|
11
|
+
|
9
12
|
require 'chronic'
|
10
13
|
require 'tty-link'
|
11
14
|
require 'tty-which'
|
15
|
+
require 'tty-markdown'
|
16
|
+
require 'plist'
|
12
17
|
# require 'amatch'
|
13
18
|
require 'haml'
|
14
19
|
require 'json'
|
@@ -16,12 +21,14 @@ require 'logger'
|
|
16
21
|
require 'safe_yaml/load'
|
17
22
|
require 'doing/hash'
|
18
23
|
require 'doing/colors'
|
24
|
+
require 'doing/template_string'
|
19
25
|
require 'doing/string'
|
20
26
|
require 'doing/string_chronify'
|
21
27
|
require 'doing/time'
|
22
28
|
require 'doing/array'
|
23
29
|
require 'doing/symbol'
|
24
30
|
require 'doing/util'
|
31
|
+
require 'doing/util_backup'
|
25
32
|
require 'doing/configuration'
|
26
33
|
require 'doing/section'
|
27
34
|
require 'doing/items'
|
@@ -35,6 +42,8 @@ require 'doing/hooks'
|
|
35
42
|
require 'doing/plugin_manager'
|
36
43
|
require 'doing/pager'
|
37
44
|
require 'doing/completion'
|
45
|
+
require 'doing/boolean_term_parser'
|
46
|
+
require 'doing/phrase_parser'
|
38
47
|
# require 'doing/markdown_document_listener'
|
39
48
|
|
40
49
|
# Main doing module
|
@@ -11,9 +11,9 @@ module Doing
|
|
11
11
|
{
|
12
12
|
trigger: 'wiki',
|
13
13
|
templates: [
|
14
|
-
{ name: 'wiki_page', trigger: 'wiki.?page' },
|
15
|
-
{ name: 'wiki_index', trigger: 'wiki.?index' },
|
16
|
-
{ name: 'wiki_css', trigger: 'wiki.?css' }
|
14
|
+
{ name: 'wiki_page', trigger: 'wiki.?page', format: 'haml', filename: 'wiki.haml' },
|
15
|
+
{ name: 'wiki_index', trigger: 'wiki.?index', format: 'haml', filename: 'wiki_index.haml' },
|
16
|
+
{ name: 'wiki_css', trigger: 'wiki.?css', format: 'css', filename: 'wiki.css' }
|
17
17
|
]
|
18
18
|
}
|
19
19
|
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<% @items.each do |i| %>Doing on <%= i[:date_object].strftime('%A %m/%d/%y') %>
|
2
|
+
|
3
|
+
<%= i[:title] %><% if i[:note].length.positive? %><%= "\n\n" + i[:note].map{|n| n.strip }.join("\n ") %><% end %>
|
4
|
+
|
5
|
+
<% if i[:human_time] && i[:time] != "00:00:00" %>_Took <%= i[:human_time] %>._<% end %>
|
6
|
+
<% end %>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# <%= @page_title %>
|
2
|
+
<% @items.each do |i| %>
|
3
|
+
- [<%= i[:done] %>] <%= i[:date] %> <%= i[:title] %> <% if i[:time] && i[:time] != "00:00:00" %>[**<%= i[:time] %>**]<% end %><% if i[:note].length.positive? %><%= "\n\n " + i[:note].map{|n| n.strip }.join("\n ") %><% end %><% end %>
|
4
|
+
|
5
|
+
<%= @totals %>
|