doing 2.1.29 → 2.1.33

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/CHANGELOG.md +4962 -0
  4. data/Dockerfile-2.6 +3 -1
  5. data/Dockerfile-2.7 +4 -2
  6. data/Dockerfile-3.0 +3 -1
  7. data/Gemfile.lock +2 -67
  8. data/README.md +1 -1
  9. data/bash_profile +13 -0
  10. data/bin/commands/again.rb +1 -1
  11. data/bin/commands/archive.rb +3 -3
  12. data/bin/commands/cancel.rb +1 -1
  13. data/bin/commands/changes.rb +2 -2
  14. data/bin/commands/commands.rb +8 -8
  15. data/bin/commands/completion.rb +61 -19
  16. data/bin/commands/config.rb +20 -17
  17. data/bin/commands/done.rb +1 -1
  18. data/bin/commands/flag.rb +1 -1
  19. data/bin/commands/grep.rb +5 -5
  20. data/bin/commands/last.rb +1 -1
  21. data/bin/commands/meanwhile.rb +1 -1
  22. data/bin/commands/now.rb +1 -1
  23. data/bin/commands/on.rb +1 -1
  24. data/bin/commands/open.rb +4 -4
  25. data/bin/commands/recent.rb +4 -4
  26. data/bin/commands/show.rb +8 -8
  27. data/bin/commands/since.rb +1 -1
  28. data/bin/commands/tag_dir.rb +27 -3
  29. data/bin/commands/today.rb +1 -1
  30. data/bin/commands/view.rb +3 -3
  31. data/bin/commands/yesterday.rb +2 -2
  32. data/bin/doing +26 -135
  33. data/docs/doc/Array.html +1 -1
  34. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  35. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  36. data/docs/doc/BooleanTermParser/Query.html +1 -1
  37. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  38. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  39. data/docs/doc/BooleanTermParser.html +1 -1
  40. data/docs/doc/Doing/Color.html +1 -1
  41. data/docs/doc/Doing/Completion.html +324 -4
  42. data/docs/doc/Doing/Configuration.html +3 -3
  43. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  44. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  45. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  46. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  47. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  48. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  49. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  50. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  51. data/docs/doc/Doing/Errors.html +1 -1
  52. data/docs/doc/Doing/Hooks.html +1 -1
  53. data/docs/doc/Doing/Item.html +125 -1
  54. data/docs/doc/Doing/Items.html +1 -1
  55. data/docs/doc/Doing/LogAdapter.html +1 -1
  56. data/docs/doc/Doing/Note.html +109 -3
  57. data/docs/doc/Doing/Pager.html +1 -1
  58. data/docs/doc/Doing/Plugins.html +1 -1
  59. data/docs/doc/Doing/Prompt.html +1 -1
  60. data/docs/doc/Doing/Section.html +1 -1
  61. data/docs/doc/Doing/TemplateString.html +1 -1
  62. data/docs/doc/Doing/Types.html +2 -2
  63. data/docs/doc/Doing/Util/Backup.html +1 -1
  64. data/docs/doc/Doing/Util.html +1 -1
  65. data/docs/doc/Doing/WWID.html +6 -6
  66. data/docs/doc/Doing.html +2 -2
  67. data/docs/doc/FalseClass.html +1 -1
  68. data/docs/doc/GLI/Commands/Help.html +1 -1
  69. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  70. data/docs/doc/GLI/Commands.html +1 -1
  71. data/docs/doc/GLI.html +1 -1
  72. data/docs/doc/Hash.html +1 -1
  73. data/docs/doc/Object.html +1 -1
  74. data/docs/doc/PhraseParser/Operator.html +1 -1
  75. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  76. data/docs/doc/PhraseParser/Query.html +1 -1
  77. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  78. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  79. data/docs/doc/PhraseParser/TermClause.html +1 -1
  80. data/docs/doc/PhraseParser.html +1 -1
  81. data/docs/doc/Status.html +1 -1
  82. data/docs/doc/String.html +1 -1
  83. data/docs/doc/Symbol.html +1 -1
  84. data/docs/doc/Time.html +1 -1
  85. data/docs/doc/TrueClass.html +1 -1
  86. data/docs/doc/_index.html +3 -1
  87. data/docs/doc/file.README.html +2 -2
  88. data/docs/doc/index.html +2 -2
  89. data/docs/doc/method_list.html +337 -241
  90. data/docs/doc/top-level-namespace.html +105 -1
  91. data/docs/index.md +1 -1
  92. data/doing.gemspec +24 -23
  93. data/doing.rdoc +34 -13
  94. data/example_plugin.rb +7 -5
  95. data/inputrc +57 -0
  96. data/lib/completion/_doing.zsh +46 -46
  97. data/lib/completion/doing.bash +2 -2
  98. data/lib/completion/doing.fish +4 -4
  99. data/lib/doing/add_options.rb +117 -0
  100. data/lib/doing/array/array.rb +16 -0
  101. data/lib/doing/changelog/changes.rb +8 -6
  102. data/lib/doing/changelog/version.rb +11 -3
  103. data/lib/doing/completion/bash_completion.rb +12 -51
  104. data/lib/doing/completion/fish_completion.rb +17 -53
  105. data/lib/doing/completion/zsh_completion.rb +21 -54
  106. data/lib/doing/completion.rb +203 -17
  107. data/lib/doing/configuration.rb +12 -6
  108. data/lib/doing/item.rb +21 -3
  109. data/lib/doing/items.rb +5 -5
  110. data/lib/doing/note.rb +24 -8
  111. data/lib/doing/plugins/export/dayone_export.rb +8 -6
  112. data/lib/doing/plugins/export/html_export.rb +4 -4
  113. data/lib/doing/plugins/export/json_export.rb +19 -20
  114. data/lib/doing/plugins/export/markdown_export.rb +2 -2
  115. data/lib/doing/plugins/export/template_export.rb +4 -4
  116. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  117. data/lib/doing/plugins/import/doing_import.rb +1 -1
  118. data/lib/doing/plugins/import/timing_import.rb +1 -1
  119. data/lib/doing/section.rb +1 -1
  120. data/lib/doing/string/highlight.rb +3 -4
  121. data/lib/doing/string/string.rb +8 -0
  122. data/lib/doing/types.rb +1 -1
  123. data/lib/doing/util.rb +1 -1
  124. data/lib/doing/util_backup.rb +12 -12
  125. data/lib/doing/version.rb +1 -1
  126. data/lib/doing/wwid.rb +75 -76
  127. data/lib/doing.rb +58 -0
  128. data/lib/examples/commands/wiki.rb +27 -19
  129. data/lib/helpers/threaded_tests.rb +2 -0
  130. data/scripts/setting_replace.rb +11 -0
  131. metadata +107 -102
  132. data/.yardoc/checksums +0 -29
  133. data/.yardoc/complete +0 -0
  134. data/.yardoc/object_types +0 -0
  135. data/.yardoc/objects/root.dat +0 -0
  136. data/.yardoc/proxy_types +0 -0
