doing 2.1.41 → 2.1.42

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