doing 2.1.27 → 2.1.31pre
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/.irbrc +1 -0
- data/.yardoc/checksums +11 -10
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +4952 -0
- data/Gemfile.lock +2 -1
- data/README.md +1 -1
- 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 +32 -18
- data/bin/commands/commands.rb +8 -8
- data/bin/commands/completion.rb +61 -19
- data/bin/commands/config.rb +16 -16
- 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/today.rb +1 -1
- data/bin/commands/view.rb +3 -3
- data/bin/commands/yesterday.rb +2 -2
- data/bin/doing +22 -133
- 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 +1 -1
- 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/doing.gemspec +1 -0
- data/doing.rdoc +46 -41
- data/example_plugin.rb +7 -5
- data/lib/completion/_doing.zsh +4 -8
- data/lib/completion/doing.bash +4 -15
- data/lib/completion/doing.fish +6 -9
- data/lib/doing/add_options.rb +117 -0
- data/lib/doing/array/array.rb +16 -0
- data/lib/doing/changelog/change.rb +1 -1
- data/lib/doing/changelog/changes.rb +26 -7
- 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 +5 -5
- 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/scripts/setting_replace.rb +11 -0
- metadata +26 -4
|
@@ -6,13 +6,15 @@ module Doing
|
|
|
6
6
|
attr_reader :changes
|
|
7
7
|
attr_writer :changes_only
|
|
8
8
|
|
|
9
|
-
def initialize(lookup: nil, search: nil,
|
|
10
|
-
@changes_only =
|
|
9
|
+
def initialize(lookup: nil, search: nil, changes: false, sort: :desc)
|
|
10
|
+
@changes_only = changes
|
|
11
11
|
changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'CHANGELOG.md'))
|
|
12
12
|
raise 'Error locating changelog' unless File.exist?(changelog)
|
|
13
13
|
|
|
14
14
|
@content = IO.read(changelog)
|
|
15
15
|
parse_changes(lookup, search)
|
|
16
|
+
|
|
17
|
+
@changes.reverse! if sort == :asc
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def latest
|
|
@@ -23,6 +25,22 @@ module Doing
|
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
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
|
+
|
|
26
44
|
def to_s
|
|
27
45
|
if @changes_only
|
|
28
46
|
@changes.map(&:changes_only).join().force_encoding('utf-8')
|
|
@@ -48,18 +66,19 @@ module Doing
|
|
|
48
66
|
def lookup(lookup_version)
|
|
49
67
|
range = []
|
|
50
68
|
|
|
51
|
-
if lookup_version =~ /([\d.]+)
|
|
69
|
+
if lookup_version =~ /([\d.]+) *(?:-|to)+ *([\d.]+)/
|
|
52
70
|
m = Regexp.last_match
|
|
53
71
|
lookup("> #{m[1]}")
|
|
54
72
|
lookup("< #{m[2]}")
|
|
55
|
-
elsif lookup_version.scan(/[
|
|
56
|
-
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*?.]+/)
|
|
57
75
|
params.each { |query| lookup(query) }
|
|
58
76
|
else
|
|
77
|
+
inclusive = lookup_version =~ /=/ ? true : false
|
|
59
78
|
comp = case lookup_version
|
|
60
79
|
when /(<|prior|before|older)/
|
|
61
80
|
:older
|
|
62
|
-
when
|
|
81
|
+
when /(>|since|after|newer)/
|
|
63
82
|
:newer
|
|
64
83
|
else
|
|
65
84
|
:equal
|
|
@@ -67,7 +86,7 @@ module Doing
|
|
|
67
86
|
version = Version.new(lookup_version)
|
|
68
87
|
|
|
69
88
|
@changes.select! do |change|
|
|
70
|
-
change.version.compare(version, comp)
|
|
89
|
+
change.version.compare(version, comp, inclusive: inclusive)
|
|
71
90
|
end
|
|
72
91
|
end
|
|
73
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
|