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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +62 -14
  7. data/Gemfile.lock +25 -1
  8. data/README.md +5 -1
  9. data/Rakefile +2 -0
  10. data/bin/doing +429 -142
  11. data/docs/_config.yml +1 -0
  12. data/{doc → docs/doc}/Array.html +63 -1
  13. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  14. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  15. data/docs/doc/BooleanTermParser/Query.html +417 -0
  16. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  17. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  18. data/docs/doc/BooleanTermParser.html +115 -0
  19. data/docs/doc/Doing/CLIFormat.html +131 -0
  20. data/{doc → docs/doc}/Doing/Color.html +2 -2
  21. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  22. data/{doc → docs/doc}/Doing/Configuration.html +163 -69
  23. data/{doc → docs/doc}/Doing/Content.html +0 -0
  24. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  32. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  33. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  34. data/{doc → docs/doc}/Doing/Item.html +135 -89
  35. data/{doc → docs/doc}/Doing/Items.html +36 -2
  36. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  37. data/{doc → docs/doc}/Doing/Note.html +5 -134
  38. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  39. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  40. data/{doc → docs/doc}/Doing/Prompt.html +70 -18
  41. data/{doc → docs/doc}/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +713 -0
  43. data/docs/doc/Doing/Util/Backup.html +686 -0
  44. data/{doc → docs/doc}/Doing/Util.html +16 -4
  45. data/{doc → docs/doc}/Doing/WWID.html +133 -73
  46. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  47. data/{doc → docs/doc}/Doing.html +4 -4
  48. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  49. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  50. data/{doc → docs/doc}/GLI.html +1 -1
  51. data/{doc → docs/doc}/Hash.html +1 -1
  52. data/docs/doc/PhraseParser/Operator.html +172 -0
  53. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  54. data/docs/doc/PhraseParser/Query.html +495 -0
  55. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  56. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  57. data/docs/doc/PhraseParser/TermClause.html +293 -0
  58. data/docs/doc/PhraseParser.html +115 -0
  59. data/{doc → docs/doc}/Status.html +1 -1
  60. data/{doc → docs/doc}/String.html +319 -13
  61. data/{doc → docs/doc}/Symbol.html +35 -1
  62. data/{doc → docs/doc}/Time.html +70 -2
  63. data/{doc → docs/doc}/_index.html +132 -4
  64. data/docs/doc/class_list.html +51 -0
  65. data/{doc → docs/doc}/css/common.css +0 -0
  66. data/{doc → docs/doc}/css/full_list.css +0 -0
  67. data/{doc → docs/doc}/css/style.css +0 -0
  68. data/{doc → docs/doc}/file.README.html +6 -2
  69. data/{doc → docs/doc}/file_list.html +0 -0
  70. data/{doc → docs/doc}/frames.html +0 -0
  71. data/{doc → docs/doc}/index.html +6 -2
  72. data/{doc → docs/doc}/js/app.js +0 -0
  73. data/{doc → docs/doc}/js/full_list.js +0 -0
  74. data/{doc → docs/doc}/js/jquery.js +0 -0
  75. data/{doc → docs/doc}/method_list.html +684 -196
  76. data/{doc → docs/doc}/top-level-namespace.html +2 -2
  77. data/docs/index.md +60 -0
  78. data/doing.gemspec +3 -0
  79. data/doing.rdoc +222 -74
  80. data/example_plugin.rb +3 -1
  81. data/lib/completion/_doing.zsh +53 -41
  82. data/lib/completion/doing.bash +17 -6
  83. data/lib/completion/doing.fish +321 -2
  84. data/lib/doing/array.rb +9 -0
  85. data/lib/doing/boolean_term_parser.rb +86 -0
  86. data/lib/doing/completion/fish_completion.rb +46 -3
  87. data/lib/doing/completion/zsh_completion.rb +1 -1
  88. data/lib/doing/configuration.rb +48 -21
  89. data/lib/doing/item.rb +105 -10
  90. data/lib/doing/items.rb +6 -0
  91. data/lib/doing/log_adapter.rb +28 -0
  92. data/lib/doing/note.rb +31 -30
  93. data/lib/doing/phrase_parser.rb +124 -0
  94. data/lib/doing/plugin_manager.rb +84 -21
  95. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  96. data/lib/doing/plugins/export/html_export.rb +2 -2
  97. data/lib/doing/plugins/export/json_export.rb +1 -0
  98. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  99. data/lib/doing/plugins/export/template_export.rb +94 -86
  100. data/lib/doing/prompt.rb +26 -15
  101. data/lib/doing/string.rb +114 -29
  102. data/lib/doing/string_chronify.rb +5 -1
  103. data/lib/doing/symbol.rb +4 -0
  104. data/lib/doing/template_string.rb +197 -0
  105. data/lib/doing/time.rb +32 -0
  106. data/lib/doing/util.rb +6 -7
  107. data/lib/doing/util_backup.rb +287 -0
  108. data/lib/doing/version.rb +1 -1
  109. data/lib/doing/wwid.rb +105 -41
  110. data/lib/doing.rb +9 -0
  111. data/lib/examples/plugins/say_export.rb +1 -1
  112. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  113. data/lib/templates/doing-dayone-entry.erb +6 -0
  114. data/lib/templates/doing-dayone.erb +5 -0
  115. metadata +136 -51
  116. 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
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.2pre'
2
+ VERSION = '2.1.6pre'
3
3
  end
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
- done_rx = /(?<=^| )@(?<tag>done|finished|completed?)\((?<date>.*?)\)/i
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.split('').join('.*?')
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.split('').join('.*?')
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
- item.date = Time.now
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
- all_tags = []
533
- items.each { |item| all_tags.concat(item.tags).uniq! }
534
- all_tags.sort
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, fuzzy: opt[:fuzzy])
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] && !opt[:tag_filter]['tags'].empty?
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.length
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
- choice = Prompt.choose_from(@content.section_titles.sort, prompt: 'Choose a section > ', fzf_args: ['--height=60%'])
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
- cfg = @config.dig('templates',
1433
- opt[:config_template]).deep_merge({
1434
- 'wrap_width' => @config['wrap_width'] || 0,
1435
- 'date_format' => @config['default_date_format'],
1436
- 'order' => @config['order'] || 'asc',
1437
- 'tags_color' => @config['tags_color'],
1438
- 'duration' => @config['duration'],
1439
- 'interval_format' => @config['interval_format']
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(Items.new, opt: opt).reverse
1534
+ items = filter_items(items, opt: opt)
1472
1535
 
1473
- items.reverse! if opt[:order] =~ /^d/i
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
@@ -73,7 +73,7 @@ module Doing
73
73
  {
74
74
  trigger: 'say(?:it)?',
75
75
  templates: [
76
- { name: 'say', trigger: 'say(?:it)?' }
76
+ { name: 'say', trigger: 'say(?:it)?', format: 'text', filename: 'say.txt' }
77
77
  ],
78
78
  config: {
79
79
  'say_voice' => 'Fiona'
@@ -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 %>