doing 2.1.2pre → 2.1.6pre

Sign up to get free protection for your applications and to get access to all the features.
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 %>