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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +11 -10
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +4952 -0
  7. data/Gemfile.lock +2 -1
  8. data/README.md +1 -1
  9. data/bin/commands/again.rb +1 -1
  10. data/bin/commands/archive.rb +3 -3
  11. data/bin/commands/cancel.rb +1 -1
  12. data/bin/commands/changes.rb +32 -18
  13. data/bin/commands/commands.rb +8 -8
  14. data/bin/commands/completion.rb +61 -19
  15. data/bin/commands/config.rb +16 -16
  16. data/bin/commands/done.rb +1 -1
  17. data/bin/commands/flag.rb +1 -1
  18. data/bin/commands/grep.rb +5 -5
  19. data/bin/commands/last.rb +1 -1
  20. data/bin/commands/meanwhile.rb +1 -1
  21. data/bin/commands/now.rb +1 -1
  22. data/bin/commands/on.rb +1 -1
  23. data/bin/commands/open.rb +4 -4
  24. data/bin/commands/recent.rb +4 -4
  25. data/bin/commands/show.rb +8 -8
  26. data/bin/commands/since.rb +1 -1
  27. data/bin/commands/today.rb +1 -1
  28. data/bin/commands/view.rb +3 -3
  29. data/bin/commands/yesterday.rb +2 -2
  30. data/bin/doing +22 -133
  31. data/docs/doc/Array.html +1 -1
  32. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  33. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  34. data/docs/doc/BooleanTermParser/Query.html +1 -1
  35. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  36. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  37. data/docs/doc/BooleanTermParser.html +1 -1
  38. data/docs/doc/Doing/Color.html +1 -1
  39. data/docs/doc/Doing/Completion.html +324 -4
  40. data/docs/doc/Doing/Configuration.html +1 -1
  41. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  42. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  43. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  44. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  45. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  46. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  47. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  48. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  49. data/docs/doc/Doing/Errors.html +1 -1
  50. data/docs/doc/Doing/Hooks.html +1 -1
  51. data/docs/doc/Doing/Item.html +125 -1
  52. data/docs/doc/Doing/Items.html +1 -1
  53. data/docs/doc/Doing/LogAdapter.html +1 -1
  54. data/docs/doc/Doing/Note.html +109 -3
  55. data/docs/doc/Doing/Pager.html +1 -1
  56. data/docs/doc/Doing/Plugins.html +1 -1
  57. data/docs/doc/Doing/Prompt.html +1 -1
  58. data/docs/doc/Doing/Section.html +1 -1
  59. data/docs/doc/Doing/TemplateString.html +1 -1
  60. data/docs/doc/Doing/Types.html +1 -1
  61. data/docs/doc/Doing/Util/Backup.html +1 -1
  62. data/docs/doc/Doing/Util.html +1 -1
  63. data/docs/doc/Doing/WWID.html +6 -6
  64. data/docs/doc/Doing.html +2 -2
  65. data/docs/doc/FalseClass.html +1 -1
  66. data/docs/doc/GLI/Commands/Help.html +1 -1
  67. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  68. data/docs/doc/GLI/Commands.html +1 -1
  69. data/docs/doc/GLI.html +1 -1
  70. data/docs/doc/Hash.html +1 -1
  71. data/docs/doc/Object.html +1 -1
  72. data/docs/doc/PhraseParser/Operator.html +1 -1
  73. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  74. data/docs/doc/PhraseParser/Query.html +1 -1
  75. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  76. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  77. data/docs/doc/PhraseParser/TermClause.html +1 -1
  78. data/docs/doc/PhraseParser.html +1 -1
  79. data/docs/doc/Status.html +1 -1
  80. data/docs/doc/String.html +1 -1
  81. data/docs/doc/Symbol.html +1 -1
  82. data/docs/doc/Time.html +1 -1
  83. data/docs/doc/TrueClass.html +1 -1
  84. data/docs/doc/_index.html +3 -1
  85. data/docs/doc/file.README.html +2 -2
  86. data/docs/doc/index.html +2 -2
  87. data/docs/doc/method_list.html +337 -241
  88. data/docs/doc/top-level-namespace.html +105 -1
  89. data/doing.gemspec +1 -0
  90. data/doing.rdoc +46 -41
  91. data/example_plugin.rb +7 -5
  92. data/lib/completion/_doing.zsh +4 -8
  93. data/lib/completion/doing.bash +4 -15
  94. data/lib/completion/doing.fish +6 -9
  95. data/lib/doing/add_options.rb +117 -0
  96. data/lib/doing/array/array.rb +16 -0
  97. data/lib/doing/changelog/change.rb +1 -1
  98. data/lib/doing/changelog/changes.rb +26 -7
  99. data/lib/doing/changelog/version.rb +11 -3
  100. data/lib/doing/completion/bash_completion.rb +12 -51
  101. data/lib/doing/completion/fish_completion.rb +16 -52
  102. data/lib/doing/completion/zsh_completion.rb +12 -51
  103. data/lib/doing/completion.rb +203 -17
  104. data/lib/doing/configuration.rb +5 -5
  105. data/lib/doing/item.rb +21 -3
  106. data/lib/doing/items.rb +5 -5
  107. data/lib/doing/note.rb +24 -8
  108. data/lib/doing/plugins/export/dayone_export.rb +8 -6
  109. data/lib/doing/plugins/export/html_export.rb +4 -4
  110. data/lib/doing/plugins/export/json_export.rb +19 -20
  111. data/lib/doing/plugins/export/markdown_export.rb +2 -2
  112. data/lib/doing/plugins/export/template_export.rb +4 -4
  113. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  114. data/lib/doing/plugins/import/doing_import.rb +1 -1
  115. data/lib/doing/plugins/import/timing_import.rb +1 -1
  116. data/lib/doing/section.rb +1 -1
  117. data/lib/doing/string/highlight.rb +3 -4
  118. data/lib/doing/string/string.rb +8 -0
  119. data/lib/doing/util.rb +1 -1
  120. data/lib/doing/util_backup.rb +12 -12
  121. data/lib/doing/version.rb +1 -1
  122. data/lib/doing/wwid.rb +75 -76
  123. data/lib/doing.rb +58 -0
  124. data/lib/examples/commands/wiki.rb +27 -19
  125. data/scripts/setting_replace.rb +11 -0
  126. 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, changes_only: false)
