doing 2.1.7 → 2.1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +9 -9
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +39 -1
  6. data/Dockerfile +9 -0
  7. data/Dockerfile-2.6 +9 -0
  8. data/Dockerfile-2.7 +8 -0
  9. data/Dockerfile-3.0 +8 -0
  10. data/Gemfile.lock +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +51 -6
  13. data/bin/doing +2098 -1944
  14. data/docs/doc/Array.html +1 -1
  15. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  16. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  17. data/docs/doc/BooleanTermParser/Query.html +1 -1
  18. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  19. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  20. data/docs/doc/BooleanTermParser.html +1 -1
  21. data/docs/doc/Doing/Color.html +1 -1
  22. data/docs/doc/Doing/Completion.html +1 -1
  23. data/docs/doc/Doing/Configuration.html +2 -2
  24. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  27. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  28. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  29. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  30. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  31. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  32. data/docs/doc/Doing/Errors.html +1 -1
  33. data/docs/doc/Doing/Hooks.html +1 -1
  34. data/docs/doc/Doing/Item.html +1 -1
  35. data/docs/doc/Doing/Items.html +1 -1
  36. data/docs/doc/Doing/LogAdapter.html +1 -1
  37. data/docs/doc/Doing/Note.html +1 -1
  38. data/docs/doc/Doing/Pager.html +1 -1
  39. data/docs/doc/Doing/Plugins.html +1 -1
  40. data/docs/doc/Doing/Prompt.html +132 -18
  41. data/docs/doc/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +2 -2
  43. data/docs/doc/Doing/Util/Backup.html +79 -2
  44. data/docs/doc/Doing/Util.html +1 -1
  45. data/docs/doc/Doing/WWID.html +90 -77
  46. data/docs/doc/Doing.html +2 -2
  47. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  48. data/docs/doc/GLI/Commands.html +1 -1
  49. data/docs/doc/GLI.html +1 -1
  50. data/docs/doc/Hash.html +1 -1
  51. data/docs/doc/PhraseParser/Operator.html +1 -1
  52. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  53. data/docs/doc/PhraseParser/Query.html +1 -1
  54. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  55. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  56. data/docs/doc/PhraseParser/TermClause.html +1 -1
  57. data/docs/doc/PhraseParser.html +1 -1
  58. data/docs/doc/Status.html +1 -1
  59. data/docs/doc/String.html +97 -1
  60. data/docs/doc/Symbol.html +36 -2
  61. data/docs/doc/Time.html +1 -1
  62. data/docs/doc/_index.html +1 -1
  63. data/docs/doc/file.README.html +2 -2
  64. data/docs/doc/index.html +2 -2
  65. data/docs/doc/method_list.html +299 -235
  66. data/docs/doc/top-level-namespace.html +1 -1
  67. data/docs/index.md +1 -1
  68. data/doing.rdoc +9 -2
  69. data/generate_completions.sh +1 -3
  70. data/lib/completion/_doing.zsh +1 -1
  71. data/lib/completion/doing.bash +2 -2
  72. data/lib/completion/doing.fish +2 -1
  73. data/lib/doing/completion/bash_completion.rb +2 -2
  74. data/lib/doing/completion/fish_completion.rb +2 -2
  75. data/lib/doing/completion/zsh_completion.rb +2 -2
  76. data/lib/doing/completion.rb +12 -2
  77. data/lib/doing/configuration.rb +19 -9
  78. data/lib/doing/hooks.rb +10 -5
  79. data/lib/doing/items.rb +16 -1
  80. data/lib/doing/log_adapter.rb +1 -0
  81. data/lib/doing/pager.rb +2 -20
  82. data/lib/doing/plugins/import/calendar_import.rb +5 -0
  83. data/lib/doing/plugins/import/doing_import.rb +2 -0
  84. data/lib/doing/plugins/import/timing_import.rb +5 -0
  85. data/lib/doing/prompt.rb +47 -8
  86. data/lib/doing/string.rb +20 -0
  87. data/lib/doing/symbol.rb +4 -0
  88. data/lib/doing/util_backup.rb +38 -8
  89. data/lib/doing/version.rb +1 -1
  90. data/lib/doing/wwid.rb +211 -106
  91. data/lib/doing.rb +1 -0
  92. data/lib/examples/plugins/hooks.rb +31 -0
  93. data/scripts/generate_bash_completions.rb +2 -2
  94. data/scripts/sort_commands.rb +59 -0
  95. metadata +7 -3
  96. data/lib/helpers/fuzzyfilefinder +0 -0
