doing 2.1.1pre → 2.1.5pre

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +58 -8
  6. data/Gemfile.lock +25 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +447 -149
  10. data/doc/Array.html +63 -1
  11. data/doc/BooleanTermParser/Clause.html +293 -0
  12. data/doc/BooleanTermParser/Operator.html +172 -0
  13. data/doc/BooleanTermParser/Query.html +417 -0
  14. data/doc/BooleanTermParser/QueryParser.html +135 -0
  15. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  16. data/doc/BooleanTermParser.html +115 -0
  17. data/doc/Doing/CLIFormat.html +131 -0
  18. data/doc/Doing/Color.html +2 -2
  19. data/doc/Doing/Completion.html +1 -1
  20. data/doc/Doing/Configuration.html +168 -73
  21. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  22. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  23. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  24. data/doc/Doing/Errors/EmptyInput.html +1 -1
  25. data/doc/Doing/Errors/NoResults.html +1 -1
  26. data/doc/Doing/Errors/PluginException.html +1 -1
  27. data/doc/Doing/Errors/UserCancelled.html +1 -1
  28. data/doc/Doing/Errors/WrongCommand.html +1 -1
  29. data/doc/Doing/Errors.html +1 -1
  30. data/doc/Doing/Hooks.html +1 -1
  31. data/doc/Doing/Item.html +177 -86
  32. data/doc/Doing/Items.html +36 -2
  33. data/doc/Doing/LogAdapter.html +70 -1
  34. data/doc/Doing/Note.html +5 -134
  35. data/doc/Doing/Pager.html +1 -1
  36. data/doc/Doing/Plugins.html +380 -40
  37. data/doc/Doing/Prompt.html +70 -18
  38. data/doc/Doing/Section.html +1 -1
  39. data/doc/Doing/TemplateString.html +713 -0
  40. data/doc/Doing/Util/Backup.html +686 -0
  41. data/doc/Doing/Util.html +16 -4
  42. data/doc/Doing/WWID.html +133 -73
  43. data/doc/Doing.html +4 -4
  44. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  45. data/doc/GLI/Commands.html +1 -1
  46. data/doc/GLI.html +1 -1
  47. data/doc/Hash.html +1 -1
  48. data/doc/PhraseParser/Operator.html +172 -0
  49. data/doc/PhraseParser/PhraseClause.html +303 -0
  50. data/doc/PhraseParser/Query.html +495 -0
  51. data/doc/PhraseParser/QueryParser.html +136 -0
  52. data/doc/PhraseParser/QueryTransformer.html +124 -0
  53. data/doc/PhraseParser/TermClause.html +293 -0
  54. data/doc/PhraseParser.html +115 -0
  55. data/doc/Status.html +1 -1
  56. data/doc/String.html +319 -13
  57. data/doc/Symbol.html +35 -1
  58. data/doc/Time.html +70 -2
  59. data/doc/_index.html +132 -4
  60. data/doc/class_list.html +1 -1
  61. data/doc/file.README.html +2 -2
  62. data/doc/index.html +2 -2
  63. data/doc/method_list.html +648 -160
  64. data/doc/top-level-namespace.html +2 -2
  65. data/doing.gemspec +3 -0
  66. data/doing.rdoc +263 -82
  67. data/lib/completion/doing.bash +18 -18
  68. data/lib/doing/array.rb +9 -0
  69. data/lib/doing/boolean_term_parser.rb +86 -0
  70. data/lib/doing/configuration.rb +63 -24
  71. data/lib/doing/item.rb +112 -10
  72. data/lib/doing/items.rb +6 -0
  73. data/lib/doing/log_adapter.rb +28 -0
  74. data/lib/doing/note.rb +31 -30
  75. data/lib/doing/phrase_parser.rb +124 -0
  76. data/lib/doing/plugin_manager.rb +57 -13
  77. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  78. data/lib/doing/plugins/export/template_export.rb +113 -81
  79. data/lib/doing/prompt.rb +26 -13
  80. data/lib/doing/string.rb +114 -29
  81. data/lib/doing/string_chronify.rb +5 -1
  82. data/lib/doing/symbol.rb +4 -0
  83. data/lib/doing/template_string.rb +197 -0
  84. data/lib/doing/time.rb +32 -0
  85. data/lib/doing/util.rb +6 -7
  86. data/lib/doing/util_backup.rb +287 -0
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +152 -55
  89. data/lib/doing.rb +9 -0
  90. data/lib/templates/doing-dayone-entry.erb +6 -0
  91. data/lib/templates/doing-dayone.erb +5 -0
  92. metadata +85 -2
