doing 2.1.2pre → 2.1.6pre
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 +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 %>
|