doing 2.1.41 → 2.1.42

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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/again.rb +1 -3
  6. data/bin/commands/changes.rb +50 -34
  7. data/bin/commands/commands.rb +77 -52
  8. data/bin/commands/commands_accepting.rb +57 -53
  9. data/bin/commands/config.rb +2 -2
  10. data/bin/commands/finish.rb +94 -68
  11. data/bin/commands/flag.rb +5 -1
  12. data/bin/commands/now.rb +151 -107
  13. data/bin/commands/on.rb +7 -4
  14. data/bin/commands/undo.rb +4 -6
  15. data/docs/doc/Array.html +3 -2
  16. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  17. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  18. data/docs/doc/BooleanTermParser/Query.html +1 -1
  19. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  20. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  21. data/docs/doc/BooleanTermParser.html +1 -1
  22. data/docs/doc/Doing/ArrayNestedHash.html +1 -1
  23. data/docs/doc/Doing/ArrayTags.html +1 -1
  24. data/docs/doc/Doing/CSVExport.html +1 -1
  25. data/docs/doc/Doing/CalendarImport.html +1 -1
  26. data/docs/doc/Doing/Change.html +1 -1
  27. data/docs/doc/Doing/Changes.html +1 -1
  28. data/docs/doc/Doing/ChronifyArray.html +1 -1
  29. data/docs/doc/Doing/ChronifyNumeric.html +1 -1
  30. data/docs/doc/Doing/ChronifyString.html +1 -1
  31. data/docs/doc/Doing/Color.html +1 -1
  32. data/docs/doc/Doing/Completion/BashCompletions.html +1 -1
  33. data/docs/doc/Doing/Completion/FishCompletions.html +1 -1
  34. data/docs/doc/Doing/Completion/StringUtils.html +1 -1
  35. data/docs/doc/Doing/Completion/ZshCompletions.html +1 -1
  36. data/docs/doc/Doing/Completion.html +1 -1
  37. data/docs/doc/Doing/Configuration.html +3 -2
  38. data/docs/doc/Doing/DayOneRenderer.html +1 -1
  39. data/docs/doc/Doing/DayoneExport.html +1 -1
  40. data/docs/doc/Doing/DoingImport.html +1 -1
  41. data/docs/doc/Doing/Entry.html +1 -1
  42. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  43. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  44. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  45. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  46. data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
  47. data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
  48. data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
  49. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  50. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  51. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  52. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  53. data/docs/doc/Doing/Errors.html +1 -1
  54. data/docs/doc/Doing/HTMLExport.html +1 -1
  55. data/docs/doc/Doing/Hooks.html +1 -1
  56. data/docs/doc/Doing/Item.html +1 -1
  57. data/docs/doc/Doing/ItemDates.html +1 -1
  58. data/docs/doc/Doing/ItemQuery.html +1 -1
  59. data/docs/doc/Doing/ItemState.html +1 -1
  60. data/docs/doc/Doing/ItemTags.html +1 -1
  61. data/docs/doc/Doing/Items.html +2 -1
  62. data/docs/doc/Doing/JSONExport.html +1 -1
  63. data/docs/doc/Doing/Logger.html +1 -1
  64. data/docs/doc/Doing/MarkdownExport.html +1 -1
  65. data/docs/doc/Doing/Note.html +3 -2
  66. data/docs/doc/Doing/Pager.html +1 -1
  67. data/docs/doc/Doing/Plugins.html +181 -76
  68. data/docs/doc/Doing/Prompt.html +1 -1
  69. data/docs/doc/Doing/PromptChoose.html +1 -1
  70. data/docs/doc/Doing/PromptFZF.html +1 -1
  71. data/docs/doc/Doing/PromptInput.html +1 -1
  72. data/docs/doc/Doing/PromptSTD.html +1 -1
  73. data/docs/doc/Doing/PromptYN.html +1 -1
  74. data/docs/doc/Doing/Section.html +1 -1
  75. data/docs/doc/Doing/StringHighlight.html +1 -1
  76. data/docs/doc/Doing/StringNormalize.html +1 -1
  77. data/docs/doc/Doing/StringQuery.html +1 -1
  78. data/docs/doc/Doing/StringTags.html +1 -1
  79. data/docs/doc/Doing/StringTransform.html +35 -1
  80. data/docs/doc/Doing/StringTruncate.html +1 -1
  81. data/docs/doc/Doing/StringURL.html +1 -1
  82. data/docs/doc/Doing/SymbolNormalize.html +1 -1
  83. data/docs/doc/Doing/TaskPaperExport.html +1 -1
  84. data/docs/doc/Doing/TemplateExport.html +1 -1
  85. data/docs/doc/Doing/TemplateString.html +2 -2
  86. data/docs/doc/Doing/TimingImport.html +1 -1
  87. data/docs/doc/Doing/Types.html +1 -1
  88. data/docs/doc/Doing/Util/Backup.html +2 -156
  89. data/docs/doc/Doing/Util.html +66 -9
  90. data/docs/doc/Doing/Version.html +1 -1
  91. data/docs/doc/Doing/WWID.html +14 -1
  92. data/docs/doc/Doing.html +4 -4
  93. data/docs/doc/FalseClass.html +1 -1
  94. data/docs/doc/GLI/Commands/Help.html +1 -1
  95. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  96. data/docs/doc/GLI/Commands.html +1 -1
  97. data/docs/doc/GLI.html +1 -1
  98. data/docs/doc/Hash.html +1 -1
  99. data/docs/doc/Numeric.html +1 -1
  100. data/docs/doc/Object.html +1 -1
  101. data/docs/doc/PhraseParser/Operator.html +1 -1
  102. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  103. data/docs/doc/PhraseParser/Query.html +1 -1
  104. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  105. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  106. data/docs/doc/PhraseParser/TermClause.html +1 -1
  107. data/docs/doc/PhraseParser.html +1 -1
  108. data/docs/doc/Status.html +1 -1
  109. data/docs/doc/String.html +2 -2
  110. data/docs/doc/Symbol.html +1 -1
  111. data/docs/doc/Time.html +1 -1
  112. data/docs/doc/TrueClass.html +1 -1
  113. data/docs/doc/_index.html +16 -9
  114. data/docs/doc/class_list.html +1 -1
  115. data/docs/doc/file.README.html +2 -2
  116. data/docs/doc/index.html +2 -2
  117. data/docs/doc/method_list.html +289 -281
  118. data/docs/doc/top-level-namespace.html +9 -1
  119. data/doing.rdoc +7 -7
  120. data/lib/doing/add_options.rb +8 -0
  121. data/lib/doing/array/array.rb +2 -0
  122. data/lib/doing/array/cleanup.rb +31 -0
  123. data/lib/doing/configuration.rb +7 -3
  124. data/lib/doing/note.rb +1 -1
  125. data/lib/doing/pager.rb +9 -3
  126. data/lib/doing/plugin_manager.rb +30 -5
  127. data/lib/doing/prompt/choose.rb +1 -1
  128. data/lib/doing/prompt/input.rb +1 -1
  129. data/lib/doing/string/transform.rb +6 -0
  130. data/lib/doing/util.rb +12 -6
  131. data/lib/doing/util_backup.rb +55 -48
  132. data/lib/doing/version.rb +1 -1
  133. data/lib/doing/wwid/editor.rb +6 -3
  134. data/lib/doing/wwid/interactive.rb +10 -20
  135. data/lib/doing/wwid/modify.rb +2 -0
  136. data/lib/doing.rb +3 -3
  137. metadata +3 -2
