doing 2.1.28 → 2.1.32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.irbrc +1 -0
- data/CHANGELOG.md +4962 -0
- data/Dockerfile-2.6 +3 -1
- data/Dockerfile-2.7 +4 -2
- data/Dockerfile-3.0 +3 -1
- data/Gemfile.lock +2 -67
- data/README.md +1 -1
- data/bash_profile +13 -0
- data/bin/commands/again.rb +1 -1
- data/bin/commands/archive.rb +3 -3
- data/bin/commands/cancel.rb +1 -1
- data/bin/commands/changes.rb +24 -14
- data/bin/commands/commands.rb +8 -8
- data/bin/commands/completion.rb +61 -19
- data/bin/commands/config.rb +20 -17
- data/bin/commands/done.rb +1 -1
- data/bin/commands/flag.rb +1 -1
- data/bin/commands/grep.rb +5 -5
- data/bin/commands/last.rb +1 -1
- data/bin/commands/meanwhile.rb +1 -1
- data/bin/commands/now.rb +1 -1
- data/bin/commands/on.rb +1 -1
- data/bin/commands/open.rb +4 -4
- data/bin/commands/recent.rb +4 -4
- data/bin/commands/show.rb +8 -8
- data/bin/commands/since.rb +1 -1
- data/bin/commands/tag_dir.rb +27 -3
- data/bin/commands/today.rb +1 -1
- data/bin/commands/view.rb +3 -3
- data/bin/commands/yesterday.rb +2 -2
- data/bin/doing +26 -135
- data/docs/doc/Array.html +1 -1
- 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/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +324 -4
- data/docs/doc/Doing/Configuration.html +3 -3
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +125 -1
- data/docs/doc/Doing/Items.html +1 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +109 -3
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +1 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +1 -1
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +6 -6
- data/docs/doc/Doing.html +2 -2
- 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/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 +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +3 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +337 -241
- data/docs/doc/top-level-namespace.html +105 -1
- data/docs/index.md +1 -1
- data/doing.gemspec +24 -23
- data/doing.rdoc +43 -41
- data/example_plugin.rb +7 -5
- data/inputrc +57 -0
- data/lib/completion/_doing.zsh +4 -8
- data/lib/completion/doing.fish +4 -8
- data/lib/doing/add_options.rb +117 -0
- data/lib/doing/array/array.rb +16 -0
- data/lib/doing/changelog/changes.rb +23 -6
- data/lib/doing/changelog/version.rb +11 -3
- data/lib/doing/completion/bash_completion.rb +12 -51
- data/lib/doing/completion/fish_completion.rb +16 -52
- data/lib/doing/completion/zsh_completion.rb +12 -51
- data/lib/doing/completion.rb +203 -17
- data/lib/doing/configuration.rb +12 -6
- data/lib/doing/item.rb +21 -3
- data/lib/doing/items.rb +5 -5
- data/lib/doing/note.rb +24 -8
- data/lib/doing/plugins/export/dayone_export.rb +8 -6
- data/lib/doing/plugins/export/html_export.rb +4 -4
- data/lib/doing/plugins/export/json_export.rb +19 -20
- data/lib/doing/plugins/export/markdown_export.rb +2 -2
- data/lib/doing/plugins/export/template_export.rb +4 -4
- 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/section.rb +1 -1
- data/lib/doing/string/highlight.rb +3 -4
- data/lib/doing/string/string.rb +8 -0
- data/lib/doing/util.rb +1 -1
- data/lib/doing/util_backup.rb +12 -12
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +75 -76
- data/lib/doing.rb +58 -0
- data/lib/examples/commands/wiki.rb +27 -19
- data/lib/helpers/threaded_tests.rb +2 -0
- data/scripts/setting_replace.rb +11 -0
- metadata +107 -103
- data/.yardoc/checksums +0 -29
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/lib/completion/doing.bash +0 -504
data/lib/doing/array/array.rb
CHANGED
@@ -2,3 +2,19 @@
|
|
2
2
|
|
3
3
|
require_relative 'tags'
|
4
4
|
require_relative 'nested_hash'
|
5
|
+
|
6
|
+
class ::Array
|
7
|
+
##
|
8
|
+
## Force UTF-8 encoding of strings in array
|
9
|
+
##
|
10
|
+
## @return [Array] Encoded lines
|
11
|
+
##
|
12
|
+
def utf8
|
13
|
+
c = self.class
|
14
|
+
if String.method_defined? :force_encoding
|
15
|
+
replace c.new(map(&:utf8))
|
16
|
+
else
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -25,9 +25,25 @@ module Doing
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
def versions
|
29
|
+
@changes.select { |change| change.entries&.count > 0 }.map { |change| change.version }
|
30
|
+
end
|
31
|
+
|
32
|
+
def interactive
|
33
|
+
Doing::Prompt.choose_from(versions,
|
34
|
+
prompt: 'Select a version to see its changelog',
|
35
|
+
sorted: false,
|
36
|
+
fzf_args: [
|
37
|
+
%(--preview='doing changes --render -l {1}'),
|
38
|
+
'--disabled',
|
39
|
+
'--height=50',
|
40
|
+
'--preview-window="right,70%"'
|
41
|
+
])
|
42
|
+
end
|
43
|
+
|
28
44
|
def to_s
|
29
45
|
if @changes_only
|
30
|
-
@changes.map(&:changes_only).join().force_encoding('utf-8')
|
46
|
+
@changes.map(&:changes_only).delete_if(&:empty?).join().gsub(/\n+/, "\n").force_encoding('utf-8')
|
31
47
|
else
|
32
48
|
@changes.map(&:to_s).join("\n\n").force_encoding('utf-8')
|
33
49
|
end
|
@@ -50,18 +66,19 @@ module Doing
|
|
50
66
|
def lookup(lookup_version)
|
51
67
|
range = []
|
52
68
|
|
53
|
-
if lookup_version =~ /([\d.]+)
|
69
|
+
if lookup_version =~ /([\d.]+) *(?:-|to)+ *([\d.]+)/
|
54
70
|
m = Regexp.last_match
|
55
71
|
lookup("> #{m[1]}")
|
56
72
|
lookup("< #{m[2]}")
|
57
|
-
elsif lookup_version.scan(/[
|
58
|
-
params = lookup_version.scan(/
|
73
|
+
elsif lookup_version.scan(/(?:<=?|prior|before|older|>=?|since|after|newer) *[0-9*?.]+/).count > 1
|
74
|
+
params = lookup_version.scan(/(?:<=?|prior|before|older|>=?|since|after|newer) *[0-9*?.]+/)
|
59
75
|
params.each { |query| lookup(query) }
|
60
76
|
else
|
77
|
+
inclusive = lookup_version =~ /=/ ? true : false
|
61
78
|
comp = case lookup_version
|
62
79
|
when /(<|prior|before|older)/
|
63
80
|
:older
|
64
|
-
when
|
81
|
+
when /(>|since|after|newer)/
|
65
82
|
:newer
|
66
83
|
else
|
67
84
|
:equal
|
@@ -69,7 +86,7 @@ module Doing
|
|
69
86
|
version = Version.new(lookup_version)
|
70
87
|
|
71
88
|
@changes.select! do |change|
|
72
|
-
change.version.compare(version, comp)
|
89
|
+
change.version.compare(version, comp, inclusive: inclusive)
|
73
90
|
end
|
74
91
|
end
|
75
92
|
end
|
@@ -37,7 +37,7 @@ module Doing
|
|
37
37
|
end
|
38
38
|
|
39
39
|
|
40
|
-
def compare(other, comp)
|
40
|
+
def compare(other, comp, inclusive: false)
|
41
41
|
case comp
|
42
42
|
when :older
|
43
43
|
if @maj <= other.maj
|
@@ -46,7 +46,11 @@ module Doing
|
|
46
46
|
elsif @maj == other.maj && (other.min.nil? || @min < other.min)
|
47
47
|
true
|
48
48
|
elsif @maj == other.maj && @min == other.min
|
49
|
-
other.patch.nil?
|
49
|
+
if other.patch.nil?
|
50
|
+
false
|
51
|
+
else
|
52
|
+
inclusive ? @patch <= other.patch : @patch < other.patch
|
53
|
+
end
|
50
54
|
else
|
51
55
|
false
|
52
56
|
end
|
@@ -60,7 +64,11 @@ module Doing
|
|
60
64
|
elsif @maj == other.maj && (other.min.nil? || @min > other.min)
|
61
65
|
true
|
62
66
|
elsif @maj == other.maj && @min == other.min
|
63
|
-
other.patch.nil?
|
67
|
+
if other.patch.nil?
|
68
|
+
false
|
69
|
+
else
|
70
|
+
inclusive ? @patch >= other.patch : @patch > other.patch
|
71
|
+
end
|
64
72
|
else
|
65
73
|
false
|
66
74
|
end
|
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Doing
|
2
4
|
module Completion
|
5
|
+
# Generate completions for Bash
|
3
6
|
class BashCompletions
|
4
7
|
attr_accessor :commands, :global_options
|
5
8
|
|
@@ -11,7 +14,7 @@ module Doing
|
|
11
14
|
@commands.each_with_index do |cmd, i|
|
12
15
|
@bar.advance(status: cmd[:commands].first)
|
13
16
|
|
14
|
-
data = get_help_sections(cmd[:commands].first)
|
17
|
+
data = Completion.get_help_sections(cmd[:commands].first)
|
15
18
|
|
16
19
|
arg = data[:synopsis].join(' ').strip.split(/ /).last
|
17
20
|
case arg
|
@@ -26,7 +29,7 @@ module Doing
|
|
26
29
|
end
|
27
30
|
|
28
31
|
if data[:command_options]
|
29
|
-
options = parse_options(data[:command_options])
|
32
|
+
options = Completion.parse_options(data[:command_options])
|
30
33
|
out << command_function(cmd[:commands].first, options, type)
|
31
34
|
|
32
35
|
if first
|
@@ -119,56 +122,13 @@ module Doing
|
|
119
122
|
[func, logic]
|
120
123
|
end
|
121
124
|
|
122
|
-
|
123
|
-
|
124
|
-
def get_help_sections(command = '')
|
125
|
-
res = `doing help #{command}`.strip
|
126
|
-
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
127
|
-
sections = {}
|
128
|
-
scanned.each do |sect|
|
129
|
-
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
130
|
-
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
131
|
-
sections[title] = content
|
132
|
-
end
|
133
|
-
sections
|
134
|
-
end
|
135
|
-
|
136
|
-
def parse_option(option)
|
137
|
-
res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
|
138
|
-
return nil unless res
|
139
|
-
{
|
140
|
-
short: res['short'],
|
141
|
-
long: res['long'],
|
142
|
-
arg: res[:arg],
|
143
|
-
description: res['desc'].short_desc
|
144
|
-
}
|
145
|
-
end
|
146
|
-
|
147
|
-
def parse_options(options)
|
148
|
-
options.map { |opt| parse_option(opt) }
|
149
|
-
end
|
150
|
-
|
151
|
-
def parse_command(command)
|
152
|
-
res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
|
153
|
-
commands = [res['cmd']]
|
154
|
-
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
155
|
-
|
156
|
-
{
|
157
|
-
commands: commands,
|
158
|
-
description: res['desc'].short_desc
|
159
|
-
}
|
160
|
-
end
|
161
|
-
|
162
|
-
def parse_commands(commands)
|
163
|
-
commands.map { |cmd| parse_command(cmd) }
|
164
|
-
end
|
165
|
-
|
166
125
|
def initialize
|
167
|
-
data = get_help_sections
|
168
|
-
@global_options = parse_options(data[:global_options])
|
169
|
-
@commands = parse_commands(data[:commands])
|
170
|
-
@bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Bash completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :
|
171
|
-
|
126
|
+
data = Completion.get_help_sections
|
127
|
+
@global_options = Completion.parse_options(data[:global_options])
|
128
|
+
@commands = Completion.parse_commands(data[:commands])
|
129
|
+
@bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Bash completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :box, hide_cursor: true, status: 'Reading subcommands')
|
130
|
+
width = TTY::Screen.columns - 45
|
131
|
+
@bar.resize(width)
|
172
132
|
end
|
173
133
|
|
174
134
|
def generate_completions
|
@@ -176,6 +136,7 @@ module Doing
|
|
176
136
|
out = []
|
177
137
|
out << main_function
|
178
138
|
out << 'complete -F _doing doing'
|
139
|
+
@bar.advance(status: '✅')
|
179
140
|
@bar.finish
|
180
141
|
out.join("\n")
|
181
142
|
end
|
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Doing
|
2
4
|
module Completion
|
5
|
+
# Generate completions for Fish
|
3
6
|
class FishCompletions
|
4
7
|
|
5
8
|
attr_accessor :commands, :global_options
|
@@ -137,53 +140,12 @@ module Doing
|
|
137
140
|
EOFUNCTIONS
|
138
141
|
end
|
139
142
|
|
140
|
-
def get_help_sections(command = '')
|
141
|
-
res = `doing help #{command}`.strip
|
142
|
-
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
143
|
-
sections = {}
|
144
|
-
scanned.each do |sect|
|
145
|
-
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
146
|
-
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
147
|
-
sections[title] = content
|
148
|
-
end
|
149
|
-
sections
|
150
|
-
end
|
151
|
-
|
152
|
-
def parse_option(option)
|
153
|
-
res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
|
154
|
-
return nil unless res
|
155
|
-
|
156
|
-
{
|
157
|
-
short: res['short'],
|
158
|
-
long: res['long'],
|
159
|
-
arg: res['arg'],
|
160
|
-
description: res['desc'].short_desc
|
161
|
-
}
|
162
|
-
end
|
163
|
-
|
164
|
-
def parse_options(options)
|
165
|
-
options.map { |opt| parse_option(opt) }
|
166
|
-
end
|
167
|
-
|
168
|
-
def parse_command(command)
|
169
|
-
res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
|
170
|
-
commands = [res['cmd']]
|
171
|
-
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
172
|
-
|
173
|
-
{
|
174
|
-
commands: commands,
|
175
|
-
description: res['desc'].short_desc
|
176
|
-
}
|
177
|
-
end
|
178
|
-
|
179
|
-
def parse_commands(commands)
|
180
|
-
commands.map { |cmd| parse_command(cmd) }
|
181
|
-
end
|
182
|
-
|
183
143
|
def generate_subcommand_completions
|
184
144
|
out = []
|
185
|
-
@commands.
|
186
|
-
|
145
|
+
@commands.each do |cmd|
|
146
|
+
desc = Shellwords.escape(cmd[:description])
|
147
|
+
cmds = cmd[:commands].join(' ')
|
148
|
+
out << "complete -xc doing -n '__fish_doing_needs_command' -a '#{cmds}' -d #{desc}"
|
187
149
|
end
|
188
150
|
|
189
151
|
out.join("\n")
|
@@ -203,14 +165,14 @@ module Doing
|
|
203
165
|
|
204
166
|
@commands.each_with_index do |cmd, i|
|
205
167
|
@bar.advance(status: cmd[:commands].first)
|
206
|
-
data = get_help_sections(cmd[:commands].first)
|
168
|
+
data = Completion.get_help_sections(cmd[:commands].first)
|
207
169
|
|
208
170
|
if data[:synopsis].join(' ').strip.split(/ /).last =~ /(path|file)/i
|
209
171
|
out << "complete -c doing -F -n '__fish_doing_using_command #{cmd[:commands].join(" ")}'"
|
210
172
|
end
|
211
173
|
|
212
174
|
if data[:command_options]
|
213
|
-
parse_options(data[:command_options]).each do |option|
|
175
|
+
Completion.parse_options(data[:command_options]).each do |option|
|
214
176
|
next if option.nil?
|
215
177
|
|
216
178
|
arg = option[:arg] ? '-r' : ''
|
@@ -267,11 +229,12 @@ module Doing
|
|
267
229
|
end
|
268
230
|
|
269
231
|
def initialize
|
270
|
-
data = get_help_sections
|
271
|
-
@global_options = parse_options(data[:global_options])
|
272
|
-
@commands = parse_commands(data[:commands])
|
273
|
-
@bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Fish completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :
|
274
|
-
|
232
|
+
data = Completion.get_help_sections
|
233
|
+
@global_options = Completion.parse_options(data[:global_options])
|
234
|
+
@commands = Completion.parse_commands(data[:commands])
|
235
|
+
@bar = TTY::ProgressBar.new("\033[0;0;33mGenerating Fish completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :block, hide_cursor: true, status: 'processing subcommands')
|
236
|
+
width = TTY::Screen.columns - 45
|
237
|
+
@bar.resize(width)
|
275
238
|
end
|
276
239
|
|
277
240
|
def generate_completions
|
@@ -280,6 +243,7 @@ module Doing
|
|
280
243
|
out << generate_helpers
|
281
244
|
out << generate_subcommand_completions
|
282
245
|
out << generate_subcommand_option_completions
|
246
|
+
@bar.advance(status: '✅')
|
283
247
|
@bar.finish
|
284
248
|
out.join("\n")
|
285
249
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Doing
|
2
4
|
module Completion
|
5
|
+
# Generate completions for zsh
|
3
6
|
class ZshCompletions
|
4
|
-
|
5
7
|
attr_accessor :commands, :global_options
|
6
8
|
|
7
9
|
def generate_helpers
|
@@ -34,53 +36,11 @@ module Doing
|
|
34
36
|
}
|
35
37
|
|
36
38
|
EOFUNCTIONS
|
39
|
+
@bar.advance(status: '✅')
|
37
40
|
@bar.finish
|
38
41
|
out
|
39
42
|
end
|
40
43
|
|
41
|
-
def get_help_sections(command = '')
|
42
|
-
res = `doing help #{command}`.strip
|
43
|
-
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
44
|
-
sections = {}
|
45
|
-
scanned.each do |sect|
|
46
|
-
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
47
|
-
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
48
|
-
sections[title] = content
|
49
|
-
end
|
50
|
-
sections
|
51
|
-
end
|
52
|
-
|
53
|
-
def parse_option(option)
|
54
|
-
res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
|
55
|
-
return nil unless res
|
56
|
-
|
57
|
-
{
|
58
|
-
short: res['short'],
|
59
|
-
long: res['long'],
|
60
|
-
arg: res[:arg],
|
61
|
-
description: res['desc'].short_desc
|
62
|
-
}
|
63
|
-
end
|
64
|
-
|
65
|
-
def parse_options(options)
|
66
|
-
options.map { |opt| parse_option(opt) }
|
67
|
-
end
|
68
|
-
|
69
|
-
def parse_command(command)
|
70
|
-
res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
|
71
|
-
commands = [res['cmd']]
|
72
|
-
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
73
|
-
|
74
|
-
{
|
75
|
-
commands: commands,
|
76
|
-
description: res['desc'].short_desc
|
77
|
-
}
|
78
|
-
end
|
79
|
-
|
80
|
-
def parse_commands(commands)
|
81
|
-
commands.map { |cmd| parse_command(cmd) }
|
82
|
-
end
|
83
|
-
|
84
44
|
def generate_subcommand_completions
|
85
45
|
out = []
|
86
46
|
@commands.each_with_index do |cmd, i|
|
@@ -98,11 +58,11 @@ module Doing
|
|
98
58
|
@commands.each_with_index do |cmd, i|
|
99
59
|
@bar.advance(status: cmd[:commands].first)
|
100
60
|
|
101
|
-
data = get_help_sections(cmd[:commands].first)
|
61
|
+
data = Completion.get_help_sections(cmd[:commands].first)
|
102
62
|
option_arr = []
|
103
63
|
|
104
64
|
if data[:command_options]
|
105
|
-
parse_options(data[:command_options]).each do |option|
|
65
|
+
Completion.parse_options(data[:command_options]).each do |option|
|
106
66
|
next if option.nil?
|
107
67
|
|
108
68
|
arg = option[:arg] ? '=' : ''
|
@@ -124,11 +84,12 @@ module Doing
|
|
124
84
|
end
|
125
85
|
|
126
86
|
def initialize
|
127
|
-
data = get_help_sections
|
128
|
-
@global_options = parse_options(data[:global_options])
|
129
|
-
@commands = parse_commands(data[:commands])
|
130
|
-
@bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :
|
131
|
-
|
87
|
+
data = Completion.get_help_sections
|
88
|
+
@global_options = Completion.parse_options(data[:global_options])
|
89
|
+
@commands = Completion.parse_commands(data[:commands])
|
90
|
+
@bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :block, hide_cursor: true, status: 'processing subcommands')
|
91
|
+
width = TTY::Screen.columns - 45
|
92
|
+
@bar.resize(width)
|
132
93
|
end
|
133
94
|
|
134
95
|
def generate_completions
|
data/lib/doing/completion.rb
CHANGED
@@ -11,25 +11,118 @@ require 'bash_completion'
|
|
11
11
|
module Doing
|
12
12
|
# Completion script generator
|
13
13
|
module Completion
|
14
|
+
OPTIONS_RX = /(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/.freeze
|
15
|
+
SECTIONS_RX = /(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/.freeze
|
16
|
+
COMMAND_RX = /^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/.freeze
|
17
|
+
|
14
18
|
class << self
|
19
|
+
def get_help_sections(command = '')
|
20
|
+
res = `doing help #{command}`.strip
|
21
|
+
scanned = res.scan(SECTIONS_RX)
|
22
|
+
sections = {}
|
23
|
+
scanned.each do |sect|
|
24
|
+
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
25
|
+
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
26
|
+
sections[title] = content
|
27
|
+
end
|
28
|
+
sections
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_option(option)
|
32
|
+
res = option.match(OPTIONS_RX)
|
33
|
+
return nil unless res
|
34
|
+
|
35
|
+
{
|
36
|
+
short: res['short'],
|
37
|
+
long: res['long'],
|
38
|
+
arg: res['arg'],
|
39
|
+
description: res['desc'].short_desc
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_options(options)
|
44
|
+
options.map { |opt| parse_option(opt) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_command(command)
|
48
|
+
res = command.match(COMMAND_RX)
|
49
|
+
commands = [res['cmd']]
|
50
|
+
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
51
|
+
|
52
|
+
{
|
53
|
+
commands: commands,
|
54
|
+
description: res['desc'].short_desc
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_commands(commands)
|
59
|
+
commands.map { |cmd| parse_command(cmd) }
|
60
|
+
end
|
61
|
+
|
15
62
|
# Generate a completion script and output to file or
|
16
63
|
# stdout
|
17
64
|
#
|
18
65
|
# @param type [String] shell to generate for (zsh|bash|fish)
|
19
66
|
# @param file [String] Path to save to, or 'stdout'
|
20
67
|
#
|
21
|
-
def generate_completion(type: 'zsh', file:
|
22
|
-
if type =~ /^all$/i
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
68
|
+
def generate_completion(type: 'zsh', file: :default, link: true)
|
69
|
+
return generate_all if type =~ /^all$/i
|
70
|
+
|
71
|
+
file = file == :default ? default_file(type) : file
|
72
|
+
file = validate_target(file)
|
73
|
+
result = generate_type(type)
|
74
|
+
|
75
|
+
if file =~ /^stdout$/i
|
76
|
+
$stdout.puts result
|
77
|
+
else
|
78
|
+
File.open(file, 'w') { |f| f.puts result }
|
79
|
+
Doing.logger.warn('File written:', "#{type} completions written to #{file}")
|
80
|
+
|
81
|
+
link_completion_type(type, file) if link
|
31
82
|
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def link_default(type)
|
86
|
+
type = normalize_type(type)
|
87
|
+
raise InvalidArgument, 'Unrecognized shell specified' if type == :invalid
|
88
|
+
|
89
|
+
return %i[zsh bash fish].each { |t| link_default(t) } if type == :all
|
32
90
|
|
91
|
+
install_builtin(type)
|
92
|
+
|
93
|
+
link_completion_type(type, File.join(default_dir, default_filenames[type]))
|
94
|
+
end
|
95
|
+
|
96
|
+
def install_builtin(type)
|
97
|
+
FileUtils.mkdir_p(default_dir)
|
98
|
+
src = File.expand_path(File.join(File.dirname(__FILE__), '..', 'completion', default_filenames[type]))
|
99
|
+
|
100
|
+
if File.exist?(File.join(default_dir, default_filenames[type]))
|
101
|
+
return unless Doing::Prompt.yn("Update #{type} completion script", default_response: 'n')
|
102
|
+
end
|
103
|
+
|
104
|
+
FileUtils.cp(src, default_dir)
|
105
|
+
Doing.logger.warn('File written:', "#{type} completions saved to #{default_file(type)}")
|
106
|
+
end
|
107
|
+
|
108
|
+
def normalize_type(type)
|
109
|
+
case type.to_s
|
110
|
+
when /^f/i
|
111
|
+
:fish
|
112
|
+
when /^b/i
|
113
|
+
:bash
|
114
|
+
when /^z/i
|
115
|
+
:zsh
|
116
|
+
when /^a/i
|
117
|
+
:all
|
118
|
+
else
|
119
|
+
:invalid
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def generate_type(type)
|
33
126
|
generator = case type.to_s
|
34
127
|
when /^f/i
|
35
128
|
FishCompletions.new
|
@@ -39,16 +132,109 @@ module Doing
|
|
39
132
|
ZshCompletions.new
|
40
133
|
end
|
41
134
|
|
42
|
-
|
135
|
+
generator.generate_completions
|
136
|
+
end
|
43
137
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
138
|
+
def validate_target(file)
|
139
|
+
unless file =~ /stdout/i
|
140
|
+
file = validate_file(file)
|
141
|
+
|
142
|
+
validate_dir(file)
|
143
|
+
end
|
144
|
+
|
145
|
+
file
|
146
|
+
end
|
147
|
+
|
148
|
+
def default_dir
|
149
|
+
File.expand_path('~/.local/share/doing/completion')
|
150
|
+
end
|
151
|
+
|
152
|
+
def default_filenames
|
153
|
+
{ zsh: '_doing.zsh', bash: 'doing.bash', fish: 'doing.fish' }
|
154
|
+
end
|
155
|
+
|
156
|
+
def default_file(type)
|
157
|
+
type = normalize_type(type)
|
158
|
+
|
159
|
+
File.join(default_dir, default_filenames[type])
|
160
|
+
end
|
161
|
+
|
162
|
+
def validate_file(file)
|
163
|
+
file = File.expand_path(file)
|
164
|
+
if File.exist?(file)
|
165
|
+
res = Doing::Prompt.yn("Overwrite #{file}", default_response: 'y')
|
166
|
+
raise UserCancelled unless res
|
167
|
+
|
168
|
+
FileUtils.rm(file) if res
|
169
|
+
end
|
170
|
+
file
|
171
|
+
end
|
172
|
+
|
173
|
+
def validate_dir(file)
|
174
|
+
dir = File.dirname(file)
|
175
|
+
unless File.directory?(dir)
|
176
|
+
res = Doing::Prompt.yn("#{dir} doesn't exist, create it", default_response: 'y')
|
177
|
+
raise UserCancelled unless res
|
178
|
+
|
179
|
+
FileUtils.mkdir_p(dir)
|
180
|
+
end
|
181
|
+
dir
|
182
|
+
end
|
183
|
+
|
184
|
+
def generate_all
|
185
|
+
Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths')
|
186
|
+
generate_completion(type: 'fish', file: 'lib/completion/doing.fish', link: false)
|
187
|
+
Doing.logger.warn('File written:', 'fish completions written to lib/completion/doing.fish')
|
188
|
+
generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh', link: false)
|
189
|
+
Doing.logger.warn('File written:', 'zsh completions written to lib/completion/_doing.zsh')
|
190
|
+
generate_completion(type: 'bash', file: 'lib/completion/doing.bash', link: false)
|
191
|
+
Doing.logger.warn('File written:', 'bash completions written to lib/completion/doing.bash')
|
192
|
+
end
|
193
|
+
|
194
|
+
def link_completion_type(type, file)
|
195
|
+
dir = File.dirname(file)
|
196
|
+
case type.to_s
|
197
|
+
when /^b/i
|
198
|
+
unless dir =~ %r{(\.bash_it/completion|bash_completion/completions)}
|
199
|
+
link_completion(file, ['~/.bash_it/completion/enabled', '/usr/share/bash_completion/completions'], 'doing.bash')
|
49
200
|
end
|
50
|
-
|
201
|
+
when /^f/i
|
202
|
+
link_completion(file, ['~/.config/fish/completions'], 'doing.fish') unless dir =~ %r{.config/fish/completions}
|
203
|
+
when /^z/i
|
204
|
+
unless dir =~ %r{(\.oh-my-zsh/completions|share/site-functions)}
|
205
|
+
link_completion(file, ['~/.oh-my-zsh/completions', '/usr/local/share/zsh/site-functions'], '_doing.zsh')
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def link_completion(file, targets, filename)
|
211
|
+
return if targets.map { |t| File.expand_path(t) }.include?(File.dirname(file))
|
212
|
+
|
213
|
+
found = false
|
214
|
+
linked = false
|
215
|
+
|
216
|
+
targets.each do |target|
|
217
|
+
next unless File.directory?(File.expand_path(target))
|
218
|
+
found = true
|
219
|
+
|
220
|
+
target_file = File.join(File.expand_path(target), filename)
|
221
|
+
next unless Doing::Prompt.yn("Create link to #{target_file}", default_response: 'n')
|
222
|
+
|
223
|
+
FileUtils.ln_s(File.expand_path(file), target_file, force: true)
|
224
|
+
Doing.logger.warn('File linked:', "#{File.expand_path(file)} -> #{target_file}")
|
225
|
+
linked = true
|
226
|
+
break
|
227
|
+
end
|
228
|
+
|
229
|
+
return if linked
|
230
|
+
|
231
|
+
unless found
|
232
|
+
$stdout.puts 'No known auto-load directory found for specified shell'.red
|
233
|
+
$stdout.puts "Looked for #{targets.join(', ')}, found no existing directory".yellow
|
51
234
|
end
|
235
|
+
$stdout.puts 'If you don\'t want to autoload completions'.yellow
|
236
|
+
$stdout.puts 'you can source the script directly in your shell\'s startup file:'.yellow
|
237
|
+
$stdout.puts %(source "#{file}").boldwhite
|
52
238
|
end
|
53
239
|
end
|
54
240
|
end
|