doing 2.1.24 → 2.1.28
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 +17 -21
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +325 -102
- data/Dockerfile +5 -5
- data/Dockerfile-2.6 +5 -5
- data/Dockerfile-2.7 +5 -4
- data/Dockerfile-3.0 +5 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +3 -3
- data/bin/commands/add_section.rb +15 -0
- data/bin/commands/again.rb +57 -0
- data/bin/commands/archive.rb +55 -0
- data/bin/commands/cancel.rb +60 -0
- data/bin/commands/changes.rb +73 -0
- data/bin/commands/choose.rb +9 -0
- data/bin/commands/colors.rb +21 -0
- data/bin/commands/commands.rb +89 -0
- data/bin/commands/commands_accepting.rb +76 -0
- data/bin/commands/completion.rb +27 -0
- data/bin/commands/config.rb +245 -0
- data/bin/commands/done.rb +235 -0
- data/bin/commands/finish.rb +126 -0
- data/bin/commands/flag.rb +90 -0
- data/bin/commands/grep.rb +108 -0
- data/bin/commands/import.rb +71 -0
- data/bin/commands/install_fzf.rb +17 -0
- data/bin/commands/last.rb +81 -0
- data/bin/commands/meanwhile.rb +76 -0
- data/bin/commands/note.rb +91 -0
- data/bin/commands/now.rb +145 -0
- data/bin/commands/on.rb +65 -0
- data/bin/commands/open.rb +53 -0
- data/bin/commands/plugins.rb +23 -0
- data/bin/commands/recent.rb +77 -0
- data/bin/commands/redo.rb +26 -0
- data/bin/commands/reset.rb +73 -0
- data/bin/commands/rotate.rb +42 -0
- data/bin/commands/sections.rb +11 -0
- data/bin/commands/select.rb +105 -0
- data/bin/commands/show.rb +185 -0
- data/bin/commands/since.rb +63 -0
- data/bin/commands/tag.rb +149 -0
- data/bin/commands/tag_dir.rb +29 -0
- data/bin/commands/tags.rb +66 -0
- data/bin/commands/template.rb +61 -0
- data/bin/commands/today.rb +64 -0
- data/bin/commands/undo.rb +49 -0
- data/bin/commands/view.rb +201 -0
- data/bin/commands/views.rb +11 -0
- data/bin/commands/yesterday.rb +72 -0
- data/bin/doing +241 -3662
- data/docs/doc/Array.html +13 -449
- data/docs/doc/BooleanTermParser/Clause.html +5 -5
- data/docs/doc/BooleanTermParser/Operator.html +4 -4
- data/docs/doc/BooleanTermParser/Query.html +8 -8
- data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
- data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +65 -59
- data/docs/doc/Doing/Completion.html +2 -2
- data/docs/doc/Doing/Configuration.html +49 -16
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
- data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
- data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
- data/docs/doc/Doing/Errors/NoResults.html +2 -2
- data/docs/doc/Doing/Errors/PluginException.html +3 -3
- data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +6 -6
- data/docs/doc/Doing/Item.html +50 -16
- data/docs/doc/Doing/Items.html +10 -10
- data/docs/doc/Doing/LogAdapter.html +24 -24
- data/docs/doc/Doing/Note.html +7 -7
- data/docs/doc/Doing/Pager.html +4 -4
- data/docs/doc/Doing/Plugins.html +7 -7
- data/docs/doc/Doing/Prompt.html +59 -14
- data/docs/doc/Doing/Section.html +6 -6
- data/docs/doc/Doing/TemplateString.html +8 -8
- data/docs/doc/Doing/Types.html +46 -1
- data/docs/doc/Doing/Util/Backup.html +10 -10
- data/docs/doc/Doing/Util.html +15 -15
- data/docs/doc/Doing/WWID.html +73 -61
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +235 -0
- data/docs/doc/GLI/Commands/Help.html +3 -3
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +45 -11
- data/docs/doc/Numeric.html +5 -5
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +4 -4
- data/docs/doc/PhraseParser/PhraseClause.html +5 -5
- data/docs/doc/PhraseParser/Query.html +10 -10
- data/docs/doc/PhraseParser/QueryParser.html +2 -2
- data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
- data/docs/doc/PhraseParser/TermClause.html +5 -5
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +7 -7
- data/docs/doc/String.html +306 -3111
- data/docs/doc/Symbol.html +45 -11
- data/docs/doc/Time.html +6 -6
- data/docs/doc/TrueClass.html +235 -0
- data/docs/doc/_index.html +37 -19
- 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 +240 -576
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +297 -169
- data/example_plugin.rb +2 -2
- data/lib/completion/_doing.zsh +35 -31
- data/lib/completion/doing.bash +30 -19
- data/lib/completion/doing.fish +81 -67
- data/lib/doing/array/array.rb +4 -0
- data/lib/doing/array/nested_hash.rb +17 -0
- data/lib/doing/{array.rb → array/tags.rb} +7 -25
- data/lib/doing/changelog/change.rb +26 -11
- data/lib/doing/changelog/changes.rb +16 -4
- data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
- data/lib/doing/chronify/chronify.rb +5 -0
- data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
- data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
- data/lib/doing/colors.rb +115 -54
- data/lib/doing/configuration.rb +9 -6
- data/lib/doing/good.rb +72 -0
- data/lib/doing/hash.rb +4 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item.rb +19 -15
- data/lib/doing/items.rb +2 -2
- data/lib/doing/log_adapter.rb +35 -2
- data/lib/doing/normalize.rb +188 -0
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/plugins/export/dayone_export.rb +1 -1
- data/lib/doing/plugins/export/html_export.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +1 -1
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +3 -1
- data/lib/doing/plugins/import/calendar_import.rb +1 -1
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/plugins/import/timing_import.rb +1 -1
- data/lib/doing/prompt.rb +9 -3
- data/lib/doing/string/highlight.rb +95 -0
- data/lib/doing/string/query.rb +129 -0
- data/lib/doing/string/string.rb +12 -0
- data/lib/doing/string/tags.rb +164 -0
- data/lib/doing/string/transform.rb +168 -0
- data/lib/doing/string/truncate.rb +75 -0
- data/lib/doing/string/url.rb +82 -0
- data/lib/doing/template_string.rb +2 -24
- data/lib/doing/types.rb +9 -0
- data/lib/doing/util.rb +20 -16
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +91 -51
- data/lib/doing.rb +5 -6
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
- data/lib/helpers/threaded_tests.rb +69 -79
- data/lib/helpers/threaded_tests_string.rb +50 -0
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +65 -8
- data/lib/doing/string.rb +0 -765
- data/lib/doing/symbol.rb +0 -28
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
# Handling of @tags in strings
|
|
5
|
+
class ::String
|
|
6
|
+
##
|
|
7
|
+
## Add @ prefix to string if needed, maintains +/- prefix
|
|
8
|
+
##
|
|
9
|
+
## @return [String] @string
|
|
10
|
+
##
|
|
11
|
+
def add_at
|
|
12
|
+
strip.sub(/^([+-]*)@?/, '\1@')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
## Removes @ prefix if needed, maintains +/- prefix
|
|
17
|
+
##
|
|
18
|
+
## @return [String] string without @ prefix
|
|
19
|
+
##
|
|
20
|
+
def remove_at
|
|
21
|
+
strip.sub(/^([+-]*)@?/, '\1')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
## Convert a list of tags to an array. Tags can be with
|
|
26
|
+
## or without @ symbols, separated by any character, and
|
|
27
|
+
## can include parenthetical values (with spaces)
|
|
28
|
+
##
|
|
29
|
+
## @return [Array] array of tags including @ symbols
|
|
30
|
+
##
|
|
31
|
+
def to_tags
|
|
32
|
+
arr = gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
|
|
33
|
+
if block_given?
|
|
34
|
+
yield arr
|
|
35
|
+
else
|
|
36
|
+
arr
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
## @brief Adds tags to a string
|
|
42
|
+
##
|
|
43
|
+
## @param tags [String or Array] List of tags to add. @ symbol optional
|
|
44
|
+
## @param remove [Boolean] remove tags instead of adding
|
|
45
|
+
##
|
|
46
|
+
## @return [String] the tagged string
|
|
47
|
+
##
|
|
48
|
+
def add_tags(tags, remove: false)
|
|
49
|
+
title = dup
|
|
50
|
+
tags = tags.to_tags
|
|
51
|
+
tags.each { |tag| title.tag!(tag, remove: remove) }
|
|
52
|
+
title
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
## @see #add_tags
|
|
56
|
+
def add_tags!(tags, remove: false)
|
|
57
|
+
replace add_tags(tags, remove: remove)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
## Add, rename, or remove a tag in place
|
|
62
|
+
##
|
|
63
|
+
## @see #tag
|
|
64
|
+
##
|
|
65
|
+
def tag!(tag, **options)
|
|
66
|
+
replace tag(tag, **options)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
## Add, rename, or remove a tag
|
|
71
|
+
##
|
|
72
|
+
## @param tag The tag
|
|
73
|
+
## @param value [String] Value for tag (@tag(value))
|
|
74
|
+
## @param remove [Boolean] Remove the tag instead of adding
|
|
75
|
+
## @param rename_to [String] Replace tag with this tag
|
|
76
|
+
## @param regex [Boolean] Tag is regular expression
|
|
77
|
+
## @param single [Boolean] Operating on a single item (for logging)
|
|
78
|
+
## @param force [Boolean] With rename_to, add tag if it doesn't exist
|
|
79
|
+
##
|
|
80
|
+
## @return [String] The string with modified tags
|
|
81
|
+
##
|
|
82
|
+
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
|
|
83
|
+
log_level = single ? :info : :debug
|
|
84
|
+
title = dup
|
|
85
|
+
title.chomp!
|
|
86
|
+
tag = tag.sub(/^@?/, '')
|
|
87
|
+
case_sensitive = tag !~ /[A-Z]/
|
|
88
|
+
|
|
89
|
+
rx_tag = if regex
|
|
90
|
+
tag.gsub(/\./, '\S')
|
|
91
|
+
else
|
|
92
|
+
tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if remove || rename_to
|
|
96
|
+
rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
|
|
97
|
+
m = title.match(rx)
|
|
98
|
+
|
|
99
|
+
if m.nil? && rename_to && force
|
|
100
|
+
title.tag!(rename_to, value: value, single: single)
|
|
101
|
+
elsif m
|
|
102
|
+
title.gsub!(rx) do
|
|
103
|
+
rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
title.dedup_tags!
|
|
107
|
+
title.chomp!
|
|
108
|
+
|
|
109
|
+
if rename_to
|
|
110
|
+
f = "@#{tag}".cyan
|
|
111
|
+
t = "@#{rename_to}".cyan
|
|
112
|
+
Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
|
|
113
|
+
else
|
|
114
|
+
f = "@#{tag}".cyan
|
|
115
|
+
Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
|
|
119
|
+
end
|
|
120
|
+
elsif title =~ /@#{tag}(?=[ (]|$)/
|
|
121
|
+
Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
|
|
122
|
+
return title
|
|
123
|
+
else
|
|
124
|
+
add = tag
|
|
125
|
+
add += "(#{value})" unless value.nil?
|
|
126
|
+
title.chomp!
|
|
127
|
+
title += " @#{add}"
|
|
128
|
+
|
|
129
|
+
title.dedup_tags!
|
|
130
|
+
title.chomp!
|
|
131
|
+
Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
title.gsub(/ +/, ' ')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
## Remove duplicate tags, leaving only first occurrence
|
|
139
|
+
##
|
|
140
|
+
## @return Deduplicated string
|
|
141
|
+
##
|
|
142
|
+
def dedup_tags
|
|
143
|
+
title = dup
|
|
144
|
+
tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
|
|
145
|
+
tags.each do |tag|
|
|
146
|
+
found = false
|
|
147
|
+
title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m|
|
|
148
|
+
if found
|
|
149
|
+
''
|
|
150
|
+
else
|
|
151
|
+
found = true
|
|
152
|
+
m
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
title
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
## @see #dedup_tags
|
|
160
|
+
def dedup_tags!
|
|
161
|
+
replace dedup_tags
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
##
|
|
5
|
+
## String helpers
|
|
6
|
+
##
|
|
7
|
+
class ::String
|
|
8
|
+
# Compress multiple spaces to single space
|
|
9
|
+
def compress
|
|
10
|
+
gsub(/ +/, ' ').strip
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def compress!
|
|
14
|
+
replace compress
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def simple_wrap(width)
|
|
18
|
+
str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
|
|
19
|
+
words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
|
|
20
|
+
out = []
|
|
21
|
+
line = []
|
|
22
|
+
|
|
23
|
+
words.each do |word|
|
|
24
|
+
if word.uncolor.length >= width
|
|
25
|
+
chars = word.uncolor.split('')
|
|
26
|
+
out << chars.slice!(0, width - 1).join('') while chars.count >= width
|
|
27
|
+
line << chars.join('')
|
|
28
|
+
next
|
|
29
|
+
elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > width
|
|
30
|
+
out.push(line.join(' '))
|
|
31
|
+
line.clear
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
line << word.uncolor
|
|
35
|
+
end
|
|
36
|
+
out.push(line.join(' '))
|
|
37
|
+
out.join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
## Wrap string at word breaks, respecting tags
|
|
42
|
+
##
|
|
43
|
+
## @param len [Integer] The length
|
|
44
|
+
## @param offset [Integer] (Optional) The width to pad each subsequent line
|
|
45
|
+
## @param prefix [String] (Optional) A prefix to add to each line
|
|
46
|
+
##
|
|
47
|
+
def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
|
|
48
|
+
last_color = color.empty? ? '' : after.last_color
|
|
49
|
+
note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
|
|
50
|
+
note = ''
|
|
51
|
+
after = after.dup if after.frozen?
|
|
52
|
+
after.sub!(note_rx) do
|
|
53
|
+
note = Regexp.last_match(0)
|
|
54
|
+
''
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
left_pad = ' ' * offset
|
|
58
|
+
left_pad += indent
|
|
59
|
+
|
|
60
|
+
# return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?
|
|
61
|
+
|
|
62
|
+
# Don't break inside of tag values
|
|
63
|
+
str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
|
|
64
|
+
|
|
65
|
+
words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
|
|
66
|
+
out = []
|
|
67
|
+
line = []
|
|
68
|
+
|
|
69
|
+
words.each do |word|
|
|
70
|
+
if word.uncolor.length >= len
|
|
71
|
+
chars = word.uncolor.split('')
|
|
72
|
+
out << chars.slice!(0, len - 1).join('') while chars.count >= len
|
|
73
|
+
line << chars.join('')
|
|
74
|
+
next
|
|
75
|
+
elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > len
|
|
76
|
+
out.push(line.join(' '))
|
|
77
|
+
line.clear
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
line << word.uncolor
|
|
81
|
+
end
|
|
82
|
+
out.push(line.join(' '))
|
|
83
|
+
|
|
84
|
+
last_color = ''
|
|
85
|
+
out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
|
|
86
|
+
|
|
87
|
+
out.map.with_index { |l, idx|
|
|
88
|
+
if !pad_first && idx == 0
|
|
89
|
+
"#{color}#{prefix}#{l}#{last_color}"
|
|
90
|
+
else
|
|
91
|
+
"#{left_pad}#{color}#{prefix}#{l}#{last_color}"
|
|
92
|
+
end
|
|
93
|
+
}.join("\n") + " #{note}".chomp
|
|
94
|
+
# res.join("\n").strip + last_color + " #{note}".chomp
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
## Capitalize on the first character on string
|
|
99
|
+
##
|
|
100
|
+
## @return Capitalized string
|
|
101
|
+
##
|
|
102
|
+
def cap_first
|
|
103
|
+
sub(/^\w/) do |m|
|
|
104
|
+
m.upcase
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
## Pluralize a string based on quantity
|
|
110
|
+
##
|
|
111
|
+
## @param number [Integer] the quantity of the
|
|
112
|
+
## object the string represents
|
|
113
|
+
##
|
|
114
|
+
def to_p(number)
|
|
115
|
+
number == 1 ? self : "#{self}s"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
## Convert a string value to an appropriate type. If
|
|
120
|
+
## kind is not specified, '[one, two]' becomes an Array,
|
|
121
|
+
## '1' becomes Integer, '1.5' becomes Float, 'true' or
|
|
122
|
+
## 'yes' becomes TrueClass, 'false' or 'no' becomes
|
|
123
|
+
## FalseClass.
|
|
124
|
+
##
|
|
125
|
+
## @param kind [String] specify string, array,
|
|
126
|
+
## integer, float, symbol, or boolean
|
|
127
|
+
## (falls back to string if value is
|
|
128
|
+
## not recognized)
|
|
129
|
+
## @return Converted object type
|
|
130
|
+
def set_type(kind = nil)
|
|
131
|
+
if kind
|
|
132
|
+
case kind.to_s
|
|
133
|
+
when /^a/i
|
|
134
|
+
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
135
|
+
when /^i/i
|
|
136
|
+
to_i
|
|
137
|
+
when /^(fa|tr)/i
|
|
138
|
+
to_bool
|
|
139
|
+
when /^f/i
|
|
140
|
+
to_f
|
|
141
|
+
when /^sy/i
|
|
142
|
+
sub(/^:/, '').to_sym
|
|
143
|
+
when /^b/i
|
|
144
|
+
self =~ /^(true|yes)$/ ? true : false
|
|
145
|
+
else
|
|
146
|
+
to_s
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
case self
|
|
150
|
+
when /(^\[.*?\]$| *, *)/
|
|
151
|
+
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
152
|
+
when /^[0-9]+$/
|
|
153
|
+
to_i
|
|
154
|
+
when /^[0-9]+\.[0-9]+$/
|
|
155
|
+
to_f
|
|
156
|
+
when /^:\w+/
|
|
157
|
+
sub(/^:/, '').to_sym
|
|
158
|
+
when /^(true|yes)$/i
|
|
159
|
+
true
|
|
160
|
+
when /^(false|no)$/i
|
|
161
|
+
false
|
|
162
|
+
else
|
|
163
|
+
to_s
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
##
|
|
5
|
+
## String truncation
|
|
6
|
+
##
|
|
7
|
+
class ::String
|
|
8
|
+
##
|
|
9
|
+
## Truncate to nearest word
|
|
10
|
+
##
|
|
11
|
+
## @param len The length
|
|
12
|
+
##
|
|
13
|
+
def trunc(len, ellipsis: '...')
|
|
14
|
+
return self if length <= len
|
|
15
|
+
|
|
16
|
+
total = 0
|
|
17
|
+
res = []
|
|
18
|
+
|
|
19
|
+
split(/ /).each do |word|
|
|
20
|
+
break if total + 1 + word.length > len
|
|
21
|
+
|
|
22
|
+
total += 1 + word.length
|
|
23
|
+
res.push(word)
|
|
24
|
+
end
|
|
25
|
+
res.join(' ') + ellipsis
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def trunc!(len, ellipsis: '...')
|
|
29
|
+
replace trunc(len, ellipsis: ellipsis)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
## Truncate from middle to end at nearest word
|
|
34
|
+
##
|
|
35
|
+
## @param len The length
|
|
36
|
+
##
|
|
37
|
+
def truncend(len, ellipsis: '...')
|
|
38
|
+
return self if length <= len
|
|
39
|
+
|
|
40
|
+
total = 0
|
|
41
|
+
res = []
|
|
42
|
+
|
|
43
|
+
split(/ /).reverse.each do |word|
|
|
44
|
+
break if total + 1 + word.length > len
|
|
45
|
+
|
|
46
|
+
total += 1 + word.length
|
|
47
|
+
res.unshift(word)
|
|
48
|
+
end
|
|
49
|
+
ellipsis + res.join(' ')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def truncend!(len, ellipsis: '...')
|
|
53
|
+
replace truncend(len, ellipsis: ellipsis)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
## Truncate string in the middle, separating at nearest word
|
|
58
|
+
##
|
|
59
|
+
## @param len The length
|
|
60
|
+
## @param ellipsis The ellipsis
|
|
61
|
+
##
|
|
62
|
+
def truncmiddle(len, ellipsis: '...')
|
|
63
|
+
return self if length <= len
|
|
64
|
+
len -= (ellipsis.length / 2).to_i
|
|
65
|
+
half = (len / 2).to_i
|
|
66
|
+
start = trunc(half, ellipsis: ellipsis)
|
|
67
|
+
finish = truncend(half, ellipsis: '')
|
|
68
|
+
start + finish
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def truncmiddle!(len, ellipsis: '...')
|
|
72
|
+
replace truncmiddle(len, ellipsis: ellipsis)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
##
|
|
5
|
+
## URL linking and formatting
|
|
6
|
+
##
|
|
7
|
+
class ::String
|
|
8
|
+
##
|
|
9
|
+
## Turn raw urls into HTML links
|
|
10
|
+
##
|
|
11
|
+
## @param opt [Hash] Additional Options
|
|
12
|
+
##
|
|
13
|
+
## @option opt [Symbol] :format can be :markdown or
|
|
14
|
+
## :html (default)
|
|
15
|
+
##
|
|
16
|
+
def link_urls(**opt)
|
|
17
|
+
fmt = opt.fetch(:format, :html)
|
|
18
|
+
return self unless fmt
|
|
19
|
+
|
|
20
|
+
str = dup
|
|
21
|
+
|
|
22
|
+
str = str.remove_self_links if fmt == :markdown
|
|
23
|
+
|
|
24
|
+
str.replace_qualified_urls(format: fmt).clean_unlinked_urls
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
## @see #link_urls
|
|
28
|
+
def link_urls!(**opt)
|
|
29
|
+
fmt = opt.fetch(:format, :html)
|
|
30
|
+
replace link_urls(format: fmt)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Remove <self-linked> formatting
|
|
34
|
+
def remove_self_links
|
|
35
|
+
gsub(/<(.*?)>/) do |match|
|
|
36
|
+
m = Regexp.last_match
|
|
37
|
+
if m[1] =~ /^https?:/
|
|
38
|
+
m[1]
|
|
39
|
+
else
|
|
40
|
+
match
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Replace qualified urls
|
|
46
|
+
def replace_qualified_urls(**options)
|
|
47
|
+
fmt = options.fetch(:format, :html)
|
|
48
|
+
gsub(%r{(?mi)(?x:
|
|
49
|
+
(?<!["'\[(\\])
|
|
50
|
+
(?<protocol>(?:http|https)://)
|
|
51
|
+
(?<domain>[\w\-]+(?:\.[\w\-]+)+)
|
|
52
|
+
(?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
|
|
53
|
+
)}) do |_match|
|
|
54
|
+
m = Regexp.last_match
|
|
55
|
+
url = "#{m['domain']}#{m['path']}"
|
|
56
|
+
proto = m['protocol'].nil? ? 'http://' : m['protocol']
|
|
57
|
+
case fmt
|
|
58
|
+
when :terminal
|
|
59
|
+
TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
|
|
60
|
+
when :html
|
|
61
|
+
%(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
|
|
62
|
+
when :markdown
|
|
63
|
+
"[#{url}](#{proto}#{url})"
|
|
64
|
+
else
|
|
65
|
+
m[0]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Clean up unlinked <urls>
|
|
71
|
+
def clean_unlinked_urls
|
|
72
|
+
gsub(/<(\w+:.*?)>/) do |match|
|
|
73
|
+
m = Regexp.last_match
|
|
74
|
+
if m[1] =~ /<a href/
|
|
75
|
+
match
|
|
76
|
+
else
|
|
77
|
+
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -5,28 +5,6 @@ module Doing
|
|
|
5
5
|
## Template string formatting
|
|
6
6
|
##
|
|
7
7
|
class TemplateString < String
|
|
8
|
-
class ::String
|
|
9
|
-
##
|
|
10
|
-
## Extract the longest valid color from a string.
|
|
11
|
-
##
|
|
12
|
-
## Allows %colors to bleed into other text and still
|
|
13
|
-
## be recognized, e.g. %greensomething still finds
|
|
14
|
-
## %green.
|
|
15
|
-
##
|
|
16
|
-
## @return [String] a valid color name
|
|
17
|
-
## @api private
|
|
18
|
-
def validate_color
|
|
19
|
-
valid_color = nil
|
|
20
|
-
compiled = ''
|
|
21
|
-
split('').each do |char|
|
|
22
|
-
compiled += char
|
|
23
|
-
valid_color = compiled if Color.attributes.include?(compiled.to_sym)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
valid_color
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
8
|
attr_reader :original
|
|
31
9
|
|
|
32
10
|
include Color
|
|
@@ -131,7 +109,7 @@ module Doing
|
|
|
131
109
|
|
|
132
110
|
after = m['after']
|
|
133
111
|
|
|
134
|
-
if value.
|
|
112
|
+
if !value.good?
|
|
135
113
|
after
|
|
136
114
|
else
|
|
137
115
|
pad = m['width'].to_i
|
|
@@ -183,7 +161,7 @@ module Doing
|
|
|
183
161
|
' '
|
|
184
162
|
else
|
|
185
163
|
line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
|
|
186
|
-
line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || tags_color.
|
|
164
|
+
line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || !tags_color.good?
|
|
187
165
|
"#{line} "
|
|
188
166
|
end
|
|
189
167
|
end.join("\n")
|
data/lib/doing/types.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Doing
|
|
4
4
|
module Types
|
|
5
|
+
REGEX_CASE = /^[cis].*?$/i.freeze
|
|
6
|
+
REGEX_TAG_SORT = /^(?:n(?:ame)?|t(?:ime)?)$/i.freeze
|
|
5
7
|
REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i.freeze
|
|
6
8
|
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i.freeze
|
|
7
9
|
REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/.freeze
|
|
@@ -20,6 +22,13 @@ module Doing
|
|
|
20
22
|
DateBeginString = Class.new(DateTime)
|
|
21
23
|
DateEndString = Class.new(DateTime)
|
|
22
24
|
DateRangeString = Class.new(Array)
|
|
25
|
+
DateRangeOptionalString = Class.new(Array)
|
|
23
26
|
DateIntervalString = Class.new(DateTime)
|
|
27
|
+
BooleanSymbol = Class.new(Symbol)
|
|
28
|
+
CaseSymbol = Class.new(Symbol)
|
|
29
|
+
AgeSymbol = Class.new(String)
|
|
30
|
+
OrderSymbol = Class.new(Symbol)
|
|
31
|
+
TagSortSymbol = Class.new(Symbol)
|
|
32
|
+
MatchingSymbol = Class.new(Symbol)
|
|
24
33
|
end
|
|
25
34
|
end
|
data/lib/doing/util.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Doing
|
|
|
19
19
|
## @param cli [String] The name or path of the cli
|
|
20
20
|
##
|
|
21
21
|
def exec_available(cli)
|
|
22
|
-
return false
|
|
22
|
+
return false unless cli.good?
|
|
23
23
|
|
|
24
24
|
!TTY::Which.which(cli).nil?
|
|
25
25
|
end
|
|
@@ -32,7 +32,7 @@ module Doing
|
|
|
32
32
|
##
|
|
33
33
|
def first_available_exec(*commands)
|
|
34
34
|
commands.compact.map(&:strip).reject(&:empty?).uniq
|
|
35
|
-
|
|
35
|
+
.find { |cmd| exec_available(cmd.split.first) }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def merge_default_proc(target, overwrite)
|
|
@@ -56,7 +56,7 @@ module Doing
|
|
|
56
56
|
# @param [Hash] other_hash The other hash
|
|
57
57
|
#
|
|
58
58
|
def deep_merge_hashes(master_hash, other_hash)
|
|
59
|
-
deep_merge_hashes!(master_hash.
|
|
59
|
+
deep_merge_hashes!(master_hash.clone, other_hash)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Merges a master hash with another hash, recursively.
|
|
@@ -157,42 +157,46 @@ module Doing
|
|
|
157
157
|
def find_default_editor(editor_for = 'default')
|
|
158
158
|
# return nil unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
|
159
159
|
|
|
160
|
-
if ENV['DOING_EDITOR_TEST']
|
|
161
|
-
return ENV['EDITOR']
|
|
162
|
-
end
|
|
160
|
+
return ENV['EDITOR'] if ENV['DOING_EDITOR_TEST']
|
|
163
161
|
|
|
164
162
|
editor_config = Doing.config.settings['editors']
|
|
165
163
|
|
|
166
164
|
if editor_config.is_a?(String)
|
|
167
|
-
|
|
165
|
+
msg = "Please update your configuration, 'editors' should be a mapping."
|
|
166
|
+
msg << ' Delete the key and run `doing config --update`.'
|
|
167
|
+
Doing.logger.warn('Deprecated:', msg)
|
|
168
168
|
return editor_config
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
if editor_config[editor_for]
|
|
172
172
|
editor = editor_config[editor_for]
|
|
173
|
-
|
|
174
|
-
return editor
|
|
173
|
+
Doing.logger.debug('Editor:', "Using #{editor} from config 'editors.#{editor_for}' for #{editor_for}")
|
|
174
|
+
return editor if editor.good?
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
if editor_for != 'editor' && editor_config['default']
|
|
178
178
|
editor = editor_config['default']
|
|
179
|
-
|
|
180
|
-
return editor
|
|
179
|
+
Doing.logger.debug('Editor:', "Using #{editor} from config: 'editors.default' for #{editor_for}")
|
|
180
|
+
return editor if editor.good?
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
editor ||= ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
if editor.good?
|
|
186
|
+
Doing.logger.debug('Editor:', "Found editor in environment variables: #{editor} for #{editor_for}")
|
|
187
187
|
return editor
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
Doing.logger.debug('
|
|
190
|
+
Doing.logger.debug('Editor:', 'No EDITOR environment variable, testing available editors')
|
|
191
191
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
|
192
192
|
editors.each do |ed|
|
|
193
|
-
|
|
193
|
+
try = TTY::Which.which(ed)
|
|
194
|
+
if try
|
|
195
|
+
Doing.logger.debug('Editor:', "Using editor #{try} for #{editor_for}")
|
|
196
|
+
return try
|
|
197
|
+
end
|
|
194
198
|
|
|
195
|
-
Doing.logger.debug('
|
|
199
|
+
Doing.logger.debug('Editor:', "#{ed} not available")
|
|
196
200
|
end
|
|
197
201
|
|
|
198
202
|
nil
|
data/lib/doing/version.rb
CHANGED