@@ -150,12 +150,20 @@
150
150
  <div class="discussion">
151
151
  <p>Add presets of flags and switches to a command.</p>
152
152
 
153
+ <p>:output_template =&gt; --output, --config_template, --template</p>
154
+
153
155
  <p>:add_entry =&gt; --noauto, --note, --ask, --editor, --back</p>
154
156
 
157
+ <p>:finish_entry =&gt; --at/finished, --from, --took</p>
158
+
159
+ <p>:time_display =&gt; --times, --duration, --totals, --tag_sort, --tag_order, --only_timed</p>
160
+
155
161
  <p>:search =&gt; --search, --case, --exact</p>
156
162
 
157
163
  <p>:tag_filter =&gt; --tag, --bool, --not, --val</p>
158
164
 
165
+ <p>:time_filter =&gt; --before, --after, --from</p>
166
+
159
167
  <p>:date_filter =&gt; --before, --after, --from</p>
160
168
 
161
169
 
@@ -206,7 +214,7 @@
206
214
  </div>
207
215
 
208
216
  <div id="footer">
209
- Generated on Wed Mar 16 09:42:15 2022 by
217
+ Generated on Fri Mar 18 12:56:57 2022 by
210
218
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
211
219
  0.9.27 (ruby-3.0.1).
212
220
  </div>