@@ -102,7 +102,7 @@
102
102
  </div>
103
103
 
104
104
  <div id="footer">
105
- Generated on Mon Dec 20 16:04:14 2021 by
105
+ Generated on Tue Dec 21 14:44:17 2021 by
106
106
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
107
107
  0.9.26 (ruby-3.0.1).
108
108
  </div>
data/docs/index.md CHANGED
@@ -6,7 +6,7 @@ _If you're one of the rare people like me who find this useful, feel free to [bu
6
6
 
7
7
 
8
8
 
9
- The current version of `doing` is 2.1.4.
9
+ The current version of `doing` is 2.1.9.
10
10
 
11
11
  Find all of the documentation in the [doing wiki](https://github.com/ttscoff/doing/wiki).
12
12
 
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.7
8
+ v2.1.10
9
9
 
10
10
  === Global Options
11
11
  === --config_file arg
@@ -397,7 +397,7 @@ File to write output to
397
397
  Shell to generate for (bash, zsh, fish)
398
398
 
399
399
  [Default Value] zsh
400
- [Must Match] (?i-mx:^[bzf](?:[ai]?sh)?$)
400
+ [Must Match] (?i-mx:^(?:[bzf](?:[ai]?sh)?|all)$)
401
401
 
402
402
 
403
403
  ==== Command: <tt>config </tt>
@@ -2155,6 +2155,13 @@ View entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all d
2155
2155
  [Default Value] None
2156
2156
 
2157
2157
 
2158
+ ===== --age AGE
2159
+
2160
+ Age (oldest|newest)
2161
+
2162
+ [Default Value] newest
2163
+
2164
+
2158
2165
  ===== -b|--bool BOOLEAN
2159
2166
 
2160
2167
  Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.
@@ -1,5 +1,3 @@
1
1
  #!/bin/bash
2
2
 
3
- bundle exec bin/doing completion --type fish --file lib/completion/doing.fish
4
- bundle exec bin/doing completion --type bash --file lib/completion/doing.bash
5
- bundle exec bin/doing completion --type zsh --file lib/completion/_doing.zsh
3
+ bundle exec bin/doing completion --type all
@@ -201,7 +201,7 @@ function _doing() {
201
201
  args=( {-f,--file=}"[Specify alternate doing file]" {-i,--interactive}"[Select from recent backups]" {-p,--prune=}"[Remove old backups]" {-r,--redo}"[Redo last undo]" )
202
202
  ;;
203
203
  view)
204
- args=( "(--after=)--after=}[View entries newer than date]" {-b,--bool=}"[Tag boolean]" "(--before=)--before=}[View entries older than date]" {-c,--count=}"[Count to display]" "(--case=)--case=}[Case sensitivity for search string matching [(c)ase-sensitive]" "(--color)--color}[Include colors in output]" "(--duration)--duration}[Show elapsed time on entries without @done tag]" "(--from=)--from=}[Date range to show]" {-i,--interactive}"[Select from a menu of matching entries to perform additional operations]" "(--not)--not}[Show items that *dont* match search stringt* match search string]" {-o,--output=}"[Output to export format]" "(--only_timed)--only_timed}[Only show items with recorded time intervals]" {-s,--section=}"[Section]" "(--search=)--search=}[Search filter]" {-t,--times}"[Show time intervals on @done tasks]" "(--tag=)--tag=}[Tag filter]" "(--tag_order=)--tag_order=}[Tag sort direction]" "(--tag_sort=)--tag_sort=}[Sort tags by]" "(--totals)--totals}[Show intervals with totals at the end of output]" {-x,--exact}"[Force exact search string matching]" )
204
+ args=( "(--after=)--after=}[View entries newer than date]" "(--age=)--age=}[Age]" {-b,--bool=}"[Tag boolean]" "(--before=)--before=}[View entries older than date]" {-c,--count=}"[Count to display]" "(--case=)--case=}[Case sensitivity for search string matching [(c)ase-sensitive]" "(--color)--color}[Include colors in output]" "(--duration)--duration}[Show elapsed time on entries without @done tag]" "(--from=)--from=}[Date range to show]" {-i,--interactive}"[Select from a menu of matching entries to perform additional operations]" "(--not)--not}[Show items that *dont* match search stringt* match search string]" {-o,--output=}"[Output to export format]" "(--only_timed)--only_timed}[Only show items with recorded time intervals]" {-s,--section=}"[Section]" "(--search=)--search=}[Search filter]" {-t,--times}"[Show time intervals on @done tasks]" "(--tag=)--tag=}[Tag filter]" "(--tag_order=)--tag_order=}[Tag sort direction]" "(--tag_sort=)--tag_sort=}[Sort tags by]" "(--totals)--totals}[Show intervals with totals at the end of output]" {-x,--exact}"[Force exact search string matching]" )
205
205
  ;;
206
206
  views)