@@ -101,9 +101,9 @@ _doing_finish() {
101
101
  _doing_grep() {
102
102
 
103
103
  if [[ "$token" == --* ]]; then
104
- COMPREPLY=( $( compgen -W '--after --before --case --from --interactive --not --output --only_timed --section --times --tag_sort --totals --exact' -- $token ) )
104
+ COMPREPLY=( $( compgen -W '--after --before --case --duration --from --interactive --not --output --only_timed --section --times --tag_sort --totals --exact' -- $token ) )
105
105
  elif [[ "$token" == -* ]]; then
106
- COMPREPLY=( $( compgen -W '-i -o -s -t -x --after --before --case --from --interactive --not --output --only_timed --section --times --tag_sort --totals --exact' -- $token ) )
106
+ COMPREPLY=( $( compgen -W '-i -o -s -t -x --after --before --case --duration --from --interactive --not --output --only_timed --section --times --tag_sort --totals --exact' -- $token ) )
107
107
 
108
108
  fi
109
109
  }
@@ -131,9 +131,9 @@ _doing_import() {
131
131
  _doing_last() {
132
132
 
133
133
  if [[ "$token" == --* ]]; then
134
- COMPREPLY=( $( compgen -W '--bool --case --editor --not --section --search --tag --exact' -- $token ) )
134
+ COMPREPLY=( $( compgen -W '--bool --case --duration --editor --not --section --search --tag --exact' -- $token ) )
135
135
  elif [[ "$token" == -* ]]; then
136
- COMPREPLY=( $( compgen -W '-e -s -x --bool --case --editor --not --section --search --tag --exact' -- $token ) )
136
+ COMPREPLY=( $( compgen -W '-e -s -x --bool --case --duration --editor --not --section --search --tag --exact' -- $token ) )
137
137
 
138
138
  fi
139
139
  }
@@ -191,9 +191,9 @@ _doing_now() {
191
191
  _doing_on() {
192
192
 
193
193
  if [[ "$token" == --* ]]; then
194
- COMPREPLY=( $( compgen -W '--output --section --times --tag_sort --totals' -- $token ) )
194
+ COMPREPLY=( $( compgen -W '--duration --output --section --times --tag_sort --totals' -- $token ) )
195
195
  elif [[ "$token" == -* ]]; then
196
- COMPREPLY=( $( compgen -W '-o -s -t --output --section --times --tag_sort --totals' -- $token ) )
196
+ COMPREPLY=( $( compgen -W '-o -s -t --duration --output --section --times --tag_sort --totals' -- $token ) )
197
197
 
198
198
  fi
199
199
  }
@@ -221,9 +221,9 @@ _doing_plugins() {
221
221
  _doing_recent() {
222
222
 
223
223
  if [[ "$token" == --* ]]; then
224
- COMPREPLY=( $( compgen -W '--interactive --section --times --tag_sort --totals' -- $token ) )
224
+ COMPREPLY=( $( compgen -W '--duration --interactive --section --times --tag_sort --totals' -- $token ) )
225
225
  elif [[ "$token" == -* ]]; then
226
- COMPREPLY=( $( compgen -W '-i -s -t --interactive --section --times --tag_sort --totals' -- $token ) )
226
+ COMPREPLY=( $( compgen -W '-i -s -t --duration --interactive --section --times --tag_sort --totals' -- $token ) )
227
227
 
228
228
  fi
229
229
  }
@@ -276,9 +276,9 @@ local words=$(doing sections)
276
276
  IFS="$OLD_IFS"
277
277
 
278
278
  if [[ "$token" == --* ]]; then
279
- COMPREPLY=( $( compgen -W '--age --after --bool --before --count --case --from --interactive --not --output --only_timed --sort --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
279
+ COMPREPLY=( $( compgen -W '--age --after --bool --before --count --case --duration --from --interactive --not --output --only_timed --sort --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
280
280
  elif [[ "$token" == -* ]]; then
281
- COMPREPLY=( $( compgen -W '-a -b -c -i -o -s -t -x --age --after --bool --before --count --case --from --interactive --not --output --only_timed --sort --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
281
+ COMPREPLY=( $( compgen -W '-a -b -c -i -o -s -t -x --age --after --bool --before --count --case --duration --from --interactive --not --output --only_timed --sort --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
282
282
  else
283
283
  local nocasematchWasOff=0
284
284
  shopt nocasematch >/dev/null || nocasematchWasOff=1
@@ -301,9 +301,9 @@ IFS="$OLD_IFS"
301
301
  _doing_since() {
302
302
 
303
303
  if [[ "$token" == --* ]]; then
304
- COMPREPLY=( $( compgen -W '--output --section --times --tag_sort --totals' -- $token ) )
304
+ COMPREPLY=( $( compgen -W '--duration --output --section --times --tag_sort --totals' -- $token ) )
305
305
  elif [[ "$token" == -* ]]; then
306
- COMPREPLY=( $( compgen -W '-o -s -t --output --section --times --tag_sort --totals' -- $token ) )
306
+ COMPREPLY=( $( compgen -W '-o -s -t --duration --output --section --times --tag_sort --totals' -- $token ) )
307
307
 
308
308
  fi
309
309
  }
@@ -331,9 +331,9 @@ _doing_template() {
331
331
  _doing_today() {
332
332
 
333
333
  if [[ "$token" == --* ]]; then
334
- COMPREPLY=( $( compgen -W '--after --before --from --output --section --times --tag_sort --totals' -- $token ) )
334
+ COMPREPLY=( $( compgen -W '--after --before --duration --from --output --section --times --tag_sort --totals' -- $token ) )
335
335
  elif [[ "$token" == -* ]]; then
336
- COMPREPLY=( $( compgen -W '-o -s -t --after --before --from --output --section --times --tag_sort --totals' -- $token ) )
336
+ COMPREPLY=( $( compgen -W '-o -s -t --after --before --duration --from --output --section --times --tag_sort --totals' -- $token ) )
337
337
 
338
338
  fi
339
339
  }
@@ -356,9 +356,9 @@ local words=$(doing views)
356
356
  IFS="$OLD_IFS"
357
357
 
358
358
  if [[ "$token" == --* ]]; then
359
- COMPREPLY=( $( compgen -W '--after --bool --before --count --case --color --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
359
+ 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 ) )
360
360
  elif [[ "$token" == -* ]]; then
361
- COMPREPLY=( $( compgen -W '-b -c -i -o -s -t -x --after --bool --before --count --case --color --from --interactive --not --output --only_timed --section --search --times --tag --tag_order --tag_sort --totals --exact' -- $token ) )
361
+ 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 ) )
362
362
  else
363
363
  local nocasematchWasOff=0
364
364
  shopt nocasematch >/dev/null || nocasematchWasOff=1
@@ -401,9 +401,9 @@ _doing_wiki() {
401
401
  _doing_yesterday() {
402
402
 
403
403
  if [[ "$token" == --* ]]; then
404
- COMPREPLY=( $( compgen -W '--after --before --from --output --section --times --tag_order --tag_sort --totals' -- $token ) )
404
+ COMPREPLY=( $( compgen -W '--after --before --duration --from --output --section --times --tag_order --tag_sort --totals' -- $token ) )
405
405
  elif [[ "$token" == -* ]]; then
406
- COMPREPLY=( $( compgen -W '-o -s -t --after --before --from --output --section --times --tag_order --tag_sort --totals' -- $token ) )
406
+ COMPREPLY=( $( compgen -W '-o -s -t --after --before --duration --from --output --section --times --tag_order --tag_sort --totals' -- $token ) )
407
407
 
408
408
  fi
409
409
  }
data/lib/doing/array.rb CHANGED
@@ -5,6 +5,15 @@ module Doing
5
5
  ## Array helpers
6
6
  ##
7
7
  class ::Array
8
+ ##
9
+ ## Convert an @tags to plain strings
10
+ ##
11
+ ## @return [Array] array of strings
12
+ ##
13
+ def tags_to_array
14
+ map { |t| t.sub(/^@/, '') }
15
+ end
16
+
8
17
  # Convert strings to @tags
9
18
  #
10
19
  # @example `['one', '@two', 'three'].to_tags`
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+
5
+ module BooleanTermParser
6
+ # This query parser adds an optional operator ("+" or "-") to the simple term
7
+ # parser. In order to do that, a new "clause" node is added to the parse tree.
8
+ class QueryParser < Parslet::Parser
9
+ rule(:term) { match('[^\s]').repeat(1).as(:term) }
10
+ rule(:operator) { (str('+') | str('-')).as(:operator) }
11
+ rule(:clause) { (operator.maybe >> term).as(:clause) }
12
+ rule(:space) { match('\s').repeat(1) }
13
+ rule(:query) { (clause >> space.maybe).repeat.as(:query) }
14
+ root(:query)
15
+ end
16
+
17
+ class QueryTransformer < Parslet::Transform
18
+ rule(:clause => subtree(:clause)) do
19
+ Clause.new(clause[:operator]&.to_s, clause[:term].to_s)
20
+ end
21
+ rule(:query => sequence(:clauses)) { Query.new(clauses) }
22
+ end
23
+
24
+ class Operator
25
+ def self.symbol(str)
26
+ case str
27
+ when '+'
28
+ :must
29
+ when '-'
30
+ :must_not
31
+ when nil
32
+ :should
33
+ else
34
+ raise "Unknown operator: #{str}"
35
+ end
36
+ end
37
+ end
38
+
39
+ class Clause
40
+ attr_accessor :operator, :term
41
+
42
+ def initialize(operator, term)
43
+ self.operator = Operator.symbol(operator)
44
+ self.term = term
45
+ end
46
+ end
47
+
48
+ class Query
49
+ attr_accessor :should_terms, :must_not_terms, :must_terms
50
+
51
+ def initialize(clauses)
52
+ grouped = clauses.chunk { |c| c.operator }.to_h
53
+ self.should_terms = grouped.fetch(:should, []).map(&:term)
54
+ self.must_not_terms = grouped.fetch(:must_not, []).map(&:term)
55
+ self.must_terms = grouped.fetch(:must, []).map(&:term)
56
+ end
57
+
58
+ def to_elasticsearch
59
+ query = {}
60
+
61
+ if should_terms.any?
62
+ query[:should] = should_terms.map do |term|
63
+ match(term)
64
+ end
65
+ end
66
+
67
+ if must_terms.any?
68
+ query[:must] = must_terms.map do |term|
69
+ match(term)
70
+ end
71
+ end
72
+
73
+ if must_not_terms.any?
74
+ query[:must_not] = must_not_terms.map do |term|
75
+ match(term)
76
+ end
77
+ end
78
+
79
+ query
80
+ end
81
+
82
+ def match(term)
83
+ term
84
+ end
85
+ end
86
+ end
@@ -7,7 +7,11 @@ module Doing
7
7
  class Configuration
8
8
  attr_reader :settings
9
9
 
10
- attr_writer :ignore_local
10
+ attr_writer :ignore_local, :config_file, :force_answer
11
+
12
+ def force_answer
13
+ @force_answer ||= false
14
+ end
11
15
 
12
16
  MissingConfigFile = Class.new(RuntimeError)
13
17
 
@@ -25,35 +29,40 @@ module Doing
25
29
  'plugin_path' => File.join(Util.user_home, '.config', 'doing', 'plugins'),
26
30
  'command_path' => File.join(Util.user_home, '.config', 'doing', 'commands')
27
31
  },
28
- 'doing_file' => '~/what_was_i_doing.md',
32
+ 'doing_file' => '~/.local/share/doing/what_was_i_doing.md',
33
+ 'backup_dir' => '~/.local/share/doing/doing_backup',
29
34
  'current_section' => 'Currently',
30
35
  'paginate' => false,
31
36
  'never_time' => [],
32
37
  'never_finish' => [],
38
+ 'date_tags' => ['done', 'defer(?:red)?', 'waiting'],
33
39
 
34
40
  'timer_format' => 'text',
41
+ 'interval_format' => 'text',
35
42
 
36
43
  'templates' => {
37
44
  'default' => {
38
45
  'date_format' => '%Y-%m-%d %H:%M',
39
- 'template' => '%date | %title%note',
46
+ 'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
47
+ %yellow%interval%boldred%duration%dark%white%80_14┃ note',
40
48
  'wrap_width' => 0,
41
49
  'order' => 'asc'
42
50
  },
43
51
  'today' => {
44
52
  'date_format' => '%_I:%M%P',
45
- 'template' => '%date: %title %interval%note',
53
+ 'template' => '%date: %title %interval%duration%note',
46
54
  'wrap_width' => 0,
47
55
  'order' => 'asc'
48
56
  },
49
57
  'last' => {
50
58
  'date_format' => '%-I:%M%P on %a',
51
- 'template' => '%title (at %date)%odnote',
59
+ 'template' => '%title (at %date) %interval%duration%odnote',
52
60
  'wrap_width' => 88
53
61
  },
54
62
  'recent' => {
55
63
  'date_format' => '%_I:%M%P',
56
- 'template' => '%shortdate: %title (%section)',
64
+ 'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
65
+ %yellow%interval%boldred%duration%dark%white%80_14┃ note',
57
66
  'wrap_width' => 88,
58
67
  'count' => 10,
59
68
  'order' => 'asc'
@@ -65,7 +74,7 @@ module Doing
65
74
  'views' => {
66
75
  'done' => {
67
76
  'date_format' => '%_I:%M%P',
68
- 'template' => '%date | %title%note',
77
+ 'template' => '%date | %title (%section)% 18: note',
69
78
  'wrap_width' => 0,
70
79
  'section' => 'All',
71
80
  'count' => 0,
@@ -86,6 +95,11 @@ module Doing
86
95
  'marker_color' => 'red',
87
96
  'default_tags' => [],
88
97
  'tag_sort' => 'name',
98
+ 'search' => {
99
+ 'matching' => 'pattern', # fuzzy, pattern, exact
100
+ 'distance' => 3,
101
+ 'case' => 'smart' # sensitive, ignore, smart
102
+ },
89
103
  'include_notes' => true
90
104
  }
91
105
 
@@ -99,24 +113,32 @@ module Doing
99
113
  @config_file ||= default_config_file
100
114
  end
101
115
 
102
- def config_file=(file)
103
- @config_file = file
104
- end
105
-
106
116
  def config_dir
107
117
  @config_dir ||= File.join(Util.user_home, '.config', 'doing')
108
- # @config_dir ||= Util.user_home
118
+ end
119
+
120
+ ##
121
+ ## Check if configuration enforces exact string matching
122
+ ##
123
+ ## @return [Boolean] exact matching enabled
124
+ ##
125
+ def exact_match?
126
+ search_settings = @settings['search']
127
+ matching = search_settings.fetch('matching', 'pattern').normalize_matching
128
+ matching == :exact
109
129
  end
110
130
 
111
131
  def default_config_file
112
- raise DoingRuntimeError, "#{config_dir} exists but is not a directory" if File.exist?(config_dir) && !File.directory?(config_dir)
132
+ if File.exist?(config_dir) && !File.directory?(config_dir)
133
+ raise DoingRuntimeError, "#{config_dir} exists but is not a directory"
134
+
135
+ end
113
136
 
114
137
  unless File.exist?(config_dir)
115
138
  FileUtils.mkdir_p(config_dir)
116
139
  Doing.logger.log_now(:warn, "Config directory created at #{config_dir}")
117
140
  end
118
141
 
119
- # File.join(config_dir, 'config.yml')
120
142
  File.join(config_dir, 'config.yml')
121
143
  end
122
144
 
@@ -130,10 +152,13 @@ module Doing
130
152
  ## @return [String] file path
131
153
  ##
132
154
  def choose_config
155
+ return @config_file if @force_answer
156
+
133
157
  if @additional_configs.count.positive?
134
- choices = [@config_file]
135
- choices.concat(@additional_configs)
136
- res = Doing::Prompt.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to update > ')
158
+ choices = [@config_file].concat(@additional_configs)
159
+ res = Doing::Prompt.choose_from(choices.uniq.sort.reverse,
160
+ sorted: false,
161
+ prompt: 'Local configs found, select which to update > ')
137
162
 
138
163
  raise UserCancelled, 'Cancelled' unless res
139
164
 
@@ -153,7 +178,7 @@ module Doing
153
178
  ## matched, first match wins)
154
179
  ## @return [Array] ordered array of resolved keys
155
180
  ##
156
- def resolve_key_path(keypath)
181
+ def resolve_key_path(keypath, create: false)
157
182
  cfg = @settings
158
183
  real_path = []
159
184
  unless keypath =~ /^[.*]?$/
@@ -170,8 +195,18 @@ module Doing
170
195
  end
171
196
 
172
197
  if new_cfg.nil?
173
- Doing.logger.error("Key match not found: #{path}")
174
- break
198
+ return nil unless create
199
+
200
+ resolved = real_path.count.positive? ? "Resolved #{real_path.join('->')}, but " : ''
201
+ Doing.logger.log_now(:warn, "#{resolved}#{path} is unknown")
202
+ new_path = [*real_path, path, *paths].join('->')
203
+ Doing.logger.log_now(:warn, "Continuing will create the path #{new_path}")
204
+ res = Prompt.yn('Key path not found, create it?', default_response: true)
205
+ raise InvalidArgument, 'Invalid key path' unless res
206
+
207
+ real_path.push(path).concat(paths)
208
+ Doing.logger.debug('Config:', "translated key path #{keypath} to #{real_path.join('.')}")
209
+ return real_path
175
210
  end
176
211
  cfg = new_cfg
177
212
  end
@@ -194,7 +229,7 @@ module Doing
194
229
  cfg = @settings
195
230
  real_path = ['config']
196
231
  unless keypath =~ /^[.*]?$/
197
- real_path = resolve_key_path(keypath)
232
+ real_path = resolve_key_path(keypath, create: false)
198
233
  return nil unless real_path&.count&.positive?
199
234
 
200
235
  cfg = cfg.dig(*real_path)
@@ -203,11 +238,15 @@ module Doing
203
238
  cfg.nil? ? nil : { real_path[-1] => cfg }
204
239
  end
205
240
 
206
- # It takes the input, fills in the defaults where values do not exist.
241
+ # It takes the input, fills in the defaults where values
242
+ # do not exist.
243
+ #
244
+ # @param user_config a Hash or Configuration of
245
+ # overrides.
207
246
  #
208
- # user_config - a Hash or Configuration of overrides.
247
+ # @return [Hash] a Configuration filled with
248
+ # defaults.
209
249
  #
210
- # Returns a Configuration filled with defaults.
211
250
  def from(user_config)
212
251
  Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
213
252
  end
data/lib/doing/item.rb CHANGED
@@ -7,7 +7,7 @@ module Doing
7
7
  class Item
8
8
  attr_accessor :date, :title, :section, :note
9
9
 
10
- attr_reader :id
10
+ # attr_reader :id
11
11
 
12
12
  ##
13
13
  ## Initialize an item with date, title, section, and
@@ -31,6 +31,13 @@ module Doing
31
31
  # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
32
32
  # end
33
33
 
34
+ ## If the entry doesn't have a @done date, return the elapsed time
35
+ def duration
36
+ return nil if @title =~ /(?<=^| )@done\b/
37
+
38
+ return Time.now - @date
39
+ end
40
+
34
41
  ##
35
42
  ## Get the difference between the item's start date and
36
43
  ## the value of its @done tag (if present)
@@ -109,7 +116,7 @@ module Doing
109
116
  ## Add (or remove) tags from the title of the item
110
117
  ##
111
118
  ## @param tags [Array] The tags to apply
112
- ## @param **options Additional options
119
+ ## @param options Additional options
113
120
  ##
114
121
  ## @option options :date [Boolean] Include timestamp?
115
122
  ## @option options :single [Boolean] Log as a single change?
@@ -155,6 +162,10 @@ module Doing
155
162
  @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
156
163
  end
157
164
 
165
+ def tag_array
166
+ tags.tags_to_array
167
+ end
168
+
158
169
  ##
159
170
  ## Test if item contains tag(s)
160
171
  ##
@@ -165,6 +176,13 @@ module Doing
165
176
  ## @return [Boolean] true if tag/bool combination passes
166
177
  ##
167
178
  def tags?(tags, bool = :and, negate: false)
179
+ if bool == :pattern
180
+ tags = tags.join(' ') if tags.is_a?(Array)
181
+ matches = tag_pattern?(tags.gsub(/ *, */, ' '))
182
+
183
+ return negate ? !matches : matches
184
+ end
185
+
168
186
  tags = split_tags(tags)
169
187
  bool = bool.normalize_bool
170
188
 
@@ -179,6 +197,10 @@ module Doing
179
197
  negate ? !matches : matches
180
198
  end
181
199
 
200
+ def ignore_case(search, case_type)
201
+ (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
202
+ end
203
+
182
204
  ##
183
205
  ## Test if item matches search string
184
206
  ##
@@ -190,9 +212,30 @@ module Doing
190
212
  ##
191
213
  ## @return [Boolean] matches search criteria
192
214
  ##
193
- def search(search, distance: 3, negate: false, case_type: :smart, fuzzy: false)
194
- text = @title + @note.to_s
195
- matches = text =~ search.to_rx(distance: distance, case_type: case_type)
215
+ def search(search, distance: nil, negate: false, case_type: nil)
216
+ prefs = Doing.config.settings['search'] || {}
217
+ matching = prefs.fetch('matching', 'pattern').normalize_matching
218
+ distance ||= prefs.fetch('distance', 3).to_i
219
+ case_type ||= prefs.fetch('case', 'smart').normalize_case
220
+
221
+ if search.is_rx? || matching == :fuzzy
222
+ matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
223
+ else
224
+ query = to_phrase_query(search.strip)
225
+
226
+ if query[:must].nil? && query[:must_not].nil?
227
+ query[:must] = query[:should]
228
+ query[:should] = []
229
+ end
230
+ matches = no_searches?(query[:must_not], case_type: case_type)
231
+ matches &&= all_searches?(query[:must], case_type: case_type)
232
+ matches &&= any_searches?(query[:should], case_type: case_type)
233
+ end
234
+ # if search =~ /(?<=\A| )[+-]\S/
235
+ # else
236
+ # text = @title + @note.to_s
237
+ # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
238
+ # end
196
239
 
197
240
  # if search.is_rx? || !fuzzy
198
241
  # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
@@ -279,33 +322,92 @@ module Doing
279
322
  start = @date
280
323
 
281
324
  t = (done - start).to_i
282
- t > 0 ? t : nil
325
+ t.positive? ? t : nil
326
+ end
327
+
328
+ def all_searches?(searches, case_type: :smart)
329
+ return true if searches.nil? || searches.empty?
330
+
331
+ text = @title + @note.to_s
332
+ searches.each do |s|
333
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
334
+ return false unless text =~ rx
335
+ end
336
+ true
337
+ end
338
+
339
+ def no_searches?(searches, case_type: :smart)
340
+ return true if searches.nil? || searches.empty?
341
+
342
+ text = @title + @note.to_s
343
+ searches.each do |s|
344
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
345
+ return false if text =~ rx
346
+ end
347
+ true
348
+ end
349
+
350
+ def any_searches?(searches, case_type: :smart)
351
+ return true if searches.nil? || searches.empty?
352
+
353
+ text = @title + @note.to_s
354
+ searches.each do |s|
355
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
356
+ return true if text =~ rx
357
+ end
358
+ false
283
359
  end
284
360
 
285
361
  def all_tags?(tags)
362
+ return true if tags.nil? || tags.empty?
363
+
286
364
  tags.each do |tag|
287
- return false unless @title =~ /@#{tag}/
365
+ return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
288
366
  end
289
367
  true
290
368
  end
291
369
 
292
370
  def no_tags?(tags)
371
+ return true if tags.nil? || tags.empty?
372
+
293
373
  tags.each do |tag|
294
- return false if @title =~ /@#{tag}/
374
+ return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
295
375
  end
296
376
  true
297
377
  end
298
378
 
299
379
  def any_tags?(tags)
380
+ return true if tags.nil? || tags.empty?
381
+
300
382
  tags.each do |tag|
301
- return true if @title =~ /@#{tag}/
383
+ return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
302
384
  end
303
385
  false
304
386
  end
305
387
 
388
+ def to_query(query)
389
+ parser = BooleanTermParser::QueryParser.new
390
+ transformer = BooleanTermParser::QueryTransformer.new
391
+ parse_tree = parser.parse(query)
392
+ transformer.apply(parse_tree).to_elasticsearch
393
+ end
394
+
395
+ def to_phrase_query(query)
396
+ parser = PhraseParser::QueryParser.new
397
+ transformer = PhraseParser::QueryTransformer.new
398
+ parse_tree = parser.parse(query)
399
+ transformer.apply(parse_tree).to_elasticsearch
400
+ end
401
+
402
+ def tag_pattern?(tags)
403
+ query = to_query(tags)
404
+
405
+ no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
406
+ end
407
+
306
408
  def split_tags(tags)
307
409
  tags = tags.split(/ *, */) if tags.is_a? String
308
- tags.map { |t| t.strip.sub(/^@/, '') }
410
+ tags.map { |t| t.strip.add_at }
309
411
  end
310
412
  end
311
413
  end
data/lib/doing/items.rb CHANGED
@@ -105,6 +105,12 @@ module Doing
105
105
  new_item
106
106
  end
107
107
 
108
+ def all_tags
109
+ each_with_object([]) do |entry, tags|
110
+ tags.concat(entry.tags).sort!.uniq!
111
+ end
112
+ end
113
+
108
114
  # Output sections and items in Doing file format
109
115
  def to_s
110
116
  out = []
@@ -38,6 +38,7 @@ module Doing
38
38
  rotated
39
39
  skipped
40
40
  updated
41
+ exported
41
42
  ].freeze
42
43
 
43
44
  #
@@ -265,6 +266,31 @@ module Doing
265
266
  end
266
267
  end
267
268
 
269
+ def benchmark(key, state)
270
+ return unless ENV['DOING_BENCHMARK']
271
+
272
+ @benchmarks ||= {}
273
+ @benchmarks[key] ||= { start: nil, finish: nil }
274
+ @benchmarks[key][state] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
275
+ end
276
+
277
+ def log_benchmarks
278
+ if ENV['DOING_BENCHMARK']
279
+ output = []
280
+ @benchmarks.each do |k, timers|
281
+ if timers[:finish] && timers[:start]
282
+ output << "#{k}: #{timers[:finish] - timers[:start]}"
283
+ else
284
+ output << "#{k}: error"
285
+ end
286
+ end
287
+ output.each do |msg|
288
+ $stdout.puts color_message(:debug, 'Benchmark:', msg)
289
+ end
290
+ end
291
+ end
292
+
293
+
268
294
  def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
269
295
  if tags_added.empty? && tags_removed.empty?
270
296
  count(:skipped, level: :debug, message: '%count %items with no change', count: count)
@@ -319,6 +345,8 @@ module Doing
319
345
  ['Archived:', data[:message] || 'completed and archived %count %items']
320
346
  when :skipped
321
347
  ['Skipped:', data[:message] || '%count %items were unchanged']
348
+ when :exported
349
+ ['Exported:', data[:message] || '%count %items were exported']
322
350
  end
323
351
  end
324
352