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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +19 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +58 -8
- data/Gemfile.lock +25 -1
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/bin/doing +447 -149
- data/doc/Array.html +63 -1
- data/doc/BooleanTermParser/Clause.html +293 -0
- data/doc/BooleanTermParser/Operator.html +172 -0
- data/doc/BooleanTermParser/Query.html +417 -0
- data/doc/BooleanTermParser/QueryParser.html +135 -0
- data/doc/BooleanTermParser/QueryTransformer.html +124 -0
- data/doc/BooleanTermParser.html +115 -0
- data/doc/Doing/CLIFormat.html +131 -0
- data/doc/Doing/Color.html +2 -2
- data/doc/Doing/Completion.html +1 -1
- data/doc/Doing/Configuration.html +168 -73
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +177 -86
- data/doc/Doing/Items.html +36 -2
- data/doc/Doing/LogAdapter.html +70 -1
- data/doc/Doing/Note.html +5 -134
- data/doc/Doing/Pager.html +1 -1
- data/doc/Doing/Plugins.html +380 -40
- data/doc/Doing/Prompt.html +70 -18
- data/doc/Doing/Section.html +1 -1
- data/doc/Doing/TemplateString.html +713 -0
- data/doc/Doing/Util/Backup.html +686 -0
- data/doc/Doing/Util.html +16 -4
- data/doc/Doing/WWID.html +133 -73
- data/doc/Doing.html +4 -4
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +1 -1
- data/doc/PhraseParser/Operator.html +172 -0
- data/doc/PhraseParser/PhraseClause.html +303 -0
- data/doc/PhraseParser/Query.html +495 -0
- data/doc/PhraseParser/QueryParser.html +136 -0
- data/doc/PhraseParser/QueryTransformer.html +124 -0
- data/doc/PhraseParser/TermClause.html +293 -0
- data/doc/PhraseParser.html +115 -0
- data/doc/Status.html +1 -1
- data/doc/String.html +319 -13
- data/doc/Symbol.html +35 -1
- data/doc/Time.html +70 -2
- data/doc/_index.html +132 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +2 -2
- data/doc/index.html +2 -2
- data/doc/method_list.html +648 -160
- data/doc/top-level-namespace.html +2 -2
- data/doing.gemspec +3 -0
- data/doing.rdoc +263 -82
- data/lib/completion/doing.bash +18 -18
- data/lib/doing/array.rb +9 -0
- data/lib/doing/boolean_term_parser.rb +86 -0
- data/lib/doing/configuration.rb +63 -24
- data/lib/doing/item.rb +112 -10
- data/lib/doing/items.rb +6 -0
- data/lib/doing/log_adapter.rb +28 -0
- data/lib/doing/note.rb +31 -30
- data/lib/doing/phrase_parser.rb +124 -0
- data/lib/doing/plugin_manager.rb +57 -13
- data/lib/doing/plugins/export/dayone_export.rb +209 -0
- data/lib/doing/plugins/export/template_export.rb +113 -81
- data/lib/doing/prompt.rb +26 -13
- data/lib/doing/string.rb +114 -29
- data/lib/doing/string_chronify.rb +5 -1
- data/lib/doing/symbol.rb +4 -0
- data/lib/doing/template_string.rb +197 -0
- data/lib/doing/time.rb +32 -0
- data/lib/doing/util.rb +6 -7
- data/lib/doing/util_backup.rb +287 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +152 -55
- data/lib/doing.rb +9 -0
- data/lib/templates/doing-dayone-entry.erb +6 -0
- data/lib/templates/doing-dayone.erb +5 -0
- metadata +85 -2
data/lib/completion/doing.bash
CHANGED
|
@@ -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
|
data/lib/doing/configuration.rb
CHANGED
|
@@ -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' => '
|
|
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' => '%
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
136
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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:
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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.
|
|
410
|
+
tags.map { |t| t.strip.add_at }
|
|
309
411
|
end
|
|
310
412
|
end
|
|
311
413
|
end
|
data/lib/doing/items.rb
CHANGED
data/lib/doing/log_adapter.rb
CHANGED
|
@@ -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
|
|