@@ -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: :square, 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' : ''
@@ -221,7 +183,7 @@ module Doing
221
183
  need_export.concat(cmd[:commands]) if option[:long] == 'output'
222
184
  need_bool.concat(cmd[:commands]) if option[:long] == 'bool'
223
185
  need_case.concat(cmd[:commands]) if option[:long] == 'case'
224
- need_case.concat(cmd[:commands]) if option[:long] == 'sort'
186
+ need_sort.concat(cmd[:commands]) if option[:long] == 'sort'
225
187
  need_tag_sort.concat(cmd[:commands]) if option[:long] == 'tag_sort'
226
188
  need_tag_order.concat(cmd[:commands]) if option[:long] == 'tag_order'
227
189
  need_age.concat(cmd[:commands]) if option[:long] == 'age'
@@ -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: :square, 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,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doing
2
4
  module Completion
3
- class ZshCompletions
5
+ class ::String
6
+ def sanitize
7
+ gsub(/'/, '\\\'').gsub(/\[/, '(').gsub(/\]/, ')')
8
+ end
9
+ end
4
10
 
11
+ # Generate completions for zsh
12
+ class ZshCompletions
5
13
  attr_accessor :commands, :global_options
6
14
 
7
15
  def generate_helpers
@@ -34,53 +42,11 @@ module Doing
34
42
  }
35
43
 
36
44
  EOFUNCTIONS
45
+ @bar.advance(status: '✅')
37
46
  @bar.finish
38
47
  out
39
48
  end
40
49
 
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
50
  def generate_subcommand_completions
85
51
  out = []
86
52
  @commands.each_with_index do |cmd, i|
@@ -98,19 +64,19 @@ module Doing
98
64
  @commands.each_with_index do |cmd, i|
99
65
  @bar.advance(status: cmd[:commands].first)
100
66
 