207
207
  args=( {-c,--column}"[List in single column]" )
@@ -366,9 +366,9 @@ local words=$(doing views)
366
366
  IFS="$OLD_IFS"
367
367
 
368
368
  if [[ "$token" == --* ]]; then
369
- COMPREPLY=( $( compgen -W '--after --bool --before --count --case --color --duration --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
369
+ COMPREPLY=( $( compgen -W '--after --age --bool --before --count --case --color --duration --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
370
370
  elif [[ "$token" == -* ]]; then
371
- COMPREPLY=( $( compgen -W '-b -c -i -o -s -t -x --after --bool --before --count --case --color --duration --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
371
+ COMPREPLY=( $( compgen -W '-b -c -i -o -s -t -x --after --age --bool --before --count --case --color --duration --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
372
372
  else
373
373
  local nocasematchWasOff=0
374
374
  shopt nocasematch >/dev/null || nocasematchWasOff=1
@@ -371,6 +371,7 @@ complete -c doing -l interactive -s i -f -n '__fish_doing_using_command undo' -
371
371
  complete -c doing -l prune -s p -f -r -n '__fish_doing_using_command undo' -d Remove\ old\ backups
372
372
  complete -c doing -l redo -s r -f -n '__fish_doing_using_command undo' -d Redo\ last\ undo
373
373
  complete -c doing -l after -f -r -n '__fish_doing_using_command view' -d View\ entries\ newer\ than\ date
374
+ complete -c doing -l age -f -r -n '__fish_doing_using_command view' -d Age
374
375
  complete -c doing -l bool -s b -f -r -n '__fish_doing_using_command view' -d Tag\ boolean
375
376
  complete -c doing -l before -f -r -n '__fish_doing_using_command view' -d View\ entries\ older\ than\ date
376
377
  complete -c doing -l count -s c -f -r -n '__fish_doing_using_command view' -d Count\ to\ display
@@ -414,5 +415,5 @@ complete -f -c doing -s b -l bool -x -n '__fish_doing_using_command again resume
414
415
  complete -f -c doing -l case -x -n '__fish_doing_using_command again resume archive move cancel finish grep search import last mark flag note reset begin rotate select show show tag tags tags view' -a 'case-sensitive ignore smart'
415
416
  complete -f -c doing -l tag_sort -x -n '__fish_doing_using_command grep search on recent show since today view yesterday' -a 'name time'
416
417
  complete -f -c doing -l tag_order -x -n '__fish_doing_using_command show view yesterday' -a 'asc desc'
417
- complete -f -c doing -s a -l age -x -n '__fish_doing_using_command show' -a 'oldest newest'
418
+ complete -f -c doing -s a -l age -x -n '__fish_doing_using_command show view' -a 'oldest newest'
418
419
  complete -f -c doing -s s -l section -x -n '__fish_doing_using_command again resume autotag cancel done did finish grep search import last mark flag meanwhile note now next on recent reset begin rotate select since tag tags today view wiki yesterday' -a '(__fish_doing_complete_sections)'
@@ -9,7 +9,7 @@ module Doing
9
9
  logic = []
10
10
 
11
11
  @commands.each_with_index do |cmd, i|
12
- @bar.advance
12
+ @bar.advance(status: cmd[:commands].first)
13
13
 
14
14
  data = get_help_sections(cmd[:commands].first)
15
15
 
@@ -167,7 +167,7 @@ module Doing
167
167
  data = get_help_sections
168
168
  @global_options = parse_options(data[:global_options])
169
169
  @commands = parse_commands(data[:commands])
170
- @bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Bash completions: \033[0;35;40m[:bar]\033[0m", total: @commands.count, bar_format: :blade)
170
+ @bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Bash completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :blade, status: 'Reading subcommands')
171
171
  @bar.resize(25)
172
172
  end
173
173
 
@@ -132,7 +132,7 @@ module Doing
132
132
  need_section = []
133
133
 
134
134
  @commands.each_with_index do |cmd, i|
135
- @bar.advance
135
+ @bar.advance(status: cmd[:commands].first)
136
136
  data = get_help_sections(cmd[:commands].first)
137
137
 
138
138
  if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
@@ -200,7 +200,7 @@ module Doing
200
200
  data = get_help_sections
201
201
  @global_options = parse_options(data[:global_options])
202
202
  @commands = parse_commands(data[:commands])
203
- @bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Fish completions: \033[0;35;40m[:bar]\033[0m", total: @commands.count, bar_format: :blade)
203
+ @bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Fish completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :blade, status: 'processing subcommands')
204
204
  @bar.resize(25)
205
205
  end
206
206
 
@@ -96,7 +96,7 @@ module Doing
96
96
  out = []
97
97
 
98
98
  @commands.each_with_index do |cmd, i|
99
- @bar.advance
99
+ @bar.advance(status: cmd[:commands].first)
100
100
 
101
101
  data = get_help_sections(cmd[:commands].first)
102
102
  option_arr = []
@@ -127,7 +127,7 @@ module Doing
127
127
  data = get_help_sections
128
128
  @global_options = parse_options(data[:global_options])
129
129
  @commands = parse_commands(data[:commands])
130
- @bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar]\033[0m", total: @commands.count, bar_format: :blade)
130
+ @bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :blade, status: 'processing subcommands')
131
131
  @bar.resize(25)
132
132
  end
133
133
 
@@ -19,11 +19,21 @@ module Doing
19
19
  # @param file [String] Path to save to, or 'stdout'
20
20
  #
21
21
  def generate_completion(type: 'zsh', file: 'stdout')
22
+ if type =~ /^all$/i
23
+ Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths')
24
+ generate_completion(type: 'fish', file: 'lib/completion/doing.fish')
25
+ Doing.logger.warn('File written:', "fish completions written to lib/completion/doing.fish")
26
+ generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh')
27
+ Doing.logger.warn('File written:', "zsh completions written to lib/completion/_doing.zsh")
28
+ generate_completion(type: 'bash', file: 'lib/completion/doing.bash')
29
+ Doing.logger.warn('File written:', "bash completions written to lib/completion/doing.bash")
30
+ return
31
+ end
22
32
 
23
33
  generator = case type.to_s
24
- when /^f/
34
+ when /^f/i
25
35
  FishCompletions.new
26
- when /^b/
36
+ when /^b/i
27
37
  BashCompletions.new
28
38
  else
29
39
  ZshCompletions.new
@@ -31,8 +31,8 @@ module Doing
31
31
  },
32
32
  'doing_file' => '~/.local/share/doing/what_was_i_doing.md',
33
33
  'backup_dir' => '~/.local/share/doing/doing_backup',
34
- 'current_section' => 'Currently',
35
34
  'history_size' => 15,
35
+ 'current_section' => 'Currently',
36
36
  'paginate' => false,
37
37
  'never_time' => [],
38
38
  'never_finish' => [],
@@ -152,17 +152,23 @@ module Doing
152
152
  ##
153
153
  ## @return [String] file path
154
154
  ##
155
- def choose_config
155
+ def choose_config(create: false)
156
156
  return @config_file if @force_answer
157
157
 
158
- if @additional_configs.count.positive?
158
+ if @additional_configs.count.positive? || create
159
159
  choices = [@config_file].concat(@additional_configs)
160
+ choices.push('Create a new .doingrc in the current directory') if create && !File.exist?('.doingrc')
160
161
  res = Doing::Prompt.choose_from(choices.uniq.sort.reverse,
161
162
  sorted: false,
162
163
  prompt: 'Local configs found, select which to update > ')
163
164
 
164
165
  raise UserCancelled, 'Cancelled' unless res
165
166
 
167
+ if res =~ /^Create a new/
168
+ res = File.expand_path('.doingrc')
169
+ FileUtils.touch(res)
170
+ end
171
+
166
172
  res.strip || @config_file
167
173
  else
168
174
  @config_file
@@ -249,7 +255,8 @@ module Doing
249
255
  # defaults.
250
256
  #
251
257
  def from(user_config)
252
- Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
258
+ # Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
259
+ Configuration[user_config].stringify_keys.deep_merge(DEFAULTS, { extend_existing_arrays: true, sort_merged_arrays: true })
253
260
  end
254
261
 
255
262
  ##
@@ -320,8 +327,8 @@ module Doing
320
327
 
321
328
  Hooks.trigger :post_config, self
322
329
 
323
- # config = local_config.deep_merge(config) unless @ignore_local
324
- config = Util.deep_merge_hashes(config, local_config) unless @ignore_local
330
+ config = local_config.deep_merge(config, { extend_existing_arrays: true, sort_merged_arrays: true }) unless @ignore_local
331
+ # config = Util.deep_merge_hashes(config, local_config) unless @ignore_local
325
332
 
326
333
  Hooks.trigger :post_local_config, self
327
334
 
@@ -397,7 +404,7 @@ module Doing
397
404
 
398
405
  begin
399
406
  additional_configs.each do |cfg|
400
- local_configs.deep_merge(Util.safe_load_file(cfg))
407
+ local_configs.deep_merge(Util.safe_load_file(cfg), { extend_existing_arrays: true, sort_merged_arrays: true })
401
408
  end
402
409
  rescue StandardError
403
410
  Doing.logger.error('Config:', 'Error reading local configuration(s)')
@@ -416,15 +423,18 @@ module Doing
416
423
  end
417
424
 
418
425
  begin
426
+
419
427
  user_config = Util.safe_load_file(config_file)
428
+ raise StandardError, 'Invalid config file format' unless user_config.is_a?(Hash)
429
+
420
430
  if user_config.key?('html_template')
421
431
  user_config['export_templates'] ||= {}
422
- user_config['export_templates'].deep_merge(user_config.delete('html_template'))
432
+ user_config['export_templates'].deep_merge(user_config.delete('html_template'), { extend_existing_arrays: true, sort_merged_arrays: true })
423
433
  end
424
434
 
425
435
  user_config['include_notes'] = user_config.delete(':include_notes') if user_config.key?(':include_notes')
426
436
 
427
- user_config.deep_merge(DEFAULTS)
437
+ user_config.deep_merge(DEFAULTS, { extend_existing_arrays: true, sort_merged_arrays: true })
428
438
  rescue StandardError => e
429
439
  Doing.logger.error('Config:', 'Error reading default configuration')
430
440
  Doing.logger.error('Error:', e.message)
data/lib/doing/hooks.rb CHANGED
@@ -6,11 +6,16 @@ module Doing
6
6
  DEFAULT_PRIORITY = 20
7
7
 
8
8
  @registry = {
9
- post_config: [],
10
- post_local_config: [],
11
- post_read: [],
12
- pre_write: [],
13
- post_write: []
9
+ post_config: [], # wwid
10
+ post_local_config: [], # wwid
11
+ post_read: [], # wwid
12
+ pre_entry_add: [], # wwid, new_entry
13
+ post_entry_added: [], # wwid, new_entry.dup
14
+ post_entry_updated: [], # wwid, entry
15
+ post_entry_removed: [], # wwid, entry.dup
16
+ pre_export: [], # wwid, format, entries
17
+ pre_write: [], # wwid, file
18
+ post_write: [] # wwid, file
14
19
  }
15
20
 
16
21
  # map of all hooks and their priorities
data/lib/doing/items.rb CHANGED
@@ -69,7 +69,7 @@ module Doing
69
69
  if section =~ /^all$/i
70
70
  dup
71
71
  else
72
- items = Items.new.concat(select { |item| item.section == section })
72
+ items = Items.new.concat(select { |item| !item.nil? && item.section == section })
73
73
  items.add_section(section, log: false)
74
74
  items
75
75
  end
@@ -84,6 +84,7 @@ module Doing
84
84
  deleted = delete(item)
85
85
  Doing.logger.count(:deleted)
86
86
  Doing.logger.info('Entry deleted:', deleted.title) if single
87
+ deleted
87
88
  end
88
89
 
89
90
  ##
@@ -111,6 +112,20 @@ module Doing
111
112
  end
112
113
  end
113
114
 
115
+ ##
116
+ ## Return Items containing items that don't exist in receiver
117
+ ##
118
+ ## @param items [Items] Receiver
119
+ ##
120
+ def diff(items)
121
+ diff = Items.new
122
+ each do |item|
123
+ res = items.select { |i| i.equal?(item) }
124
+ diff.push(item) unless res.count.positive?
125
+ end
126
+ diff
127
+ end
128
+
114
129
  # Output sections and items in Doing file format
115
130
  def to_s
116
131
  out = []
@@ -259,6 +259,7 @@ module Doing
259
259
 
260
260
  if @logdev == $stdout
261
261
  $stdout.print results.map {|res| res[:message].uncolor }.join("\n")
262
+ $stdout.puts
262
263
  else
263
264
  results.each do |msg|
264
265
  @logdev.puts color_message(msg[:level], msg[:message])
data/lib/doing/pager.rb CHANGED
@@ -64,22 +64,8 @@ module Doing
64
64
 
65
65
  private
66
66
 
67
- def command_exist?(command)
68
- exts = ENV.fetch("PATHEXT", "").split(::File::PATH_SEPARATOR)
69
- if Pathname.new(command).absolute?
70
- ::File.exist?(command) ||
71
- exts.any? { |ext| ::File.exist?("#{command}#{ext}")}
72
- else
73
- ENV.fetch("PATH", "").split(::File::PATH_SEPARATOR).any? do |dir|
74
- file = ::File.join(dir, command)
75
- ::File.exist?(file) ||
76
- exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
77
- end
78
- end
79
- end
80
-
81
67
  def git_pager
82
- command_exist?("git") ? `git config --get-all core.pager` : nil
68
+ TTY::Which.exist?('git') ? `#{TTY::Which.which('git')} config --get-all core.pager` : nil
83
69
  end
84
70
 
85
71
  def pagers
@@ -91,11 +77,7 @@ module Doing
91
77
  execs = commands.empty? ? pagers : commands
92
78
  execs
93
79
  .compact.map(&:strip).reject(&:empty?).uniq
94
- .find { |cmd| command_exist?(cmd.split.first) }
95
- end
96
-
97
- def exec_available?(*commands)
98
- !find_executable(*commands).nil?
80
+ .find { |cmd| TTY::Which.exist?(cmd.split.first) }
99
81
  end
100
82
 
101
83
  def which_pager
@@ -73,7 +73,12 @@ module Doing
73
73
  dups = filtered - new_items.count
74
74
  Doing.logger.info(%(Skipped #{dups} items with overlapping times)) if dups.positive?
75
75
 
76
+ new_items.map { |item| Hooks.trigger :pre_entry_add, self, item }
77
+
76
78
  wwid.content.concat(new_items)
79
+
80
+ new_items.map { |item| Hooks.trigger :post_entry_added, self, item.dup }
81
+
77
82
  Doing.logger.info(%(Imported #{new_items.count} items to #{section}))
78
83
  end
79
84
 
@@ -80,7 +80,9 @@ module Doing
80
80
 
81
81
  imported.each do |item|
82
82
  wwid.content.add_section(item.section) unless wwid.content.section?(item.section)
83
+ Hooks.trigger :pre_entry_add, self, item
83
84
  wwid.content.push(item)
85
+ Hooks.trigger :post_entry_added, self, item.dup
84
86
  end
85
87
 
86
88
  Doing.logger.info('Imported:', "#{imported.count} items")
@@ -77,7 +77,12 @@ module Doing
77
77
  dups = filtered - new_items.count
78
78
  Doing.logger.debug('Skipped:' , %(#{dups} items with overlapping times)) if dups.positive?
79
79
 
80
+ new_items.map { |item| Hooks.trigger :pre_entry_add, self, item }
81
+
80
82
  wwid.content.concat(new_items)
83
+
84
+ new_items.map { |item| Hooks.trigger :post_entry_added, self, item.dup }
85
+
81
86
  Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
82
87
  end
83
88
 
data/lib/doing/prompt.rb CHANGED
@@ -86,7 +86,38 @@ module Doing
86
86
  @fzf ||= install_fzf
87
87
  end
88
88
 
89
- def install_fzf
89
+ def uninstall_fzf
90
+ fzf_bin = File.join(File.dirname(__FILE__), '../helpers/fzf/bin/fzf')
91
+ FileUtils.rm_f(fzf_bin) if File.exist?(fzf_bin)
92
+ Doing.logger.warn('fzf:', "removed #{fzf_bin}")
93
+ end
94
+
95
+ def which_fzf
96
+ fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
97
+ fzf_bin = File.join(fzf_dir, 'bin/fzf')
98
+ return fzf_bin if File.exist?(fzf_bin)
99
+
100
+ Doing.logger.debug('fzf:', 'Using user-installed fzf')
101
+ TTY::Which.which('fzf')
102
+ end
103
+
104
+ def silence_std(file = '/dev/null')
105
+ $stdout = File.new(file, 'w')
106
+ $stderr = File.new(file, 'w')
107
+ end
108
+
109
+ def restore_std
110
+ $stdout = STDOUT
111
+ $stderr = STDERR
112
+ end
113
+
114
+ def install_fzf(force: false)
115
+ if force
116
+ uninstall_fzf
117
+ elsif which_fzf
118
+ return which_fzf
119
+ end
120
+
90
121
  fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
91
122
  FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
92
123
  fzf_bin = File.join(fzf_dir, 'bin/fzf')
@@ -94,17 +125,25 @@ module Doing
94
125
 
95
126
  prev_level = Doing.logger.level
96
127
  Doing.logger.adjust_verbosity({ log_level: :info })
97
- Doing.logger.log_now(:warn, 'Compiling and installing fzf -- this will only happen once')
98
- Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
128
+ Doing.logger.log_now(:warn, 'fzf:', 'Compiling and installing fzf -- this will only happen once')
129
+ Doing.logger.log_now(:warn, 'fzf:', 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
99
130
 
100
- system("'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
131
+ silence_std
132
+ `'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
101
133
  unless File.exist?(fzf_bin)
134
+ restore_std
102
135
  Doing.logger.log_now(:warn, 'Error installing, trying again as root')
103
- system("sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
136
+ silence_std
137
+ `sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
138
+ end
139
+ restore_std
140
+ unless File.exist?(fzf_bin)
141
+ Doing.logger.error('fzf:', 'unable to install fzf. You can install manually and Doing will use the system version.')
142
+ Doing.logger.error('fzf:', 'see https://github.com/junegunn/fzf#installation')
143
+ raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues')
104
144
  end
105
- raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues') unless File.exist?(fzf_bin)
106
145
 
107
- Doing.logger.info("fzf installed to #{fzf}")
146
+ Doing.logger.info('fzf:', "installed to #{fzf}")
108
147
  Doing.logger.adjust_verbosity({ log_level: prev_level })
109
148
  fzf_bin
110
149
  end
@@ -189,7 +228,7 @@ module Doing
189
228
  %(-q "#{query}"),
190
229
  '--info=inline'
191
230
  ]
192
- fzf_args.push('-1') unless opt.fetch(:show_if_single)
231
+ fzf_args.push('-1') unless opt.fetch(:show_if_single, false)
193
232
  fzf_args << case case_sensitive
194
233
  when :sensitive
195
234
  '+i'
data/lib/doing/string.rb CHANGED
@@ -263,6 +263,26 @@ module Doing
263
263
  number == 1 ? self : "#{self}s"
264
264
  end
265
265
 
266
+ ##
267
+ ## Convert an age string to a qualified type
268
+ ##
269
+ ## @return [Symbol] :oldest or :newest
270
+ ##
271
+ def normalize_age!(default = :newest)
272
+ replace normalize_age(default)
273
+ end
274
+
275
+ def normalize_age(default = :newest)
276
+ case self
277
+ when /^o/i
278
+ :oldest
279
+ when /^n/i
280
+ :newest
281
+ else
282
+ default
283
+ end
284
+ end
285
+
266
286
  ##
267
287
  ## Convert a sort order string to a qualified type
268
288
  ##
data/lib/doing/symbol.rb CHANGED
@@ -9,6 +9,10 @@ module Doing
9
9
  to_s.normalize_bool(default)
10
10
  end
11
11
 
12
+ def normalize_age(default = :newest)
13
+ to_s.normalize_age(default)
14
+ end
15
+
12
16
  def normalize_order(default = 'asc')
13
17
  to_s.normalize_order(default)
14
18
  end
@@ -20,6 +20,34 @@ module Doing
20
20
  backups[limit..-1].each do |file|
21
21
  FileUtils.rm(File.join(backup_dir, file))
22
22
  end
23
+
24
+ clear_redo(filename)
25
+ end
26
+
27
+ ##
28
+ ## Delete all redo files
29
+ ##
30
+ ## @param limit Maximum number of backups to retain
31
+ ##
32
+ def clear_redo(filename)
33
+ filename ||= Doing.config.settings['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
+ ##
41
+ ## Retrieve the most recent backup
42
+ ##
43
+ ## @param filename The filename
44
+ ## @return [String] filename
45
+ ##
46
+ def last_backup(filename = nil, count: 1)
47
+ filename ||= Doing.config.settings['doing_file']
48
+
49
+ backup = get_backups(filename).slice(count - 1)
50
+ backup.nil? ? nil : File.join(backup_dir, backup)
23
51
  end
24
52
 
25
53
  ##
@@ -33,15 +61,13 @@ module Doing
33
61
  Doing.logger.benchmark(:restore_backup, :start)
34
62
  filename ||= Doing.config.settings['doing_file']
35
63
 
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)
64
+ backup_file = last_backup(filename, count: count)
65
+ raise DoingRuntimeError, 'End of undo history' if backup_file.nil?
40
66
 
41
67
  save_undone(filename)
42
68
  FileUtils.mv(backup_file, filename)
43
69
  prune_backups_after(File.basename(backup_file))
44
- Doing.logger.warn('File update:', "restored from #{result}")
70
+ Doing.logger.warn('File update:', "restored from #{backup_file}")
45
71
  Doing.logger.benchmark(:restore_backup, :finish)
46
72
  end
47
73
 
@@ -103,6 +129,8 @@ module Doing
103
129
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
104
130
  end
105
131
 
132
+ raise DoingRuntimeError, 'No backup files to load' if options.empty?
133
+
106
134
  backup_file = show_menu(options, filename)
107
135
  idx = undones.index(File.basename(backup_file))
108
136
  skipped = undones.slice!(idx, undones.count - idx)
@@ -135,8 +163,10 @@ module Doing
135
163
  arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
136
164
  end
137
165
 
166
+ raise DoingRuntimeError, 'No backup files to load' if options.empty?
167
+
138
168
  backup_file = show_menu(options, filename)
139
- write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
169
+ Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
140
170
  FileUtils.mv(backup_file, filename)
141
171
  prune_backups_after(File.basename(backup_file))
142
172
  Doing.logger.warn('File update:', "restored from #{backup_file}")
@@ -215,10 +245,10 @@ module Doing
215
245
  Time.now.strftime('%Y-%m-%d_%H.%M.%S')
216
246
  end
217
247
 
218
- def get_backups(filename = nil)
248
+ def get_backups(filename = nil, include_forward: false)
219
249
  filename ||= Doing.config.settings['doing_file']
220
250
  backups = Dir.glob("*___#{File.basename(filename)}", base: backup_dir).sort.reverse
221
- backups.delete_if { |f| f =~ /^undone/ }
251
+ backups.delete_if { |f| f =~ /^undone/ } unless include_forward
222
252
  end
223
253
 
224
254
  def save_undone(filename = nil)
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.7'
2
+ VERSION = '2.1.11'
3
3
  end