data/doing.rdoc CHANGED
@@ -5,7 +5,7 @@ record of what you've been doing, complete with tag-based time tracking. The
5
5
  command line tool allows you to add entries, annotate with tags and notes, and
6
6
  view your entries with myriad options, with a focus on a "natural" language syntax.
7
7
 
8
- v2.1.41
8
+ v2.1.42
9
9
 
10
10
  === Global Options
11
11
  === --config_file arg
@@ -435,8 +435,8 @@ Display a formatted list of changes in recent versions.
435
435
  ===== -l|--lookup VERSION
436
436
 
437
437
  Look up a specific version. Specify versions as "MAJ.MIN.PATCH", MIN
438
- and PATCH are optional. Use > or < to see all changes since or prior
439
- to a version. Wildcards (*?) accepted unless using < or >.
438
+ and PATCH are optional. Use > or < to see all changes since or prior
439
+ to a version. Wildcards (*?) accepted unless using < or >.
440
440
 
441
441
  [Default Value] None
442
442
  [Must Match] (?-mix:^(?:(?:(?:[<>=]+|p(?:rior)|b(?:efore)|o(?:lder)|s(?:ince)|a(?:fter)|n(?:ewer))? *[0-9.*?]{1,10} *)+|(?:[\d.]+ *(?:-|to)+ *[0-9.]{1,10}))$)
@@ -445,7 +445,7 @@ Look up a specific version. Specify versions as "MAJ.MIN.PATCH", MIN
445
445
  ===== -s|--search arg
446
446
 
447
447
  Show changelogs matching search terms (uses pattern-based searching).
448
- Add slashes to search with regular expressions, e.g. `--search "/output.*flag/"`
448
+ Add slashes to search with regular expressions, e.g. `--search "/output.*flag/"`
449
449
 
450
450
  [Default Value] None
451
451
 
@@ -816,8 +816,8 @@ Archive entries
816
816
 
817
817
  ===== --auto
818
818
  Auto-generate finish dates from next entry's start time.
819
- Automatically generate completion dates 1 minute before next item (in any section) began.
820
- --auto overrides the --date and --back parameters.
819
+ Automatically generate completion dates 1 minute before next item (in any section) began.
820
+ --auto overrides the --date and --back parameters.
821
821
 
822
822
 
823
823
 
@@ -1482,7 +1482,7 @@ Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]
1482
1482
  ===== --from TIME_RANGE
1483
1483
 
1484
1484
  Set a start and optionally end time as a date range ("from 1pm to 2:30pm").
1485
- If an end time is provided, a dated @done tag will be added
1485
+ If an end time is provided, a dated @done tag will be added
1486
1486
 
1487
1487
  [Default Value] None
1488
1488
 
@@ -3,12 +3,20 @@
3
3
  ##
4
4
  ## Add presets of flags and switches to a command.
5
5
  ##
6
+ ## :output_template => --output, --config_template, --template
7
+ ##
6
8
  ## :add_entry => --noauto, --note, --ask, --editor, --back
7
9
  ##
10
+ ## :finish_entry => --at/finished, --from, --took
11
+ ##
12
+ ## :time_display => --times, --duration, --totals, --tag_sort, --tag_order, --only_timed
13
+ ##
8
14
  ## :search => --search, --case, --exact
9
15
  ##
10
16
  ## :tag_filter => --tag, --bool, --not, --val
11
17
  ##
18
+ ## :time_filter => --before, --after, --from
19
+ ##
12
20
  ## :date_filter => --before, --after, --from
13
21
  ##
14
22
  ## @param type [Symbol] The type
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative 'tags'
4
4
  require_relative 'nested_hash'
5
+ require_relative 'cleanup'
5
6
 
6
7
  module Doing
7
8
  class ::Array
8
9
  include ArrayTags
9
10
  include ArrayNestedHash
11
+ include ArrayCleanup
10
12
  ##
11
13
  ## Force UTF-8 encoding of strings in array
