doing 2.1.30 → 2.1.34

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/CHANGELOG.md +4972 -0
  4. data/Dockerfile-2.6 +3 -1
  5. data/Dockerfile-2.7 +4 -2
  6. data/Dockerfile-3.0 +3 -1
  7. data/Gemfile.lock +1 -67
  8. data/README.md +1 -1
  9. data/bash_profile +13 -0
  10. data/bin/commands/again.rb +1 -1
  11. data/bin/commands/archive.rb +3 -3
  12. data/bin/commands/cancel.rb +1 -1
  13. data/bin/commands/commands.rb +8 -8
  14. data/bin/commands/completion.rb +61 -19
  15. data/bin/commands/config.rb +22 -19
  16. data/bin/commands/done.rb +2 -2
  17. data/bin/commands/flag.rb +1 -1
  18. data/bin/commands/grep.rb +6 -33
  19. data/bin/commands/last.rb +1 -1
  20. data/bin/commands/meanwhile.rb +2 -2
  21. data/bin/commands/now.rb +2 -2
  22. data/bin/commands/on.rb +6 -16
  23. data/bin/commands/open.rb +1 -1
  24. data/bin/commands/recent.rb +5 -17
  25. data/bin/commands/rotate.rb +17 -0
  26. data/bin/commands/sections.rb +82 -7
  27. data/bin/commands/show.rb +8 -28
  28. data/bin/commands/since.rb +5 -16
  29. data/bin/commands/tag_dir.rb +27 -3
  30. data/bin/commands/today.rb +3 -28
  31. data/bin/commands/view.rb +3 -3
  32. data/bin/commands/yesterday.rb +3 -36
  33. data/bin/doing +29 -139
  34. data/docs/doc/Array.html +1 -1
  35. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  36. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  37. data/docs/doc/BooleanTermParser/Query.html +1 -1
  38. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  39. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  40. data/docs/doc/BooleanTermParser.html +1 -1
  41. data/docs/doc/Doing/Color.html +1 -1
  42. data/docs/doc/Doing/Completion.html +324 -4
  43. data/docs/doc/Doing/Configuration.html +3 -3
  44. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  45. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  46. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  47. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  48. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  49. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  50. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  51. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  52. data/docs/doc/Doing/Errors.html +1 -1
  53. data/docs/doc/Doing/Hooks.html +1 -1
  54. data/docs/doc/Doing/Item.html +144 -3
  55. data/docs/doc/Doing/Items.html +209 -1
  56. data/docs/doc/Doing/LogAdapter.html +1 -1
  57. data/docs/doc/Doing/Logger.html +1807 -0
  58. data/docs/doc/Doing/Note.html +109 -3
  59. data/docs/doc/Doing/Pager.html +1 -1
  60. data/docs/doc/Doing/Plugins.html +1 -1
  61. data/docs/doc/Doing/Prompt.html +1 -1
  62. data/docs/doc/Doing/Section.html +1 -1
  63. data/docs/doc/Doing/TemplateString.html +1 -1
  64. data/docs/doc/Doing/Types.html +3 -3
  65. data/docs/doc/Doing/Util/Backup.html +1 -1
  66. data/docs/doc/Doing/Util.html +1 -1
  67. data/docs/doc/Doing/WWID.html +8 -58
  68. data/docs/doc/Doing.html +4 -4
  69. data/docs/doc/FalseClass.html +1 -1
  70. data/docs/doc/GLI/Commands/Help.html +1 -1
  71. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  72. data/docs/doc/GLI/Commands.html +1 -1
  73. data/docs/doc/GLI.html +1 -1
  74. data/docs/doc/Hash.html +1 -1
  75. data/docs/doc/Object.html +1 -1
  76. data/docs/doc/PhraseParser/Operator.html +1 -1
  77. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  78. data/docs/doc/PhraseParser/Query.html +1 -1
  79. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  80. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  81. data/docs/doc/PhraseParser/TermClause.html +1 -1
  82. data/docs/doc/PhraseParser.html +1 -1
  83. data/docs/doc/Status.html +1 -1
  84. data/docs/doc/String.html +1 -1
  85. data/docs/doc/Symbol.html +1 -1
  86. data/docs/doc/Time.html +1 -1
  87. data/docs/doc/TrueClass.html +1 -1
  88. data/docs/doc/_index.html +12 -10
  89. data/docs/doc/class_list.html +1 -1
  90. data/docs/doc/file.README.html +2 -2
  91. data/docs/doc/index.html +2 -2
  92. data/docs/doc/method_list.html +424 -304
  93. data/docs/doc/top-level-namespace.html +105 -1
  94. data/docs/index.md +1 -1
  95. data/doing.gemspec +24 -24
  96. data/doing.rdoc +259 -26
  97. data/example_plugin.rb +7 -5
  98. data/inputrc +57 -0
  99. data/lib/completion/_doing.zsh +48 -52
  100. data/lib/completion/doing.bash +14 -25
  101. data/lib/completion/doing.fish +41 -15
  102. data/lib/doing/add_options.rb +152 -0
  103. data/lib/doing/array/array.rb +16 -0
  104. data/lib/doing/changelog/changes.rb +1 -1
  105. data/lib/doing/chronify/string.rb +1 -1
  106. data/lib/doing/completion/bash_completion.rb +12 -51
  107. data/lib/doing/completion/fish_completion.rb +17 -53
  108. data/lib/doing/completion/zsh_completion.rb +21 -59
  109. data/lib/doing/completion.rb +203 -17
  110. data/lib/doing/configuration.rb +7 -1
  111. data/lib/doing/item.rb +30 -5
  112. data/lib/doing/items.rb +53 -5
  113. data/lib/doing/{log_adapter.rb → logger.rb} +8 -2
  114. data/lib/doing/note.rb +24 -8
  115. data/lib/doing/plugins/export/dayone_export.rb +8 -6
  116. data/lib/doing/plugins/export/html_export.rb +4 -4
  117. data/lib/doing/plugins/export/json_export.rb +19 -20
  118. data/lib/doing/plugins/export/markdown_export.rb +2 -2
  119. data/lib/doing/plugins/export/template_export.rb +4 -4
  120. data/lib/doing/plugins/import/calendar_import.rb +2 -2
  121. data/lib/doing/plugins/import/doing_import.rb +2 -2
  122. data/lib/doing/plugins/import/timing_import.rb +2 -2
  123. data/lib/doing/string/highlight.rb +3 -4
  124. data/lib/doing/string/string.rb +8 -0
  125. data/lib/doing/string/tags.rb +1 -1
  126. data/lib/doing/types.rb +2 -2
  127. data/lib/doing/util.rb +1 -1
  128. data/lib/doing/util_backup.rb +12 -12
  129. data/lib/doing/version.rb +1 -1
  130. data/lib/doing/wwid.rb +119 -120
  131. data/lib/doing.rb +61 -3
  132. data/lib/examples/commands/wiki.rb +27 -19
  133. data/lib/examples/plugins/capture_thing_import.rb +1 -1
  134. data/lib/helpers/threaded_tests.rb +2 -0
  135. data/scripts/setting_replace.rb +11 -0
  136. metadata +109 -124
  137. data/.yardoc/checksums +0 -29
  138. data/.yardoc/complete +0 -0
  139. data/.yardoc/object_types +0 -0
  140. data/.yardoc/objects/root.dat +0 -0
  141. data/.yardoc/proxy_types +0 -0
  142. data/bin/commands/add_section.rb +0 -15