10
- @changes_only = 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.]+) *-+ *([\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(/[<>]/).count > 1
56
- params = lookup_version.scan(/[<>] [\d.]+/)
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 />|since|after|newer/
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? ? false : @patch < other.patch
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? || @patch >= other.patch
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: :blade, status: 'Reading subcommands')
171
- @bar.resize(25)
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.each_with_index do |cmd, i|
186
- out << "complete -xc doing -n '__fish_doing_needs_command' -a '#{cmd[:commands].join(' ')}' -d #{Shellwords.escape(cmd[:description])}"
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: :blade, status: 'processing subcommands')
274
- @bar.resize(25)
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: :blade, status: 'processing subcommands')
131
- @bar.resize(25)
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
@@ -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: 'stdout')
22
- if type =~ /^all$/i
23
- Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths')
24
- generate_completion(type: 'fish', file: 'lib/completion/doing.fish')
25
- Doing.logger.warn('File written:', "fish completions written to lib/completion/doing.fish")
26
- generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh')
27
- Doing.logger.warn('File written:', "zsh completions written to lib/completion/_doing.zsh")
28
- generate_completion(type: 'bash', file: 'lib/completion/doing.bash')
29
- Doing.logger.warn('File written:', "bash completions written to lib/completion/doing.bash")
30
- return
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
- result = generator.generate_completions
135
+ generator.generate_completions
136
+ end
43
137
 
44
- if file =~ /^stdout$/i
45
- $stdout.puts result
46
- else
47
- File.open(File.expand_path(file), 'w') do |f|
48
- f.puts result
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
- Doing.logger.warn('File written:', "#{type} completions written to #{file}")
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