12
14
  ##
@@ -0,0 +1,31 @@
1
+ module Doing
2
+ module ArrayCleanup
3
+ ##
4
+ ## Like Array#compact -- removes nil items, but also
5
+ ## removes empty strings, zero or negative numbers and FalseClass items
6
+ ##
7
+ ## @return [Array] Array without "bad" elements
8
+ ##
9
+ def remove_bad
10
+ compact.map { |x| x.is_a?(String) ? x.strip : x }.select(&:good?)
11
+ end
12
+
13
+ def remove_bad!
14
+ replace remove_empty
15
+ end
16
+
17
+ ##
18
+ ## Like Array#compact -- removes nil items, but also
19
+ ## removes empty elements
20
+ ##
21
+ ## @return [Array] Array without empty elements
22
+ ##
23
+ def remove_empty
24
+ compact.map { |x| x.is_a?(String) ? x.strip : x }.reject { |x| x.is_a?(String) ? x.empty? : false }
25
+ end
26
+
27
+ def remove_empty!
28
+ replace remove_empty
29
+ end
30
+ end
31
+ end
@@ -23,7 +23,8 @@ module Doing
23
23
  'editors' => {
24
24
  'default' => ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR'],
25
25
  'doing_file' => nil,
26
- 'config' => nil
26
+ 'config' => nil,
27
+ 'pager' => nil
27
28
  },