101
- data = get_help_sections(cmd[:commands].first)
67
+ data = Completion.get_help_sections(cmd[:commands].first)
102
68
  option_arr = []
103
69
 
104
70
  if data[:command_options]
105
- parse_options(data[:command_options]).each do |option|
71
+ Completion.parse_options(data[:command_options]).each do |option|
106
72
  next if option.nil?
107
73
 
108
- arg = option[:arg] ? '=' : ''
74
+ arg = option[:arg] ? ":#{option[:arg]}:" : ''
109
75
 
110
76
  option_arr << if option[:short]
111
- %({-#{option[:short]},--#{option[:long]}#{arg}}"[#{option[:description].gsub(/'/, '\\\'')}]")
77
+ %({'(--#{option[:long]})-#{option[:short]}','(-#{option[:short]})--#{option[:long]}'}"[#{option[:description].sanitize}]#{arg}")
112
78
  else
113
- %("(--#{option[:long]}#{arg})--#{option[:long]}#{arg}}[#{option[:description].gsub(/'/, '\\\'')}]")
79
+ %("--#{option[:long]}[#{option[:description].sanitize}]#{arg}")
114
80
  end
115
81
  end
116
82
  end
@@ -124,11 +90,12 @@ module Doing
124
90
  end
125
91
 
126
92
  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)
93
+ data = Completion.get_help_sections
94
+ @global_options = Completion.parse_options(data[:global_options])
95
+ @commands = Completion.parse_commands(data[:commands])
96
+ @bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :square, hide_cursor: true, status: 'processing subcommands')
97
+ width = TTY::Screen.columns - 45
98
+ @bar.resize(width)
132
99
  end
133
100
 
134
101
  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
@@ -156,7 +156,13 @@ module Doing
156
156
  ##
157
157
  ## @return [String] file path
158
158
  ##
159
- def choose_config(create: false)
159
+ def choose_config(create: false, local: false)
160
+ if local && create
161
+ res = File.expand_path('.doingrc')
162
+ FileUtils.touch(res)
163
+ return res
164
+ end
165
+
160
166
  return @config_file if @force_answer
161
167
 
162
168
  if @additional_configs&.count&.positive? || create
@@ -220,9 +226,9 @@ module Doing
220
226
  return nil unless create
221
227
  end
222
228
 
223
- resolved = real_path.count.positive? ? "Resolved #{real_path.join('->')}, but " : ''
229
+ resolved = real_path.count.positive? ? "Resolved #{real_path.join('.')}, but " : ''
224
230
  Doing.logger.log_now(:warn, "#{resolved}#{path} is unknown")
225
- new_path = [*real_path, path, *paths].join('->')
231
+ new_path = [*real_path, path, *paths].join('.')
226
232
  Doing.logger.log_now(:warn, "Continuing will create the path #{new_path}")
227
233
  res = Prompt.yn('Key path not found, create it?', default_response: true)
228
234
  raise InvalidArgument, 'Invalid key path' unless res
@@ -374,7 +380,7 @@ module Doing
374
380
  deprecated = true
375
381
  config['editors']['default'] ||= config['editor']
376
382
  config.delete('editor')
377
- Doing.logger.debug('Deprecated:', "config key 'editor' is now 'editors->default', please update your config.")
383
+ Doing.logger.debug('Deprecated:', "config key 'editor' is now 'editors.default', please update your config.")
378
384
  end
379
385
 
380
386
  if config.key?('config_editor_app') && !config['editors']['config']
@@ -382,7 +388,7 @@ module Doing
382
388
  config['editors']['config'] = config['config_editor_app']
383
389
  config.delete('config_editor_app')
384
390
  Doing.logger.debug('Deprecated:',
385
- "config key 'config_editor_app' is now 'editors->config', please update your config.")
391
+ "config key 'config_editor_app' is now 'editors.config', please update your config.")
386
392
  end
387
393
 
388
394
  if config.key?('editor_app') && !config['editors']['doing_file']
@@ -390,7 +396,7 @@ module Doing
390
396
  config['editors']['doing_file'] = config['editor_app']
391
397
  config.delete('editor_app')
392
398
  Doing.logger.debug('Deprecated:',
393
- "config key 'editor_app' is now 'editors->doing_file', please update your config.")
399
+ "config key 'editor_app' is now 'editors.doing_file', please update your config.")
394
400
  end
395
401
 
396
402
  Doing.logger.warn('Deprecated:', 'outdated keys found, please run `doing config --update`.') if deprecated