doing 2.1.39 → 2.1.42
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/.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
|