28
29
  'plugins' => {
29
30
  'plugin_path' => File.join(Util.user_home, '.config', 'doing', 'plugins'),
@@ -204,6 +205,7 @@ module Doing
204
205
  real_path = []
205
206
  unless keypath =~ /^[.*]?$/
206
207
  paths = keypath.split(/[:.]/)
208
+ element_count = paths.count
207
209
  while paths.length.positive? && !cfg.nil?
208
210
  path = paths.shift
209
211
  new_cfg = nil
@@ -220,6 +222,8 @@ module Doing
220
222
  end
221
223
 
222
224
  if new_cfg.nil?
225
+ return real_path if real_path[-1] == path && real_path.count == element_count
226
+
223
227
  if distance < 5 && !create
224
228
  return resolve_key_path(keypath, create: false, distance: distance + 1)
225
229
  else
@@ -228,12 +232,12 @@ module Doing
228
232
 
229
233
  resolved = real_path.count.positive? ? "Resolved #{real_path.join('.')}, but " : ''
230
234
  Doing.logger.log_now(:warn, "#{resolved}#{path} is unknown")
231
- new_path = [*real_path, path, *paths].join('.')
235
+ new_path = [*real_path, path, *paths].compact.join('.')
232
236
  Doing.logger.log_now(:warn, "Continuing will create the path #{new_path}")
233
237
  res = Prompt.yn('Key path not found, create it?', default_response: true)
234
238
  raise InvalidArgument, 'Invalid key path' unless res
235
239
 
236
- real_path.push(path).concat(paths)
240
+ real_path.push(path).concat(paths).compact!
237
241
  Doing.logger.debug('Config:', "translated key path #{keypath} to #{real_path.join('.')}")
238
242
  return real_path
239
243
  end
data/lib/doing/note.rb CHANGED
@@ -20,7 +20,7 @@ module Doing
20
20
  ##
21
21
  ## Add note contents, optionally replacing existing note
22
22
  ##
23
- ## @param note [Array] The note to add, can be
23
+ ## @param note [Array|String|Note] The note to add, can be
24
24
  ## String, Array, or Note
25
25
  ## @param replace [Boolean] replace existing
26
26
  ## content
data/lib/doing/pager.rb CHANGED
@@ -70,14 +70,20 @@ module Doing
70
70
  end
71
71
 
72
72
  def pagers
73
- [ENV['GIT_PAGER'], ENV['PAGER'], git_pager,
74
- 'bat -p --pager="less -Xr"', 'less -Xr', 'more -r'].compact
73
+ [
74
+ Doing.setting('editors.pager'),
75
+ ENV['PAGER'],
76
+ 'less -Xr',
77
+ ENV['GIT_PAGER'],
78
+ git_pager,
79
+ 'more -r'
80
+ ].remove_bad
75
81
  end
76
82
 
77
83
  def find_executable(*commands)
78
84
  execs = commands.empty? ? pagers : commands
79
85
  execs
80
- .compact.map(&:strip).reject(&:empty?).uniq
86
+ .remove_bad.uniq
81
87
  .find { |cmd| TTY::Which.exist?(cmd.split.first) }
82
88
  end
83
89
 
@@ -4,10 +4,17 @@ module Doing
4
4
  # Plugin handling
5
5
  module Plugins
6
6
  class << self
7
+ # Return the user's home directory
7
8
  def user_home
8
9
  @user_home ||= Util.user_home
9
10
  end
10
11
 
12
+ # Storage for registered plugins. Hash with :import
13
+ # and :export keys containing hashes of available
14
+ # plugins.
15
+ #
16
+ # @return [Hash] registered plugins
17
+ #
11
18
  def plugins
12
19
  @plugins ||= {
13
20
  import: {},
@@ -81,6 +88,17 @@ module Doing
81
88
  Doing.logger.debug('Plugin Manager:', "Registered #{type} plugin \"#{title}\"")
82
89
  end
83
90
 
91
+ ##
92
+ ## Verifies that a plugin is properly configured with
93
+ ## necessary methods for its type. If the plugin fails
94
+ ## validation, a PluginUncallable exception will be
95
+ ## raised.
96
+ ##
97
+ ## @param title [String] The title
98
+ ## @param type [Symbol] type, :import or
99
+ ## :export
100
+ ## @param klass [Class] Plugin class
101
+ ##
84
102
  def validate_plugin(title, type, klass)
85
103
  type = valid_type(type)
86
104
  if type == :import && !klass.respond_to?(:import)
@@ -122,8 +140,10 @@ module Doing
122
140
  ##
123
141
  ## List available plugins to stdout
124
142
  ##
125
- ## @param options { type, separator }
143
+ ## @param options [Hash] additional options
126
144
  ##
145
+ ## @option options :column [Boolean] display results in a single column
146
+ ## @option options :type [String] Plugin type: all, import, or export
127
147
  def list_plugins(options = {})
128
148
  separator = options[:column] ? "\n" : "\t"
129
149
  type = options[:type].nil? || options[:type] =~ /all/i ? 'all' : valid_type(options[:type])
@@ -144,9 +164,9 @@ module Doing
144
164
  ##
145
165
  ## Return array of available plugin names
146
166
  ##
147
- ## @param type Plugin type (:import, :export)
167
+ ## @param type [Symbol] Plugin type (:import, :export)
148
168
  ##
149
- ## @return [Array<String>] plugin names
169
+ ## @return [Array] Array of plugin names (String)
150
170
  ##
151
171
  def available_plugins(type: :export)
152
172
  type = valid_type(type)
@@ -159,7 +179,7 @@ module Doing
159
179
  ## @param type Plugin type (:import, :export)
160
180
  ## @param separator The separator to join names with
161
181
  ##
162
- ## @return [String] Plugin names
182
+ ## @return [String] Plugin names joined with separator
163
183
  ##
164
184
  def plugin_names(type: :export, separator: '|')
165
185
  type = valid_type(type)
@@ -190,7 +210,7 @@ module Doing
190
210
  ## @param type [Symbol] Plugin type (:import,
191
211
  ## :export)
192
212
  ##
193
- ## @return [Array<String>] template names
213
+ ## @return [Array] Array of template names (String)
194
214
  ##
195
215
  def plugin_templates(type: :export)
196
216
  type = valid_type(type)
@@ -236,6 +256,9 @@ module Doing
236
256
  ## @param trigger [String] The trigger to test
237
257
  ## @param type [Symbol] the plugin type
238
258
  ## (:import, :export)
259
+ ## @param save_to [String] if a path is
260
+ ## specified, write the template
261
+ ## to that path. Nil for STDOUT
239
262
  ##
240
263
  ## @return [String] string content of template for trigger
241
264
  ##
@@ -255,6 +278,8 @@ module Doing
255
278
  raise Errors::InvalidArgument, "No template type matched \"#{trigger}\""
256
279
  end
257
280
 
281
+ private
282
+
258
283
  def save_template(tpl, dir, filename)
259
284
  dir = File.expand_path(dir)
260
285
  FileUtils.mkdir_p(dir) unless File.exist?(dir)
@@ -104,8 +104,8 @@ module Doing
104
104
 
105
105
  fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
106
106
  end
107
-
108
107
  res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
108
+
109
109
  selected = []
110
110
  res.split(/\n/).each do |item|
111
111
  idx = item.match(/^ *(\d+)\)/)[1].to_i
@@ -94,7 +94,7 @@ module Doing
94
94
  res << line.chomp
95
95
  end
96
96
  rescue Interrupt
97
- raise UserCancelled
97
+ return nil
98
98
  end
99
99
 
100
100
  res.join("\n").strip
@@ -164,5 +164,11 @@ module Doing
164
164
  end
165
165
  end
166
166
  end
167
+
168
+ def titlecase
169
+ tr('_', ' ').
170
+ gsub(/\s+/, ' ').
171
+ gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
172
+ end
167
173
  end
168
174
  end
data/lib/doing/util.rb CHANGED
@@ -47,13 +47,13 @@ module Doing
47
47
  end
48
48
  end
49
49
 
50
- # Non-destructive version of deep_merge_hashes! See that
51
- # method.
50
+ # Non-destructive version of deep_merge_hashes!
51
+ # @see {deep_merge_hashes!}
52
52
  #
53
53
  # @return the merged hashes.
54
54
  #
55
- # @param [Hash] master_hash The master hash
56
- # @param [Hash] other_hash The other hash
55
+ # @param master_hash [Hash] The master hash
56
+ # @param other_hash [Hash] The other hash
57
57
  #
58
58
  def deep_merge_hashes(master_hash, other_hash)
59
59
  deep_merge_hashes!(master_hash.clone, other_hash)
@@ -61,13 +61,19 @@ module Doing
61
61
 
62
62
  # Merges a master hash with another hash, recursively.
63
63
  #
64
- # master_hash - the "parent" hash whose values will be overridden
65
- # other_hash - the other hash whose values will be persisted after the merge
64
+ # @param target [Hash] the "parent" hash whose
65
+ # values will be overridden
66
+ # @param overwrite [Hash] the other hash whose
67
+ # values will be persisted after
68
+ # the merge
66
69
  #
67
70
  # This code was lovingly stolen from some random gem:
68
71
  # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
69
72
  #
70
73
  # Thanks to whoever made it.
74
+ #
75
+ # @return [Hash] merged hashes
76
+ #
71
77
  def deep_merge_hashes!(target, overwrite)
72
78
  merge_values(target, overwrite)
73
79
  merge_default_proc(target, overwrite)
@@ -24,19 +24,6 @@ module Doing
24
24
  clear_redo(filename)
25
25
  end
26
26
 
27
- ##
28
- ## Delete all redo files
29
- ##
30
- ## @param filename [String] The filename
31
- ##
32
- def clear_redo(filename)
33
- filename ||= Doing.setting('doing_file')
34
- backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
35
- backups.each do |file|
36
- FileUtils.rm(File.join(backup_dir, file))
37
- end
38
- end
39
-
40
27
  ##
41
28
  ## Retrieve the most recent backup
42
29
  ##
@@ -65,7 +52,8 @@ module Doing
65
52
  raise HistoryLimitError, 'End of undo history' if backup_file.nil?
66
53
 
67
54
  save_undone(filename)
68
- FileUtils.mv(backup_file, filename)
55
+ move_backup(backup_file, filename)
56
+
69
57
  prune_backups_after(File.basename(backup_file))
70
58
  Doing.logger.warn('File update:', "restored from #{backup_file}")
71
59
  Doing.logger.benchmark(:restore_backup, :finish)
@@ -78,7 +66,7 @@ module Doing
78
66
  ##
79
67
  def redo_backup(filename = nil, count: 1)
80
68
  filename ||= Doing.setting('doing_file')
81
- # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
69
+
82
70
  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
83
71
  total = undones.count
84
72
  count = total if count > total
@@ -90,7 +78,7 @@ module Doing
90
78
 
91
79
  redo_file = File.join(backup_dir, undone)
92
80
 
93
- FileUtils.move(redo_file, filename)
81
+ move_backup(redo_file, filename)
94
82
 
95
83
  skipped.each do |f|
96
84
  FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
@@ -100,14 +88,6 @@ module Doing
100
88
  Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
101
89
  end
102
90
 
103
- def clear_undone(filename = nil)
104
- filename ||= Doing.setting('doing_file')
105
- # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
106
- Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
107
- FileUtils.rm(File.join(backup_dir, f))
108
- end
109
- end
110
-
111
91
  ##
112
92
  ## Select from recent undos. If a filename is
113
93
  ## provided, only backups of that filename will be used.
@@ -136,7 +116,7 @@ module Doing
136
116
 
137
117
  redo_file = File.join(backup_dir, undone)
138
118
 
139
- FileUtils.move(redo_file, filename)
119
+ move_backup(redo_file, filename)
140
120
 
141
121
  skipped.each do |f|
142
122
  FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
@@ -165,11 +145,47 @@ module Doing
165
145
 
166
146
  backup_file = show_menu(options, filename)
167
147
  Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
168
- FileUtils.mv(backup_file, filename)
148
+ move_backup(backup_file, filename)
169
149
  prune_backups_after(File.basename(backup_file))
170
150
  Doing.logger.warn('File update:', "restored from #{backup_file}")
171
151
  end
172
152
 
153
+ ##
154
+ ## Writes a copy of the content to a dated backup file
155
+ ## in a hidden directory
156
+ ##
157
+ ## @param filename [String] The filename
158
+ ##
159
+ def write_backup(filename = nil)
160
+ Doing.logger.benchmark(:_write_backup, :start)
161
+ filename ||= Doing.setting('doing_file')
162
+
163
+ unless File.exist?(filename)
164
+ Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
165
+ return
166
+ end
167
+
168
+ backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
169
+ # compressed = Zlib::Deflate.deflate(content)
170
+ # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
171
+ # gz.write(IO.read(filename))
172
+ # end
173
+
174
+ FileUtils.cp(filename, backup_file)
175
+
176
+ prune_backups(filename, Doing.setting('history_size').to_i)
177
+ clear_undone(filename)
178
+ Doing.logger.benchmark(:_write_backup, :finish)
179
+ end
180
+
181
+ private
182
+
183
+ def move_backup(source, dest)
184
+ Hooks.trigger :pre_write, WWID.new, dest
185
+ FileUtils.mv(source, dest)
186
+ Hooks.trigger :post_write, dest
187
+ end
188
+
173
189
  def show_menu(options, filename)
174
190
  if TTY::Which.which('colordiff')
175
191
  preview = 'colordiff -U 1'
@@ -209,36 +225,27 @@ module Doing
209
225
  result.strip.split(/\t/).last
210
226
  end
211
227
 
228
+ def clear_undone(filename = nil)
229
+ filename ||= Doing.setting('doing_file')
230
+ # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
231
+ Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
232
+ FileUtils.rm(File.join(backup_dir, f))
233
+ end
234
+ end
235
+
212
236
  ##
213
- ## Writes a copy of the content to a dated backup file
214
- ## in a hidden directory
237
+ ## Delete all redo files
215
238
  ##
216
239
  ## @param filename [String] The filename
217
240
  ##
218
- def write_backup(filename = nil)
219
- Doing.logger.benchmark(:_write_backup, :start)
241
+ def clear_redo(filename)
220
242
  filename ||= Doing.setting('doing_file')
221
-
222
- unless File.exist?(filename)
223
- Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
224
- return
243
+ backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
244
+ backups.each do |file|
245
+ FileUtils.rm(File.join(backup_dir, file))
225
246
  end
226
-
227
- backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
228
- # compressed = Zlib::Deflate.deflate(content)
229
- # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
230
- # gz.write(IO.read(filename))
231
- # end
232
-
233
- FileUtils.cp(filename, backup_file)
234
-
235
- prune_backups(filename, Doing.setting('history_size').to_i)
236
- clear_undone(filename)
237
- Doing.logger.benchmark(:_write_backup, :finish)
238
247
  end
239
248
 
240
- private
241
-
242
249
  def timestamp_filename
243
250
  Time.now.strftime('%Y-%m-%d_%H.%M.%S')
244
251
  end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.41'
2
+ VERSION = '2.1.42'
3
3
  end
@@ -12,7 +12,7 @@ module Doing
12
12
 
13
13
  raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
14
14
 
15
- tmpfile = Tempfile.new(['doing', '.txt'])
15
+ tmpfile = Tempfile.new(['doing_temp', '.doing'])
16
16
 
17
17
  File.open(tmpfile.path, 'w+') do |f|
18
18
  f.puts input
@@ -134,14 +134,15 @@ module Doing
134
134
  end
135
135
  divider = "-----------"
136
136
  notice =<<~EONOTICE
137
+
137
138
  # - You may delete entries, but leave all divider lines (---) in place.
138
139
  # - Start and @done dates replaced with a time string (yesterday 3pm) will
139
140
  # be parsed automatically. Do not delete the pipe (|) between start date
140
141
  # and entry title.
141
142
  EONOTICE
142
- input = "#{editable_items.map(&:strip).join("\n#{divider}\n")}\n\n#{notice}"
143
+ input = "#{editable_items.map(&:strip).join("\n#{divider}\n")}\n"
143
144
 
144
- new_items = fork_editor(input).split(/^#{divider}/).map(&:strip)
145
+ new_items = fork_editor(input, message: notice).split(/^#{divider}/).map(&:strip)
145
146
 
146
147
  new_items.each_with_index do |new_item, i|
147
148
  input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
@@ -190,6 +191,8 @@ module Doing
190
191
  content = ["#{item.date.strftime('%F %R')} | #{item.title.dup}"]
191
192
  content << item.note.strip_lines.join("\n") unless item.note.empty?
192
193
  new_item = fork_editor(content.join("\n"))
194
+ raise UserCancelled, 'No change' if new_item.strip == content.join("\n").strip
195
+
193
196
  date, title, note = format_input(new_item)
194
197
  date ||= item.date
195
198
 
@@ -92,22 +92,21 @@ module Doing
92
92
 
93
93
  actions.concat(['resume/repeat', 'begin/reset']) if items.count == 1
94
94
 
95
- choice = Prompt.choose_from(actions,
95
+ choice = Prompt.choose_from(actions.map(&:titlecase),
96
96
  prompt: 'What do you want to do with the selected items? > ',
97
97
  multiple: true,
98
98
  sorted: false,
99
99
  fzf_args: ["--height=#{actions.count + 3}", '--tac', '--no-sort', '--info=hidden'])
100
100
  return unless choice
101
101
 
102
- to_do = choice.strip.split(/\n/)
102
+ to_do = choice.strip.split(/\n/).map(&:downcase)
103
+
103
104
  to_do.each do |action|
104
105
  case action
105
- when /resume/
106
- opt[:resume] = true
107
- when /reset/
108
- opt[:reset] = true
109
- when /autotag/
110
- opt[:autotag] = true
106
+ when /(resume|reset|autotag|archive|delete|finish|cancel|flag)/
107
+ opt[action.to_sym] = true
108
+ when /edit/
109
+ opt[:editor] = true
111
110
  when /(add|remove) tag/
112
111
  type = action =~ /^add/ ? 'add' : 'remove'
113
112
  raise InvalidArgument, "'add tag' and 'remove tag' can not be used together" if opt[:tag]
@@ -149,21 +148,9 @@ module Doing
149
148
 
150
149
  opt[:save_to] = filename
151
150
  end
152
- when /archive/
153
- opt[:archive] = true
154
- when /delete/
155
- opt[:delete] = true
156
- when /edit/
157
- opt[:editor] = true
158
- when /finish/
159
- opt[:finish] = true
160
- when /cancel/
161
- opt[:cancel] = true
162
151
  when /move/
163
152
  section = choose_section.strip
164
153
  opt[:move] = section.strip unless section =~ /^ *$/
165
- when /flag/
166
- opt[:flag] = true
167
154
  end
168
155
  end
169
156
  end
@@ -263,6 +250,9 @@ module Doing
263
250
  write(@doing_file)
264
251
 
265
252
  if opt[:editor]
253
+ sleep 2 # This seems to be necessary between running fzf
254
+ # and forking the editor, otherwise vim gets all
255
+ # screwy and I can't figure out why
266
256
  edit_items(items) # hooked
267
257
 
268
258
  write(@doing_file)
@@ -509,6 +509,8 @@ module Doing
509
509
  new_tag = r
510
510
 
511
511
  m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
512
+ next if v.nil?
513
+
512
514
  new_tag.gsub!("\\#{idx + 1}", v)
513
515
  end
514
516
  # Replace original tag if /r