@@ -1,7 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doing
2
4
  module Completion
3
- class ZshCompletions
5
+ class ::String
6
+ def sanitize
7
+ gsub(/'/, '\\\'').gsub(/\[/, '(').gsub(/\]/, ')')
8
+ end
9
+ end
4
10
 
11
+ # Generate completions for zsh
12
+ class ZshCompletions
5
13
  attr_accessor :commands, :global_options
6
14
 
7
15
  def generate_helpers
@@ -34,58 +42,11 @@ module Doing
34
42
  }
35
43
 
36
44
  EOFUNCTIONS
45
+ @bar.advance(status: '✅')
37
46
  @bar.finish
38
47
  out
39
48
  end
40
49
 
41
- def get_help_sections(command = '')
42
- res = `doing help #{command}`.strip
43
- scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
44
- sections = {}
45
- scanned.each do |sect|
46
- title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
47
- content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
48
- sections[title] = content
49
- end
50
- sections
51
- end
52
-
53
- def parse_option(option)
54
- res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
55
- return nil unless res
56
-
57
- {
58
- short: res['short'],
59
- long: res['long'],
60
- arg: res[:arg],
61
- description: res['desc'].short_desc
62
- }
63
- end
64
-
65
- def parse_options(options)
66
- options.map { |opt| parse_option(opt) }
67
- end
68
-
69
- def parse_command(command)
70
- res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
71
- if res.nil?
72
- Doing.logger.error('Completion:', "Error parsing #{command}")
73
- return nil
74
-
75
- end
76
- commands = [res['cmd']]
77
- commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
78
-
79
- {
80
- commands: commands,
81
- description: res['desc'].short_desc
82
- }
83
- end
84
-
85
- def parse_commands(commands)
86
- commands.map { |cmd| parse_command(cmd) }
87
- end
88
-
89
50
  def generate_subcommand_completions
