doing 2.1.39 → 2.1.42
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +4 -4
- data/bin/commands/again.rb +1 -3
- data/bin/commands/changes.rb +50 -34
- data/bin/commands/commands.rb +77 -52
- data/bin/commands/commands_accepting.rb +57 -53
- data/bin/commands/config.rb +45 -36
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +90 -59
- data/bin/commands/flag.rb +5 -1
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/now.rb +151 -107
- data/bin/commands/on.rb +8 -18
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +6 -17
- data/bin/commands/since.rb +1 -12
- data/bin/commands/tag_dir.rb +49 -15
- data/bin/commands/today.rb +2 -13
- data/bin/commands/undo.rb +4 -6
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +15 -8
- data/{Dockerfile → docker/Dockerfile} +3 -1
- data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
- data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
- data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
- data/{bash_profile → docker/bash_profile} +0 -0
- data/{inputrc → docker/inputrc} +0 -0
- data/docs/doc/Array.html +85 -2
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/ArrayNestedHash.html +198 -0
- data/docs/doc/Doing/ArrayTags.html +424 -0
- data/docs/doc/Doing/CSVExport.html +266 -0
- data/docs/doc/Doing/CalendarImport.html +232 -0
- data/docs/doc/Doing/Change.html +617 -0
- data/docs/doc/Doing/Changes.html +468 -0
- data/docs/doc/Doing/ChronifyArray.html +347 -0
- data/docs/doc/Doing/ChronifyNumeric.html +271 -0
- data/docs/doc/Doing/ChronifyString.html +682 -0
- data/docs/doc/Doing/Color.html +167 -21
- data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
- data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
- data/docs/doc/Doing/Completion/StringUtils.html +229 -0
- data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
- data/docs/doc/Doing/Completion.html +17 -3
- data/docs/doc/Doing/Configuration.html +3 -2
- data/docs/doc/Doing/DayOneRenderer.html +383 -0
- data/docs/doc/Doing/DayoneExport.html +290 -0
- data/docs/doc/Doing/DoingImport.html +391 -0
- data/docs/doc/Doing/Entry.html +381 -0
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/HTMLExport.html +256 -0
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +179 -1660
- data/docs/doc/Doing/ItemDates.html +564 -0
- data/docs/doc/Doing/ItemQuery.html +614 -0
- data/docs/doc/Doing/ItemState.html +387 -0
- data/docs/doc/Doing/ItemTags.html +498 -0
- data/docs/doc/Doing/Items.html +581 -15
- data/docs/doc/Doing/JSONExport.html +222 -0
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/MarkdownExport.html +266 -0
- data/docs/doc/Doing/MarkdownRenderer.html +383 -0
- data/docs/doc/Doing/Note.html +18 -4
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +181 -76
- data/docs/doc/Doing/Prompt.html +32 -683
- data/docs/doc/Doing/PromptChoose.html +484 -0
- data/docs/doc/Doing/PromptFZF.html +391 -0
- data/docs/doc/Doing/PromptInput.html +572 -0
- data/docs/doc/Doing/PromptSTD.html +293 -0
- data/docs/doc/Doing/PromptYN.html +237 -0
- data/docs/doc/Doing/Section.html +58 -2
- data/docs/doc/Doing/StringHighlight.html +533 -0
- data/docs/doc/Doing/StringNormalize.html +929 -0
- data/docs/doc/Doing/StringQuery.html +725 -0
- data/docs/doc/Doing/StringTags.html +884 -0
- data/docs/doc/Doing/StringTransform.html +599 -0
- data/docs/doc/Doing/StringTruncate.html +448 -0
- data/docs/doc/Doing/StringURL.html +409 -0
- data/docs/doc/Doing/SymbolNormalize.html +341 -0
- data/docs/doc/Doing/TaskPaperExport.html +222 -0
- data/docs/doc/Doing/TemplateExport.html +249 -0
- data/docs/doc/Doing/TemplateString.html +102 -3
- data/docs/doc/Doing/TimingImport.html +285 -0
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +11 -163
- data/docs/doc/Doing/Util.html +67 -10
- data/docs/doc/Doing/Version.html +523 -0
- data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
- data/docs/doc/Doing/WWID.html +476 -139
- data/docs/doc/Doing/WWIDDisplay.html +865 -0
- data/docs/doc/Doing/WWIDEditor.html +466 -0
- data/docs/doc/Doing/WWIDFileTools.html +359 -0
- data/docs/doc/Doing/WWIDFilter.html +466 -0
- data/docs/doc/Doing/WWIDGuess.html +299 -0
- data/docs/doc/Doing/WWIDInteractive.html +752 -0
- data/docs/doc/Doing/WWIDModify.html +1078 -0
- data/docs/doc/Doing/WWIDTags.html +302 -0
- data/docs/doc/Doing/WWIDTimers.html +359 -0
- data/docs/doc/Doing/WWIDUtil.html +510 -0
- data/docs/doc/Doing.html +9 -6
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +23 -78
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +58 -633
- data/docs/doc/Symbol.html +9 -224
- data/docs/doc/Time.html +119 -13
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +348 -4
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +1904 -592
- data/docs/doc/top-level-namespace.html +12 -4
- data/docs/index.md +1 -1
- data/doing.rdoc +67 -15
- data/lib/completion/_doing.zsh +6 -6
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +10 -3
- data/lib/doing/add_options.rb +39 -1
- data/lib/doing/array/array.rb +18 -12
- data/lib/doing/array/cleanup.rb +31 -0
- data/lib/doing/array/nested_hash.rb +1 -1
- data/lib/doing/array/tags.rb +6 -5
- data/lib/doing/changelog/changelog.rb +6 -0
- data/lib/doing/chronify/array.rb +65 -25
- data/lib/doing/chronify/chronify.rb +12 -0
- data/lib/doing/chronify/numeric.rb +3 -2
- data/lib/doing/chronify/string.rb +1 -1
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion/completion_string.rb +25 -0
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/configuration.rb +7 -3
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/good.rb +8 -0
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/item.rb +128 -0
- data/lib/doing/{item.rb → item/query.rb} +2 -353
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/items/filter.rb +67 -0
- data/lib/doing/items/items.rb +57 -0
- data/lib/doing/items/modify.rb +36 -0
- data/lib/doing/items/sections.rb +83 -0
- data/lib/doing/items/util.rb +74 -0
- data/lib/doing/normalize.rb +10 -2
- data/lib/doing/note.rb +1 -1
- data/lib/doing/pager.rb +9 -3
- data/lib/doing/plugin_manager.rb +33 -8
- data/lib/doing/plugins/export/markdown_export.rb +4 -2
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/prompt/choose.rb +118 -0
- data/lib/doing/prompt/fzf.rb +84 -0
- data/lib/doing/prompt/input.rb +129 -0
- data/lib/doing/prompt/prompt.rb +41 -0
- data/lib/doing/prompt/std.rb +32 -0
- data/lib/doing/prompt/yn.rb +64 -0
- data/lib/doing/section.rb +4 -0
- data/lib/doing/string/highlight.rb +1 -1
- data/lib/doing/string/query.rb +1 -1
- data/lib/doing/string/string.rb +18 -7
- data/lib/doing/string/tags.rb +14 -3
- data/lib/doing/string/transform.rb +7 -1
- data/lib/doing/string/truncate.rb +1 -1
- data/lib/doing/string/url.rb +1 -1
- data/lib/doing/time.rb +19 -1
- data/lib/doing/util.rb +12 -6
- data/lib/doing/util_backup.rb +62 -57
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +396 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +183 -0
- data/lib/doing/wwid/filter.rb +226 -0
- data/lib/doing/wwid/guess.rb +85 -0
- data/lib/doing/wwid/interactive.rb +377 -0
- data/lib/doing/wwid/modify.rb +617 -0
- data/lib/doing/wwid/tags.rb +51 -0
- data/lib/doing/wwid/timers.rb +342 -0
- data/lib/doing/wwid/wwid.rb +121 -0
- data/lib/doing/wwid/wwidutil.rb +101 -0
- data/lib/doing.rb +7 -7
- data/lib/helpers/threaded_tests.rb +1 -0
- metadata +94 -14
- data/lib/doing/changelog.rb +0 -6
- data/lib/doing/completion/string.rb +0 -17
- data/lib/doing/items.rb +0 -196
- data/lib/doing/prompt.rb +0 -330
- data/lib/doing/wwid.rb +0 -2398
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# A Doing entry
|
5
|
+
module ItemTags
|
6
|
+
##
|
7
|
+
## Add (or remove) tags from the title of the item
|
8
|
+
##
|
9
|
+
## @param tags [Array] The tags to apply
|
10
|
+
## @param options Additional options
|
11
|
+
##
|
12
|
+
## @option options :date [Boolean] Include timestamp?
|
13
|
+
## @option options :single [Boolean] Log as a single change?
|
14
|
+
## @option options :value [String] A value to include as @tag(value)
|
15
|
+
## @option options :remove [Boolean] if true remove instead of adding
|
16
|
+
## @option options :rename_to [String] if not nil, rename target tag to this tag name
|
17
|
+
## @option options :regex [Boolean] treat target tag string as regex pattern
|
18
|
+
## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
|
19
|
+
##
|
20
|
+
def tag(tags, **options)
|
21
|
+
added = []
|
22
|
+
removed = []
|
23
|
+
|
24
|
+
date = options.fetch(:date, false)
|
25
|
+
options[:value] ||= date ? Time.now.strftime('%F %R') : nil
|
26
|
+
options.delete(:date)
|
27
|
+
|
28
|
+
single = options.fetch(:single, false)
|
29
|
+
options.delete(:single)
|
30
|
+
|
31
|
+
tags = tags.to_tags if tags.is_a? ::String
|
32
|
+
|
33
|
+
remove = options.fetch(:remove, false)
|
34
|
+
tags.each do |tag|
|
35
|
+
if tag =~ /^(\S+)\((.*?)\)$/
|
36
|
+
m = Regexp.last_match
|
37
|
+
tag = m[1]
|
38
|
+
options[:value] ||= m[2]
|
39
|
+
end
|
40
|
+
|
41
|
+
bool = remove ? :and : :not
|
42
|
+
if tags?(tag, bool) || options[:value]
|
43
|
+
@title = @title.tag(tag, **options).strip
|
44
|
+
remove ? removed.push(tag) : added.push(tag)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
|
49
|
+
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
## Get a list of tags on the item
|
55
|
+
##
|
56
|
+
## @return [Array] array of tags (no values)
|
57
|
+
##
|
58
|
+
def tags
|
59
|
+
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
## Return all tags including parenthetical values
|
64
|
+
##
|
65
|
+
## @return [Array<Array>] Array of array pairs,
|
66
|
+
## [[tag1, value], [tag2, value]]
|
67
|
+
##
|
68
|
+
def tags_with_values
|
69
|
+
@title.scan(/(?<= |\A)@([^\s(]+)(?:\((.*?)\))?/).map { |tag| [tag[0], tag[1]] }.sort.uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
## convert tags on item to an array with @ symbols removed
|
74
|
+
##
|
75
|
+
## @return [Array] array of tags
|
76
|
+
##
|
77
|
+
def tag_array
|
78
|
+
tags.tags_to_array
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def split_tags(tags)
|
84
|
+
tags.to_tags.tags_to_array
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class Items < Array
|
5
|
+
# Get a new Items object containing only items in a
|
6
|
+
# specified section
|
7
|
+
#
|
8
|
+
# @param section [String] section title
|
9
|
+
#
|
10
|
+
# @return [Items] Array of items
|
11
|
+
#
|
12
|
+
def in_section(section)
|
13
|
+
if section =~ /^all$/i
|
14
|
+
dup
|
15
|
+
else
|
16
|
+
items = Items.new.concat(select { |item| !item.nil? && item.section == section })
|
17
|
+
items.add_section(section, log: false)
|
18
|
+
items
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
## Search Items for a string (title and note)
|
24
|
+
##
|
25
|
+
## @param query [String] The query
|
26
|
+
## @param case_type [Symbol] The case type
|
27
|
+
## (:smart, :sensitive, :ignore)
|
28
|
+
##
|
29
|
+
## @return [Items] array of items matching search
|
30
|
+
##
|
31
|
+
def search(query, case_type: :smart)
|
32
|
+
WWID.new.fuzzy_filter_items(self, query, case_type: case_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
## Search items by tags
|
37
|
+
##
|
38
|
+
## @param tags [Array,String] The tags by which to
|
39
|
+
## filter
|
40
|
+
## @param bool [Symbol] The bool with which to
|
41
|
+
## combine multiple tags
|
42
|
+
##
|
43
|
+
## @return [Items] array of items matching tag filter
|
44
|
+
##
|
45
|
+
def tagged(tags, bool: :and)
|
46
|
+
WWID.new.filter_items(self, opt: { tag: tags, bool: bool })
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
## Filter Items by date. String arguments will be
|
51
|
+
## chronified
|
52
|
+
##
|
53
|
+
## @param start [Time,String] Filter items after
|
54
|
+
## this date
|
55
|
+
## @param finish [Time,String] Filter items before
|
56
|
+
## this date
|
57
|
+
##
|
58
|
+
## @return [Items] array of items with dates between
|
59
|
+
## targets
|
60
|
+
##
|
61
|
+
def between_dates(start, finish)
|
62
|
+
start = start.chronify(guess: :begin, future: false) if start.is_a?(String)
|
63
|
+
finish = finish.chronify(guess: :end) if finish.is_a?(String)
|
64
|
+
WWID.new.filter_items(self, opt: { date_filter: [start, finish] })
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'filter'
|
4
|
+
require_relative 'modify'
|
5
|
+
require_relative 'sections'
|
6
|
+
require_relative 'util'
|
7
|
+
|
8
|
+
module Doing
|
9
|
+
# A collection of Item objects
|
10
|
+
class Items < Array
|
11
|
+
attr_accessor :sections
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
@sections = []
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
## Test if self includes Item
|
20
|
+
##
|
21
|
+
## @param item [Item] The item to search for
|
22
|
+
## @param match_section [Boolean] Section must match
|
23
|
+
##
|
24
|
+
## @return [Boolean] True if Item exists
|
25
|
+
##
|
26
|
+
def include?(item, match_section: true)
|
27
|
+
includes = false
|
28
|
+
each do |other_item|
|
29
|
+
if other_item.equal?(item, match_section: match_section)
|
30
|
+
includes = true
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
includes
|
36
|
+
end
|
37
|
+
|
38
|
+
# Output sections and items in Doing file format
|
39
|
+
def to_s
|
40
|
+
out = []
|
41
|
+
@sections.each do |section|
|
42
|
+
out.push(section.original)
|
43
|
+
items = in_section(section.title).sort_by { |i| [i.date, i.title] }
|
44
|
+
items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
|
45
|
+
items.each { |item| out.push(item.to_s) }
|
46
|
+
end
|
47
|
+
|
48
|
+
out.join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
# @private
|
52
|
+
def inspect
|
53
|
+
sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
|
54
|
+
"#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class Items < Array
|
5
|
+
##
|
6
|
+
## Delete an item from the index
|
7
|
+
##
|
8
|
+
## @param item The item
|
9
|
+
##
|
10
|
+
def delete_item(item, single: false)
|
11
|
+
deleted = delete(item)
|
12
|
+
Doing.logger.count(:deleted)
|
13
|
+
Doing.logger.info('Entry deleted:', deleted.title) if single
|
14
|
+
deleted
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
## Update an item in the index with a modified item
|
19
|
+
##
|
20
|
+
## @param old_item The old item
|
21
|
+
## @param new_item The new item
|
22
|
+
##
|
23
|
+
def update_item(old_item, new_item)
|
24
|
+
s_idx = index { |item| item.equal?(old_item) }
|
25
|
+
|
26
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
27
|
+
|
28
|
+
return if fetch(s_idx).equal?(new_item)
|
29
|
+
|
30
|
+
self[s_idx] = new_item
|
31
|
+
Doing.logger.count(:updated)
|
32
|
+
Doing.logger.info('Entry updated:', self[s_idx].title.trunc(60))
|
33
|
+
new_item
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class Items < Array
|
5
|
+
# List sections, title only
|
6
|
+
#
|
7
|
+
# @return [Array] section titles
|
8
|
+
#
|
9
|
+
def section_titles
|
10
|
+
@sections.map(&:title)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Test if section already exists
|
14
|
+
#
|
15
|
+
# @param section [String] section title
|
16
|
+
#
|
17
|
+
# @return [Boolean] true if section exists
|
18
|
+
#
|
19
|
+
def section?(section)
|
20
|
+
section = section.is_a?(Section) ? section.title.downcase : section.downcase
|
21
|
+
@sections.map { |i| i.title.downcase }.include?(section)
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
## Return the best section match for a search query
|
26
|
+
##
|
27
|
+
## @param frag The search query
|
28
|
+
## @param distance The distance apart characters can be (fuzziness)
|
29
|
+
##
|
30
|
+
## @return [Section] (first) matching section object
|
31
|
+
##
|
32
|
+
def guess_section(frag, distance: 2)
|
33
|
+
section = nil
|
34
|
+
re = frag.to_rx(distance: distance, case_type: :ignore)
|
35
|
+
@sections.each do |sect|
|
36
|
+
next unless sect.title =~ /#{re}/i
|
37
|
+
|
38
|
+
Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
|
39
|
+
section = sect
|
40
|
+
break
|
41
|
+
end
|
42
|
+
|
43
|
+
section
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add a new section to the sections array. Accepts
|
47
|
+
# either a Section object, or a title string that will
|
48
|
+
# be converted into a Section.
|
49
|
+
#
|
50
|
+
# @param section [Section] The section to add. A
|
51
|
+
# String value will be converted to
|
52
|
+
# Section automatically.
|
53
|
+
# @param log [Boolean] Add a log message
|
54
|
+
# notifying the user about the
|
55
|
+
# creation of the section.
|
56
|
+
#
|
57
|
+
# @return nothing
|
58
|
+
#
|
59
|
+
def add_section(section, log: false)
|
60
|
+
section = section.is_a?(Section) ? section : Section.new(section.cap_first)
|
61
|
+
|
62
|
+
return if section?(section)
|
63
|
+
|
64
|
+
@sections.push(section)
|
65
|
+
Doing.logger.info('New section:', %("#{section}" added)) if log
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete_section(section, log: false)
|
69
|
+
return unless section?(section)
|
70
|
+
|
71
|
+
raise DoingRuntimeError, 'Section not empty' if in_section(section).count.positive?
|
72
|
+
|
73
|
+
@sections.each do |sect|
|
74
|
+
next unless sect.title == section && in_section(sect).count.zero?
|
75
|
+
|
76
|
+
@sections.delete(sect)
|
77
|
+
Doing.logger.info('Removed section:', %("#{section}" removed)) if log
|
78
|
+
end
|
79
|
+
|
80
|
+
Doing.logger.error('Not found:', %("#{section}" not found))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class Items < Array
|
5
|
+
# # Create a deep copy of Items
|
6
|
+
# def clone
|
7
|
+
# Marshal.load(Marshal.dump(self))
|
8
|
+
# end
|
9
|
+
|
10
|
+
def delete(item)
|
11
|
+
deleted = nil
|
12
|
+
each_with_index do |i, idx|
|
13
|
+
if i.equal?(item, match_section: true)
|
14
|
+
deleted = delete_at(idx)
|
15
|
+
break
|
16
|
+
end
|
17
|
+
end
|
18
|
+
deleted
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
## Get all tags on Items in self
|
23
|
+
##
|
24
|
+
## @return [Array] array of tags
|
25
|
+
##
|
26
|
+
def all_tags
|
27
|
+
each_with_object([]) do |entry, tags|
|
28
|
+
tags.concat(entry.tags).sort!.uniq!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
## Return Items containing items that don't exist in
|
34
|
+
## receiver
|
35
|
+
##
|
36
|
+
## @param items [Items] Receiver
|
37
|
+
##
|
38
|
+
## @return [Hash] Hash of added and deleted items
|
39
|
+
##
|
40
|
+
def diff(items)
|
41
|
+
a = clone
|
42
|
+
b = items.clone
|
43
|
+
|
44
|
+
a.delete_if do |item|
|
45
|
+
if b.include?(item)
|
46
|
+
b.delete(item)
|
47
|
+
true
|
48
|
+
else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
{ added: b, deleted: a }
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
|
57
|
+
##
|
58
|
+
## @return [Items] Items array with duplicate entries removed
|
59
|
+
##
|
60
|
+
def dedup(match_section: true)
|
61
|
+
unique = Items.new
|
62
|
+
each do |item|
|
63
|
+
unique.push(item) unless unique.include?(item, match_section: match_section)
|
64
|
+
end
|
65
|
+
|
66
|
+
unique
|
67
|
+
end
|
68
|
+
|
69
|
+
# @see #dedup
|
70
|
+
def dedup!(match_section: true)
|
71
|
+
replace dedup(match_section: match_section)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/doing/normalize.rb
CHANGED
@@ -4,7 +4,7 @@ module Doing
|
|
4
4
|
##
|
5
5
|
## String to symbol conversion
|
6
6
|
##
|
7
|
-
|
7
|
+
module StringNormalize
|
8
8
|
##
|
9
9
|
## Convert tag sort string to a qualified type
|
10
10
|
##
|
@@ -160,7 +160,7 @@ module Doing
|
|
160
160
|
##
|
161
161
|
## Symbol helpers
|
162
162
|
##
|
163
|
-
|
163
|
+
module SymbolNormalize
|
164
164
|
def normalize_tag_sort(default = :name)
|
165
165
|
to_s.normalize_tag_sort
|
166
166
|
end
|
@@ -186,3 +186,11 @@ module Doing
|
|
186
186
|
end
|
187
187
|
end
|
188
188
|
end
|
189
|
+
|
190
|
+
class ::String
|
191
|
+
include Doing::StringNormalize
|
192
|
+
end
|
193
|
+
|
194
|
+
class ::Symbol
|
195
|
+
include Doing::SymbolNormalize
|
196
|
+
end
|
data/lib/doing/note.rb
CHANGED
@@ -20,7 +20,7 @@ module Doing
|
|
20
20
|
##
|
21
21
|
## Add note contents, optionally replacing existing note
|
22
22
|
##
|
23
|
-
## @param note [Array] The note to add, can be
|
23
|
+
## @param note [Array|String|Note] The note to add, can be
|
24
24
|
## String, Array, or Note
|
25
25
|
## @param replace [Boolean] replace existing
|
26
26
|
## content
|
data/lib/doing/pager.rb
CHANGED
@@ -70,14 +70,20 @@ module Doing
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def pagers
|
73
|
-
[
|
74
|
-
|
73
|
+
[
|
74
|
+
Doing.setting('editors.pager'),
|
75
|
+
ENV['PAGER'],
|
76
|
+
'less -Xr',
|
77
|
+
ENV['GIT_PAGER'],
|
78
|
+
git_pager,
|
79
|
+
'more -r'
|
80
|
+
].remove_bad
|
75
81
|
end
|
76
82
|
|
77
83
|
def find_executable(*commands)
|
78
84
|
execs = commands.empty? ? pagers : commands
|
79
85
|
execs
|
80
|
-
.
|
86
|
+
.remove_bad.uniq
|
81
87
|
.find { |cmd| TTY::Which.exist?(cmd.split.first) }
|
82
88
|
end
|
83
89
|
|
data/lib/doing/plugin_manager.rb
CHANGED
@@ -4,10 +4,17 @@ module Doing
|
|
4
4
|
# Plugin handling
|
5
5
|
module Plugins
|
6
6
|
class << self
|
7
|
+
# Return the user's home directory
|
7
8
|
def user_home
|
8
9
|
@user_home ||= Util.user_home
|
9
10
|
end
|
10
11
|
|
12
|
+
# Storage for registered plugins. Hash with :import
|
13
|
+
# and :export keys containing hashes of available
|
14
|
+
# plugins.
|
15
|
+
#
|
16
|
+
# @return [Hash] registered plugins
|
17
|
+
#
|
11
18
|
def plugins
|
12
19
|
@plugins ||= {
|
13
20
|
import: {},
|
@@ -81,14 +88,25 @@ module Doing
|
|
81
88
|
Doing.logger.debug('Plugin Manager:', "Registered #{type} plugin \"#{title}\"")
|
82
89
|
end
|
83
90
|
|
91
|
+
##
|
92
|
+
## Verifies that a plugin is properly configured with
|
93
|
+
## necessary methods for its type. If the plugin fails
|
94
|
+
## validation, a PluginUncallable exception will be
|
95
|
+
## raised.
|
96
|
+
##
|
97
|
+
## @param title [String] The title
|
98
|
+
## @param type [Symbol] type, :import or
|
99
|
+
## :export
|
100
|
+
## @param klass [Class] Plugin class
|
101
|
+
##
|
84
102
|
def validate_plugin(title, type, klass)
|
85
103
|
type = valid_type(type)
|
86
104
|
if type == :import && !klass.respond_to?(:import)
|
87
|
-
raise Errors::PluginUncallable.new('Import plugins must respond to :import', type
|
105
|
+
raise Errors::PluginUncallable.new('Import plugins must respond to :import', type, title)
|
88
106
|
end
|
89
107
|
|
90
108
|
if type == :export && !klass.respond_to?(:render)
|
91
|
-
raise Errors::PluginUncallable.new('Export plugins must respond to :render', type
|
109
|
+
raise Errors::PluginUncallable.new('Export plugins must respond to :render', type, title)
|
92
110
|
end
|
93
111
|
|
94
112
|
type
|
@@ -113,7 +131,7 @@ module Doing
|
|
113
131
|
when /^e(x(p(o(r(t)?)?)?)?)?$/
|
114
132
|
:export
|
115
133
|
else
|
116
|
-
raise Errors::InvalidPluginType
|
134
|
+
raise Errors::InvalidPluginType.new('Invalid plugin type', 'unrecognized')
|
117
135
|
end
|
118
136
|
|
119
137
|
type.to_sym
|
@@ -122,8 +140,10 @@ module Doing
|
|
122
140
|
##
|
123
141
|
## List available plugins to stdout
|
124
142
|
##
|
125
|
-
## @param options
|
143
|
+
## @param options [Hash] additional options
|
126
144
|
##
|
145
|
+
## @option options :column [Boolean] display results in a single column
|
146
|
+
## @option options :type [String] Plugin type: all, import, or export
|
127
147
|
def list_plugins(options = {})
|
128
148
|
separator = options[:column] ? "\n" : "\t"
|
129
149
|
type = options[:type].nil? || options[:type] =~ /all/i ? 'all' : valid_type(options[:type])
|
@@ -144,9 +164,9 @@ module Doing
|
|
144
164
|
##
|
145
165
|
## Return array of available plugin names
|
146
166
|
##
|
147
|
-
## @param type Plugin type (:import, :export)
|
167
|
+
## @param type [Symbol] Plugin type (:import, :export)
|
148
168
|
##
|
149
|
-
## @return [Array
|
169
|
+
## @return [Array] Array of plugin names (String)
|
150
170
|
##
|
151
171
|
def available_plugins(type: :export)
|
152
172
|
type = valid_type(type)
|
@@ -159,7 +179,7 @@ module Doing
|
|
159
179
|
## @param type Plugin type (:import, :export)
|
160
180
|
## @param separator The separator to join names with
|
161
181
|
##
|
162
|
-
## @return [String] Plugin names
|
182
|
+
## @return [String] Plugin names joined with separator
|
163
183
|
##
|
164
184
|
def plugin_names(type: :export, separator: '|')
|
165
185
|
type = valid_type(type)
|
@@ -190,7 +210,7 @@ module Doing
|
|
190
210
|
## @param type [Symbol] Plugin type (:import,
|
191
211
|
## :export)
|
192
212
|
##
|
193
|
-
## @return [Array
|
213
|
+
## @return [Array] Array of template names (String)
|
194
214
|
##
|
195
215
|
def plugin_templates(type: :export)
|
196
216
|
type = valid_type(type)
|
@@ -236,6 +256,9 @@ module Doing
|
|
236
256
|
## @param trigger [String] The trigger to test
|
237
257
|
## @param type [Symbol] the plugin type
|
238
258
|
## (:import, :export)
|
259
|
+
## @param save_to [String] if a path is
|
260
|
+
## specified, write the template
|
261
|
+
## to that path. Nil for STDOUT
|
239
262
|
##
|
240
263
|
## @return [String] string content of template for trigger
|
241
264
|
##
|
@@ -255,6 +278,8 @@ module Doing
|
|
255
278
|
raise Errors::InvalidArgument, "No template type matched \"#{trigger}\""
|
256
279
|
end
|
257
280
|
|
281
|
+
private
|
282
|
+
|
258
283
|
def save_template(tpl, dir, filename)
|
259
284
|
dir = File.expand_path(dir)
|
260
285
|
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
@@ -5,6 +5,7 @@
|
|
5
5
|
# author: Brett Terpstra
|
6
6
|
# url: https://brettterpstra.com
|
7
7
|
module Doing
|
8
|
+
# @private
|
8
9
|
class MarkdownRenderer
|
9
10
|
attr_accessor :items, :page_title, :totals
|
10
11
|
|
@@ -15,12 +16,13 @@ module Doing
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def get_binding
|
18
|
-
binding
|
19
|
+
binding
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
23
|
+
# Markdown Export Plugin
|
22
24
|
class MarkdownExport
|
23
|
-
include
|
25
|
+
include Util
|
24
26
|
|
25
27
|
def self.settings
|
26
28
|
{
|
@@ -17,7 +17,7 @@ module Doing
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.render(wwid, items, variables: {})
|
20
|
-
|
20
|
+
Doing.logger.benchmark(:template_render, :start)
|
21
21
|
return if items.nil?
|
22
22
|
|
23
23
|
opt = variables[:options]
|
@@ -126,18 +126,18 @@ module Doing
|
|
126
126
|
|
127
127
|
output.gsub!(/\\%/, '%')
|
128
128
|
|
129
|
-
output.highlight_search!(opt[:search]) if opt[:
|
129
|
+
output.highlight_search!(opt[:search]) if opt[:output] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
|
130
130
|
|
131
131
|
out += "#{output}\n"
|
132
132
|
end
|
133
133
|
|
134
|
-
# Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:
|
134
|
+
# Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:output]}")
|
135
135
|
if opt[:totals]
|
136
136
|
out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
|
137
137
|
sort_by: opt[:sort_tags],
|
138
138
|
sort_order: opt[:tag_order])
|
139
139
|
end
|
140
|
-
|
140
|
+
Doing.logger.benchmark(:template_render, :finish)
|
141
141
|
out
|
142
142
|
end
|
143
143
|
|
Binary file
|