90
51
  out = []
91
52
  @commands.each_with_index do |cmd, i|
@@ -103,19 +64,19 @@ module Doing
103
64
  @commands.each_with_index do |cmd, i|
104
65
  @bar.advance(status: cmd[:commands].first)
105
66
 
106
- data = get_help_sections(cmd[:commands].first)
67
+ data = Completion.get_help_sections(cmd[:commands].first)
107
68
  option_arr = []
108
69
 
109
70
  if data[:command_options]
110
- parse_options(data[:command_options]).each do |option|
71
+ Completion.parse_options(data[:command_options]).each do |option|
111
72
  next if option.nil?
112
73
 
113
- arg = option[:arg] ? '=' : ''
74
+ arg = option[:arg] ? ":#{option[:arg]}:" : ''
114
75
 
115
76
  option_arr << if option[:short]
116
- %({-#{option[:short]},--#{option[:long]}#{arg}}"[#{option[:description].gsub(/'/, '\\\'')}]")
77
+ %({'(--#{option[:long]})-#{option[:short]}','(-#{option[:short]})--#{option[:long]}'}"[#{option[:description].sanitize}]#{arg}")
117
78
  else
118
- %("(--#{option[:long]}#{arg})--#{option[:long]}#{arg}}[#{option[:description].gsub(/'/, '\\\'')}]")
79
+ %("--#{option[:long]}[#{option[:description].sanitize}]#{arg}")
119
80
  end
120
81
  end
121
82
  end
@@ -129,11 +90,12 @@ module Doing
129
90
  end
130
91
 
131
92
  def initialize
132
- data = get_help_sections
133
- @global_options = parse_options(data[:global_options])
134
- @commands = parse_commands(data[:commands])
135
- @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')
136
- @bar.resize(25)
93
+ data = Completion.get_help_sections
94
+ @global_options = Completion.parse_options(data[:global_options])
95
+ @commands = Completion.parse_commands(data[:commands])
96
+ @bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :square, hide_cursor: true, status: 'processing subcommands')
97
+ width = TTY::Screen.columns - 45
98
+ @bar.resize(width)
137
99
  end
138
100
 
139
101
  def generate_completions
@@ -11,25 +11,118 @@ require 'bash_completion'
11
11
  module Doing
12
12
  # Completion script generator
13
13
  module Completion
14
+ OPTIONS_RX = /(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/.freeze
15
+ SECTIONS_RX = /(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/.freeze
16
+ COMMAND_RX = /^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/.freeze
17
+
14
18
  class << self
19
+ def get_help_sections(command = '')
20
+ res = `doing help #{command}`.strip
21
+ scanned = res.scan(SECTIONS_RX)
22
+ sections = {}
23
+ scanned.each do |sect|
24
+ title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
25
+ content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
26
+ sections[title] = content
27
+ end
28
+ sections
29
+ end
30
+
31
+ def parse_option(option)
32
+ res = option.match(OPTIONS_RX)
33
+ return nil unless res
34
+
35
+ {
36
+ short: res['short'],
37
+ long: res['long'],
38
+ arg: res['arg'],
39
+ description: res['desc'].short_desc
40
+ }
41
+ end
42
+
43
+ def parse_options(options)
44
+ options.map { |opt| parse_option(opt) }
45
+ end
46
+
47
+ def parse_command(command)
48
+ res = command.match(COMMAND_RX)
49
+ commands = [res['cmd']]
50
+ commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
51
+
52
+ {
53
+ commands: commands,
54
+ description: res['desc'].short_desc
55
+ }
56
+ end
57
+
58
+ def parse_commands(commands)
59
+ commands.map { |cmd| parse_command(cmd) }
60
+ end
61
+
15
62
  # Generate a completion script and output to file or
16
63
  # stdout
17
64
  #
18
65
  # @param type [String] shell to generate for (zsh|bash|fish)
19
66
  # @param file [String] Path to save to, or 'stdout'
20
67
  #
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
68
+ def generate_completion(type: 'zsh', file: :default, link: true)
69
+ return generate_all if type =~ /^all$/i
70
+
71
+ file = file == :default ? default_file(type) : file
72
+ file = validate_target(file)
73
+ result = generate_type(type)
74
+
75
+ if file =~ /^stdout$/i
76
+ $stdout.puts result
77
+ else
78
+ File.open(file, 'w') { |f| f.puts result }
79
+ Doing.logger.warn('File written:', "#{type} completions written to #{file}")
80
+
81
+ link_completion_type(type, file) if link
31
82
  end
83
+ end
84
+
85
+ def link_default(type)
86
+ type = normalize_type(type)
87
+ raise InvalidArgument, 'Unrecognized shell specified' if type == :invalid
88
+
89
+ return %i[zsh bash fish].each { |t| link_default(t) } if type == :all
32
90
 
91
+ install_builtin(type)
92
+
93
+ link_completion_type(type, File.join(default_dir, default_filenames[type]))
94
+ end
95
+
96
+ def install_builtin(type)
97
+ FileUtils.mkdir_p(default_dir)
98
+ src = File.expand_path(File.join(File.dirname(__FILE__), '..', 'completion', default_filenames[type]))
99
+
100
+ if File.exist?(File.join(default_dir, default_filenames[type]))
101
+ return unless Doing::Prompt.yn("Update #{type} completion script", default_response: 'n')
102
+ end
103
+
104
+ FileUtils.cp(src, default_dir)
105
+ Doing.logger.warn('File written:', "#{type} completions saved to #{default_file(type)}")
106
+ end
107
+
108
+ def normalize_type(type)
109
+ case type.to_s
110
+ when /^f/i
111
+ :fish
112
+ when /^b/i
113
+ :bash
114
+ when /^z/i
115
+ :zsh
116
+ when /^a/i
117
+ :all
118
+ else
119
+ :invalid
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def generate_type(type)
33
126
  generator = case type.to_s
34
127
  when /^f/i
35
128
  FishCompletions.new
@@ -39,16 +132,109 @@ module Doing
39
132
  ZshCompletions.new
40
133
  end
41
134
 
42
- result = generator.generate_completions
135
+ generator.generate_completions
136
+ end
43
137
 
44
- if file =~ /^stdout$/i
45
- $stdout.puts result
46
- else
47
- File.open(File.expand_path(file), 'w') do |f|
48
- f.puts result
138
+ def validate_target(file)
139
+ unless file =~ /stdout/i
140
+ file = validate_file(file)
141
+
142
+ validate_dir(file)
143
+ end
144
+
145
+ file
146
+ end
147
+
148
+ def default_dir
149
+ File.expand_path('~/.local/share/doing/completion')
150
+ end
151
+
152
+ def default_filenames
153
+ { zsh: '_doing.zsh', bash: 'doing.bash', fish: 'doing.fish' }
154
+ end
155
+
156
+ def default_file(type)
157
+ type = normalize_type(type)
158
+
159
+ File.join(default_dir, default_filenames[type])
160
+ end
161
+
162
+ def validate_file(file)
163
+ file = File.expand_path(file)
164
+ if File.exist?(file)
165
+ res = Doing::Prompt.yn("Overwrite #{file}", default_response: 'y')
166
+ raise UserCancelled unless res
167
+
168
+ FileUtils.rm(file) if res
169
+ end
170
+ file
171
+ end
172
+
173
+ def validate_dir(file)
174
+ dir = File.dirname(file)
175
+ unless File.directory?(dir)
176
+ res = Doing::Prompt.yn("#{dir} doesn't exist, create it", default_response: 'y')
177
+ raise UserCancelled unless res
178
+
179
+ FileUtils.mkdir_p(dir)
180
+ end
181
+ dir
182
+ end
183
+
184
+ def generate_all
185
+ Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths')
186
+ generate_completion(type: 'fish', file: 'lib/completion/doing.fish', link: false)
187
+ Doing.logger.warn('File written:', 'fish completions written to lib/completion/doing.fish')
188
+ generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh', link: false)
189
+ Doing.logger.warn('File written:', 'zsh completions written to lib/completion/_doing.zsh')
190
+ generate_completion(type: 'bash', file: 'lib/completion/doing.bash', link: false)
191
+ Doing.logger.warn('File written:', 'bash completions written to lib/completion/doing.bash')
192
+ end
193
+
194
+ def link_completion_type(type, file)
195
+ dir = File.dirname(file)
196
+ case type.to_s
197
+ when /^b/i
198
+ unless dir =~ %r{(\.bash_it/completion|bash_completion/completions)}
199
+ link_completion(file, ['~/.bash_it/completion/enabled', '/usr/share/bash_completion/completions'], 'doing.bash')
49
200
  end
50
- Doing.logger.warn('File written:', "#{type} completions written to #{file}")
201
+ when /^f/i
202
+ link_completion(file, ['~/.config/fish/completions'], 'doing.fish') unless dir =~ %r{.config/fish/completions}
203
+ when /^z/i
204
+ unless dir =~ %r{(\.oh-my-zsh/completions|share/site-functions)}
205
+ link_completion(file, ['~/.oh-my-zsh/completions', '/usr/local/share/zsh/site-functions'], '_doing.zsh')
206
+ end
207
+ end
208
+ end
209
+
210
+ def link_completion(file, targets, filename)
211
+ return if targets.map { |t| File.expand_path(t) }.include?(File.dirname(file))
212
+
213
+ found = false
214
+ linked = false
215
+
216
+ targets.each do |target|
217
+ next unless File.directory?(File.expand_path(target))
218
+ found = true
219
+
220
+ target_file = File.join(File.expand_path(target), filename)
221
+ next unless Doing::Prompt.yn("Create link to #{target_file}", default_response: 'n')
222
+
223
+ FileUtils.ln_s(File.expand_path(file), target_file, force: true)
224
+ Doing.logger.warn('File linked:', "#{File.expand_path(file)} -> #{target_file}")
225
+ linked = true
226
+ break
227
+ end
228
+
229
+ return if linked
230
+
231
+ unless found
232
+ $stdout.puts 'No known auto-load directory found for specified shell'.red
233
+ $stdout.puts "Looked for #{targets.join(', ')}, found no existing directory".yellow
51
234
  end
235
+ $stdout.puts 'If you don\'t want to autoload completions'.yellow
236
+ $stdout.puts 'you can source the script directly in your shell\'s startup file:'.yellow
237
+ $stdout.puts %(source "#{file}").boldwhite
52
238
  end
53
239
  end
54
240
  end
@@ -156,7 +156,13 @@ module Doing
156
156
  ##
157
157
  ## @return [String] file path
158
158
  ##
159
- def choose_config(create: false)
159
+ def choose_config(create: false, local: false)
160
+ if local && create
161
+ res = File.expand_path('.doingrc')
162
+ FileUtils.touch(res)
163
+ return res
164
+ end
165
+
160
166
  return @config_file if @force_answer
161
167
 
162
168
  if @additional_configs&.count&.positive? || create
data/lib/doing/item.rb CHANGED
@@ -35,6 +35,8 @@ module Doing
35
35
 
36
36
  ## If the entry doesn't have a @done date, return the elapsed time
37
37
  def duration
38
+ return nil unless should_time? && should_finish?
39
+
38
40
  return nil if @title =~ /(?<=^| )@done\b/
39
41
 
40
42
  return Time.now - @date
@@ -88,17 +90,20 @@ module Doing
88
90
  ##
89
91
  ## Test for equality between items
90
92
  ##
91
- ## @param other [Item] The other item
93
+ ## @param other [Item] The other item
94
+ ## @param match_section [Boolean] If true, require item sections to match
92
95
  ##
93
96
  ## @return [Boolean] is equal?
94
97
  ##
95
- def equal?(other)
98
+ def equal?(other, match_section: false)
96
99
  return false if @title.strip != other.title.strip
97
100
 
98
101
  return false if @date != other.date
99
102
 
100
103
  return false unless @note.equal?(other.note)
101
104
 
105
+ return false if match_section && @section != other.section
106
+
102
107
  true
103
108
  end
104
109
 
@@ -272,7 +277,7 @@ module Doing
272
277
  end
273
278
 
274
279
  def highlight_search(search, distance: nil, negate: false, case_type: nil)
275
- prefs = Doing.config.settings['search'] || {}
280
+ prefs = Doing.setting('search', {})
276
281
  matching = prefs.fetch('matching', 'pattern').normalize_matching
277
282
  distance ||= prefs.fetch('distance', 3).to_i
278
283
  case_type ||= prefs.fetch('case', 'smart').normalize_case
@@ -311,7 +316,7 @@ module Doing
311
316
  ## @return [Boolean] matches search criteria
312
317
  ##
313
318
  def search(search, distance: nil, negate: false, case_type: nil)
314
- prefs = Doing.config.settings['search'] || {}
319
+ prefs = Doing.setting('search', {})
315
320
  matching = prefs.fetch('matching', 'pattern').normalize_matching
316
321
  distance ||= prefs.fetch('distance', 3).to_i
317
322
  case_type ||= prefs.fetch('case', 'smart').normalize_case
@@ -354,6 +359,24 @@ module Doing
354
359
  negate ? !matches : matches
355
360
  end
356
361
 
362
+ ##
363
+ ## Test if item has a @done tag
364
+ ##
365
+ ## @return [Boolean] true item has @done tag
366
+ ##
367
+ def finished?
368
+ tags?('done')
369
+ end
370
+
371
+ ##
372
+ ## Test if item does not contain @done tag
373
+ ##
374
+ ## @return [Boolean] true if item is missing @done tag
375
+ ##
376
+ def unfinished?
377
+ tags?('done', negate: true)
378
+ end
379
+
357
380
  ##
358
381
  ## Test if item is included in never_finish config and
359
382
  ## thus should not receive a @done tag
@@ -436,7 +459,7 @@ module Doing
436
459
  private
437
460
 
438
461
  def should?(key)
439
- config = Doing.config.settings
462
+ config = Doing.settings
440
463
  return true unless config[key].is_a?(Array)
441
464
 
442
465
  config[key].each do |tag|
@@ -451,6 +474,8 @@ module Doing
451
474
  end
452
475
 
453
476
  def calc_interval
477
+ return nil unless should_time? && should_finish?
478
+
454
479
  done = end_date
455
480
  return nil if done.nil?
456
481
 
data/lib/doing/items.rb CHANGED
@@ -58,6 +58,24 @@ module Doing
58
58
  Doing.logger.info('New section:', %("#{section}" added)) if log
59
59
  end
60
60
 
61
+ def delete_section(section, log: false)
62
+ return unless section?(section)
63
+
64
+ raise DoingRuntimeError, 'Section not empty' if in_section(section).count > 0
65
+
66
+ deleted = false
67
+
68
+ @sections.each do |sect|
69
+ if sect.title == section && in_section(sect).count.zero?
70
+ @sections.delete(sect)
71
+ Doing.logger.info('Removed section:', %("#{section}" removed)) if log
72
+ return
73
+ end
74
+ end
75
+
76
+ Doing.logger.error('Not found:', %("#{section}" not found))
77
+ end
78
+
61
79
  # Get a new Items object containing only items in a
62
80
  # specified section
63
81
  #
@@ -126,14 +144,44 @@ module Doing
126
144
  diff
127
145
  end
128
146
 
147
+ ##
148
+ ## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
149
+ ##
150
+ ## @return [Items] Items array with duplicate entries removed
151
+ ##
152
+ def dedup(match_section: true)
153
+ unique = Items.new
154
+ each do |item|
155
+ unique.push(item) unless unique.include?(item, match_section: match_section)
156
+ end
157
+
158
+ unique
159
+ end
160
+
161
+ def dedup!(match_section: true)
162
+ replace dedup(match_section: match_section)
163
+ end
164
+
165
+ def include?(item, match_section: true)
166
+ includes = false
167
+ each do |other_item|
168
+ if other_item.equal?(item, match_section: match_section)
169
+ includes = true
170
+ break
171
+ end
172
+ end
173
+
174
+ includes
175
+ end
176
+
129
177
  # Output sections and items in Doing file format
130
178
  def to_s
131
179
  out = []
132
180
  @sections.each do |section|
133
181
  out.push(section.original)
134
- items = in_section(section.title).sort_by { |i| i.date }
135
- items.reverse! if Doing.config.settings['doing_file_sort'].normalize_order == :desc
136
- items.each { |item| out.push(item.to_s)}
182
+ items = in_section(section.title).sort_by(&:date)
183
+ items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
184
+ items.each { |item| out.push(item.to_s) }
137
185
  end
138
186
 
139
187
  out.join("\n")
@@ -141,8 +189,8 @@ module Doing
141
189
 
142
190
  # @private
143
191
  def inspect
144
- "#<Doing::Items #{count} items, #{@sections.count} sections: #{@sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')}>"
192
+ sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
193
+ "#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
145
194
  end
146
-
147
195
  end
148
196
  end
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## Log adapter
6
6
  ##
7
- class LogAdapter
7
+ class Logger
8
8
  # Sets the log device
9
9
  attr_writer :logdev
10
10
 
@@ -332,9 +332,15 @@ module Doing
332
332
  if tags_added.empty?
333
333
  count(:skipped, level: :debug, message: 'no tags added to %count %items')
334
334
  elsif single && item
335
+ elapsed = if item && tags_added.include?('done')
336
+ item.interval ? " (#{item.interval&.time_string(format: :dhm)})" : ''
337
+ else
338
+ ''
339
+ end
340
+
335
341
  added = tags_added.log_tags
336
342
  info('Tagged:',
337
- %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added} to #{item.title}))
343
+ %(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added}#{elapsed} to #{item.title}))
338
344
  else
339
345
  count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
340
346
  end
data/lib/doing/note.rb CHANGED
@@ -5,7 +5,6 @@ module Doing
5
5
  ## This class describes an item note.
6
6
  ##
7
7
  class Note < Array
8
-
9
8
  ##
10
9
  ## Initializes a new note
11
10
  ##
@@ -28,9 +27,10 @@ module Doing
28
27
  ##
29
28
  def add(note, replace: false)
30
29
  clear if replace
31
- if note.is_a?(String)
30
+ case note
31
+ when String
32
32
  append_string(note)
33
- elsif note.is_a?(Array)
33
+ when Array
34
34
  append(note)
35
35
  end
36
36
  end
@@ -55,7 +55,7 @@ module Doing
55
55
  ## @return [Array] Stripped note
56
56
  ##
57
57
  def strip_lines
58
- map(&:strip)
58
+ Note.new(map(&:strip))
59
59
  end
60
60
 
61
61
  def strip_lines!
@@ -64,8 +64,24 @@ module Doing
64
64
 
65
65
  ##
66
66
  ## Note as multi-line string
67
- def to_s
68
- compress.strip_lines.map { |l| "\t\t#{l}" }.join("\n")
67
+ ##
68
+ ## @param prefix [String] prefix for each line (default two tabs, TaskPaper format)
69
+ ##
70
+ def to_s(prefix: "\t\t")
71
+ compress.strip_lines.map { |l| "#{prefix}#{l}" }.join("\n")
72
+ end
73
+
74
+ ##
75
+ ## Returns note as a single line, newlines separated by
76
+ ## space
77
+ ##
78
+ ## @return [String] Line representation of the Note.
79
+ ##
80
+ ## @param separator The separator with which to
81
+ ## join multiple lines
82
+ ##
83
+ def to_line(separator: ' ')
84
+ compress.strip_lines.join(separator)
69
85
  end
70
86
 
71
87
  # @private
@@ -94,7 +110,7 @@ module Doing
94
110
  ## @param lines [Array] Array of strings
95
111
  ##
96
112
  def append(lines)
97
- concat(lines)
113
+ concat(lines.utf8)
98
114
  replace compress
99
115
  end
100
116
 
@@ -105,7 +121,7 @@ module Doing
105
121
  ## newlines will be split
106
122
  ##
107
123
  def append_string(input)
108
- concat(input.split(/\n/).map(&:strip))
124
+ concat(input.utf8.split(/\n/).map(&:strip))
109
125
  replace compress
110
126
  end
111
127
  end