Porytiles
Loading...
Searching...
No Matches
command_completion.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <iostream>
4#include <string>
5
6#include "CLI/CLI.hpp"
7
9
10#include "command.hpp"
11
27class CompletionCommand final : public Command {
28 public:
29 explicit CompletionCommand(CLI::App &parent_app) : Command{parent_app, kCommandName, kCommandDesc, kCommandGroup}
30 {
31 CLI::App &cmd = get_app();
32 cmd.add_option("<shell>", shell_, "Shell type: bash, zsh, or fish")->required();
33 }
34
35 void Run() override
36 {
37 if (shell_ == "bash") {
38 output_bash_completion();
39 }
40 else if (shell_ == "zsh") {
41 output_zsh_completion();
42 }
43 else if (shell_ == "fish") {
44 output_fish_completion();
45 }
46 else {
47 std::cerr << "Unknown shell type: " << shell_ << "\n";
48 std::cerr << "Supported shells: bash, zsh, fish\n";
49 throw CLI::RuntimeError{1};
50 }
51 }
52
53 private:
54 static constexpr auto kCommandName = "completion";
55 static constexpr auto kCommandDesc = "Generate shell completion scripts.";
56 static constexpr auto kCommandGroup = "UTILITIES";
57 std::string shell_;
58
59 void output_bash_completion()
60 {
61 std::cout << "# Bash completion for porytiles\n";
62 std::cout << "# Add to ~/.bashrc or ~/.bash_completion:\n";
63 std::cout << "# eval \"$(porytiles completion bash)\"\n";
64 std::cout << "\n";
65
66 // Helper function to extract project root from command line
67 std::cout << "# Helper function to extract --project-root or -C from command line\n";
68 std::cout << "_porytiles_get_project_root() {\n";
69 std::cout << " local i\n";
70 std::cout << " for ((i=1; i < ${#COMP_WORDS[@]}; i++)); do\n";
71 std::cout << " case \"${COMP_WORDS[i]}\" in\n";
72 std::cout << " -C|--project-root)\n";
73 std::cout << " if [[ $((i+1)) -lt ${#COMP_WORDS[@]} ]]; then\n";
74 std::cout << " echo \"${COMP_WORDS[$((i+1))]}\"\n";
75 std::cout << " return\n";
76 std::cout << " fi\n";
77 std::cout << " ;;\n";
78 std::cout << " esac\n";
79 std::cout << " done\n";
80 std::cout << " echo \".\"\n";
81 std::cout << "}\n";
82 std::cout << "\n";
83
84 // Helper function to get tileset completions
85 std::cout << "# Helper function to get tileset completions\n";
86 std::cout << "_porytiles_complete_tilesets() {\n";
87 std::cout << " local filter=\"$1\"\n";
88 std::cout << " local project_root\n";
89 std::cout << " project_root=$(_porytiles_get_project_root)\n";
90 std::cout << " porytiles list-tilesets -C \"$project_root\" --filter \"$filter\" --prefix \"$cur\" "
91 "2>/dev/null\n";
92 std::cout << "}\n";
93 std::cout << "\n";
94
95 std::cout << "_porytiles_completions() {\n";
96 std::cout << " local cur prev opts subcommand\n";
97 std::cout << " COMPREPLY=()\n";
98 std::cout << " cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
99 std::cout << " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n";
100 std::cout << "\n";
101
102 // Find the subcommand
103 std::cout << " # Find the subcommand\n";
104 std::cout << " subcommand=\"\"\n";
105 std::cout << " for ((i=1; i < COMP_CWORD; i++)); do\n";
106 std::cout << " case \"${COMP_WORDS[i]}\" in\n";
107 std::cout << " compile-tileset|create-tileset|decompile-tileset|dump-tileset-config|"
108 "import-tileset|list-tilesets|completion)\n";
109 std::cout << " subcommand=\"${COMP_WORDS[i]}\"\n";
110 std::cout << " break\n";
111 std::cout << " ;;\n";
112 std::cout << " esac\n";
113 std::cout << " done\n";
114 std::cout << "\n";
115
116 std::cout << " # Main commands\n";
117 std::cout << " local commands=\"compile-tileset create-tileset decompile-tileset dump-tileset-config "
118 "import-tileset list-tilesets completion\"\n";
119 std::cout << "\n";
120 std::cout << " # Config options\n";
121 std::cout << " local config_opts=\"";
122
124 for (const auto &opt : meta) {
125 std::cout << "--" << opt.long_name << " ";
126 if (opt.has_negation) {
127 std::cout << "--no-" << opt.long_name << " ";
128 }
129 }
130
131 std::cout << "\"\n";
132 std::cout << "\n";
133
134 // Handle completion based on context
135 std::cout << " # If no subcommand yet, complete commands\n";
136 std::cout << " if [[ -z \"$subcommand\" ]]; then\n";
137 std::cout << " if [[ ${cur} == -* ]]; then\n";
138 std::cout << " COMPREPLY=( $(compgen -W \"-C --project-root\" -- ${cur}) )\n";
139 std::cout << " else\n";
140 std::cout << " COMPREPLY=( $(compgen -W \"${commands}\" -- ${cur}) )\n";
141 std::cout << " fi\n";
142 std::cout << " return 0\n";
143 std::cout << " fi\n";
144 std::cout << "\n";
145
146 std::cout << " case \"${prev}\" in\n";
147
148 // Add case statements for enum options
149 for (const auto &opt : meta) {
150 if (!opt.choices.empty()) {
151 std::cout << " --" << opt.long_name << ")\n";
152 std::cout << " COMPREPLY=( $(compgen -W \"";
153 for (std::size_t i = 0; i < opt.choices.size(); ++i) {
154 if (i > 0) {
155 std::cout << " ";
156 }
157 std::cout << opt.choices[i];
158 }
159 std::cout << "\" -- ${cur}) )\n";
160 std::cout << " return 0\n";
161 std::cout << " ;;\n";
162 }
163 }
164
165 std::cout << " -C|--project-root)\n";
166 std::cout << " # Complete directories\n";
167 std::cout << " COMPREPLY=( $(compgen -d -- ${cur}) )\n";
168 std::cout << " return 0\n";
169 std::cout << " ;;\n";
170 std::cout << " *)\n";
171 std::cout << " ;;\n";
172 std::cout << " esac\n";
173 std::cout << "\n";
174
175 // Options completion (include command-specific flags)
176 std::cout << " if [[ ${cur} == -* ]]; then\n";
177 std::cout << " local extra_opts=\"\"\n";
178 std::cout << " if [[ \"$subcommand\" == \"create-tileset\" ]]; then\n";
179 std::cout << " extra_opts=\"--secondary\"\n";
180 std::cout << " fi\n";
181 std::cout << " COMPREPLY=( $(compgen -W \"${config_opts} ${extra_opts}\" -- ${cur}) )\n";
182 std::cout << " return 0\n";
183 std::cout << " fi\n";
184 std::cout << "\n";
185
186 // Tileset name completion based on subcommand
187 std::cout << " # Complete tileset names based on subcommand\n";
188 std::cout << " case \"$subcommand\" in\n";
189 std::cout << " compile-tileset|decompile-tileset)\n";
190 std::cout << " # Only managed tilesets\n";
191 std::cout << " COMPREPLY=( $(_porytiles_complete_tilesets managed) )\n";
192 std::cout << " ;;\n";
193 std::cout << " dump-tileset-config)\n";
194 std::cout << " # All tilesets (config can be dumped for any)\n";
195 std::cout << " COMPREPLY=( $(_porytiles_complete_tilesets all) )\n";
196 std::cout << " ;;\n";
197 std::cout << " import-tileset)\n";
198 std::cout << " # Only unmanaged tilesets\n";
199 std::cout << " COMPREPLY=( $(_porytiles_complete_tilesets unmanaged) )\n";
200 std::cout << " ;;\n";
201 std::cout << " create-tileset)\n";
202 std::cout << " # No completion - user provides new name\n";
203 std::cout << " ;;\n";
204 std::cout << " list-tilesets)\n";
205 std::cout << " # Complete --filter values\n";
206 std::cout << " if [[ \"${prev}\" == \"--filter\" ]]; then\n";
207 std::cout << " COMPREPLY=( $(compgen -W \"all managed unmanaged\" -- ${cur}) )\n";
208 std::cout << " fi\n";
209 std::cout << " ;;\n";
210 std::cout << " completion)\n";
211 std::cout << " COMPREPLY=( $(compgen -W \"bash zsh fish\" -- ${cur}) )\n";
212 std::cout << " ;;\n";
213 std::cout << " esac\n";
214 std::cout << "\n";
215 std::cout << " return 0\n";
216 std::cout << "}\n";
217 std::cout << "\n";
218 std::cout << "complete -F _porytiles_completions porytiles\n";
219 }
220
221 void output_zsh_completion()
222 {
223 std::cout << "#compdef porytiles\n";
224 std::cout << "# Zsh completion for porytiles\n";
225 std::cout << "# Add to ~/.zshrc or place in a directory in your $fpath:\n";
226 std::cout << "# eval \"$(porytiles completion zsh)\"\n";
227 std::cout << "\n";
228
229 // Helper function to extract project root
230 std::cout << "# Helper function to extract --project-root or -C from command line\n";
231 std::cout << "_porytiles_get_project_root() {\n";
232 std::cout << " local i\n";
233 std::cout << " for ((i=1; i <= ${#words[@]}; i++)); do\n";
234 std::cout << " case \"${words[i]}\" in\n";
235 std::cout << " -C|--project-root)\n";
236 std::cout << " if [[ $((i+1)) -le ${#words[@]} ]]; then\n";
237 std::cout << " echo \"${words[$((i+1))]}\"\n";
238 std::cout << " return\n";
239 std::cout << " fi\n";
240 std::cout << " ;;\n";
241 std::cout << " esac\n";
242 std::cout << " done\n";
243 std::cout << " echo \".\"\n";
244 std::cout << "}\n";
245 std::cout << "\n";
246
247 // Helper function for tileset completion
248 std::cout << "# Helper function to complete tileset names\n";
249 std::cout << "_porytiles_complete_tilesets() {\n";
250 std::cout << " local filter=\"$1\"\n";
251 std::cout << " local project_root\n";
252 std::cout << " project_root=$(_porytiles_get_project_root)\n";
253 std::cout << " local -a tilesets\n";
254 std::cout << " tilesets=(${(f)\"$(porytiles list-tilesets -C \"$project_root\" --filter \"$filter\" "
255 "2>/dev/null)\"})\n";
256 std::cout << " _describe 'tileset' tilesets\n";
257 std::cout << "}\n";
258 std::cout << "\n";
259
260 std::cout << "_porytiles() {\n";
261 std::cout << " local state line\n";
262 std::cout << " local -a commands\n";
263 std::cout << " commands=(\n";
264 std::cout << " 'compile-tileset:Compile a tileset'\n";
265 std::cout << " 'create-tileset:Create a new tileset'\n";
266 std::cout << " 'decompile-tileset:Decompile a tileset'\n";
267 std::cout << " 'dump-tileset-config:Dump the full configuration provenance chain for a tileset'\n";
268 std::cout << " 'import-tileset:Import a pre-existing tileset'\n";
269 std::cout << " 'list-tilesets:List tileset names in the project'\n";
270 std::cout << " 'completion:Generate shell completion scripts'\n";
271 std::cout << " )\n";
272 std::cout << "\n";
273 std::cout << " local -a config_opts\n";
274 std::cout << " config_opts=(\n";
275 std::cout << " '-C[Set project root directory]:directory:_files -/'\n";
276 std::cout << " '--project-root[Set project root directory]:directory:_files -/'\n";
277
279 for (const auto &opt : meta) {
280 std::cout << " '--" << opt.long_name << "[" << opt.description << "]";
281 if (!opt.choices.empty()) {
282 std::cout << ":choice:(";
283 for (std::size_t i = 0; i < opt.choices.size(); ++i) {
284 if (i > 0) {
285 std::cout << " ";
286 }
287 std::cout << opt.choices[i];
288 }
289 std::cout << ")";
290 }
291 std::cout << "'\n";
292 if (opt.has_negation) {
293 std::cout << " '--no-" << opt.long_name << "[Disable " << opt.description << "]'\n";
294 }
295 }
296
297 std::cout << " )\n";
298 std::cout << "\n";
299 std::cout << " _arguments -C \\\n";
300 std::cout << " '-C[Set project root directory]:directory:_files -/' \\\n";
301 std::cout << " '--project-root[Set project root directory]:directory:_files -/' \\\n";
302 std::cout << " '1:command:->command' \\\n";
303 std::cout << " '*::arg:->args'\n";
304 std::cout << "\n";
305 std::cout << " case \"$state\" in\n";
306 std::cout << " command)\n";
307 std::cout << " _describe 'command' commands\n";
308 std::cout << " ;;\n";
309 std::cout << " args)\n";
310 std::cout << " case \"$line[1]\" in\n";
311 std::cout << " compile-tileset|decompile-tileset)\n";
312 std::cout << " _arguments \\\n";
313 std::cout << " $config_opts \\\n";
314 std::cout << " '1:tileset:->tileset_managed'\n";
315 std::cout
316 << " [[ \"$state\" == tileset_managed ]] && _porytiles_complete_tilesets managed\n";
317 std::cout << " ;;\n";
318 std::cout << " dump-tileset-config)\n";
319 std::cout << " _arguments \\\n";
320 std::cout << " $config_opts \\\n";
321 std::cout << " '1:tileset:->tileset_all'\n";
322 std::cout << " [[ \"$state\" == tileset_all ]] && _porytiles_complete_tilesets all\n";
323 std::cout << " ;;\n";
324 std::cout << " import-tileset)\n";
325 std::cout << " _arguments \\\n";
326 std::cout << " $config_opts \\\n";
327 std::cout << " '1:tileset:->tileset_unmanaged'\n";
328 std::cout
329 << " [[ \"$state\" == tileset_unmanaged ]] && _porytiles_complete_tilesets unmanaged\n";
330 std::cout << " ;;\n";
331 std::cout << " create-tileset)\n";
332 std::cout << " _arguments \\\n";
333 std::cout << " $config_opts \\\n";
334 std::cout << " '--secondary[Create a secondary tileset instead of a primary]' \\\n";
335 std::cout << " '1:tileset name:'\n";
336 std::cout << " ;;\n";
337 std::cout << " list-tilesets)\n";
338 std::cout << " _arguments \\\n";
339 std::cout << " '-C[Set project root directory]:directory:_files -/' \\\n";
340 std::cout << " '--project-root[Set project root directory]:directory:_files -/' \\\n";
341 std::cout << " '--filter[Filter mode]:filter:(all managed unmanaged)' \\\n";
342 std::cout << " '--prefix[Only show tilesets starting with this prefix]:prefix:'\n";
343 std::cout << " ;;\n";
344 std::cout << " completion)\n";
345 std::cout << " _arguments '1:shell:(bash zsh fish)'\n";
346 std::cout << " ;;\n";
347 std::cout << " *)\n";
348 std::cout << " _values 'config options' $config_opts\n";
349 std::cout << " ;;\n";
350 std::cout << " esac\n";
351 std::cout << " ;;\n";
352 std::cout << " esac\n";
353 std::cout << "}\n";
354 std::cout << "\n";
355 std::cout << "compdef _porytiles porytiles\n";
356 }
357
358 void output_fish_completion()
359 {
360 std::cout << "# Fish completion for porytiles\n";
361 std::cout << "# Save to completions directory (eval does not work reliably in fish):\n";
362 std::cout << "# porytiles completion fish > ~/.config/fish/completions/porytiles.fish\n";
363 std::cout << "# Then restart fish or run: source ~/.config/fish/completions/porytiles.fish\n";
364 std::cout << "\n";
365 std::cout << "# Erase any existing completions for porytiles\n";
366 std::cout << "complete -e -c porytiles\n";
367 std::cout << "\n";
368 std::cout << "# Disable file completions globally for porytiles\n";
369 std::cout << "complete -c porytiles -f\n";
370 std::cout << "\n";
371
372 // Helper function to get project root
373 std::cout << "# Helper function to extract --project-root or -C from command line\n";
374 std::cout << "function __porytiles_get_project_root\n";
375 std::cout << " set -l tokens (commandline -opc)\n";
376 std::cout << " for i in (seq 1 (count $tokens))\n";
377 std::cout << " switch $tokens[$i]\n";
378 std::cout << " case -C --project-root\n";
379 std::cout << " set -l next (math $i + 1)\n";
380 std::cout << " if test $next -le (count $tokens)\n";
381 std::cout << " echo $tokens[$next]\n";
382 std::cout << " return\n";
383 std::cout << " end\n";
384 std::cout << " end\n";
385 std::cout << " end\n";
386 std::cout << " echo \".\"\n";
387 std::cout << "end\n";
388 std::cout << "\n";
389
390 // Helper function for tileset completion
391 std::cout << "# Helper function to complete tileset names\n";
392 std::cout << "function __porytiles_complete_tilesets\n";
393 std::cout << " set -l filter $argv[1]\n";
394 std::cout << " set -l project_root (__porytiles_get_project_root)\n";
395 std::cout << " porytiles list-tilesets -C \"$project_root\" --filter \"$filter\" 2>/dev/null\n";
396 std::cout << "end\n";
397 std::cout << "\n";
398
399 // Helper functions for context detection
400 std::cout << "# Helper functions for detecting current subcommand\n";
401 std::cout << "function __porytiles_needs_subcommand\n";
402 std::cout << " set -l cmd (commandline -opc)\n";
403 std::cout << " for word in $cmd[2..-1]\n";
404 std::cout << " switch $word\n";
405 std::cout << " case compile-tileset create-tileset decompile-tileset dump-tileset-config "
406 "import-tileset list-tilesets completion\n";
407 std::cout << " return 1\n";
408 std::cout << " end\n";
409 std::cout << " end\n";
410 std::cout << " return 0\n";
411 std::cout << "end\n";
412 std::cout << "\n";
413
414 std::cout << "function __porytiles_using_subcommand\n";
415 std::cout << " set -l cmd (commandline -opc)\n";
416 std::cout << " for word in $cmd[2..-1]\n";
417 std::cout << " if test \"$word\" = \"$argv[1]\"\n";
418 std::cout << " return 0\n";
419 std::cout << " end\n";
420 std::cout << " end\n";
421 std::cout << " return 1\n";
422 std::cout << "end\n";
423 std::cout << "\n";
424
425 // Commands
426 std::cout << "# Commands\n";
427 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a compile-tileset -d 'Compile a "
428 "tileset'\n";
429 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a create-tileset -d 'Create a new "
430 "tileset'\n";
431 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a decompile-tileset -d 'Decompile a "
432 "tileset'\n";
433 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a dump-tileset-config -d 'Dump the "
434 "full configuration provenance chain for a tileset'\n";
435 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a import-tileset -d 'Import a "
436 "pre-existing tileset'\n";
437 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a list-tilesets -d 'List tileset "
438 "names in the project'\n";
439 std::cout << "complete -c porytiles -f -n __porytiles_needs_subcommand -a completion -d 'Generate shell "
440 "completion scripts'\n";
441 std::cout << "\n";
442
443 // Global options
444 std::cout << "# Global options\n";
445 std::cout << "complete -c porytiles -f -s C -l project-root -d 'Set project root directory' -xa '(__fish_"
446 "complete_directories)'\n";
447 std::cout << "\n";
448
449 // Tileset name completions per subcommand
450 std::cout << "# Tileset name completions\n";
451 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand compile-tileset' -a "
452 "'(__porytiles_complete_tilesets managed)'\n";
453 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand decompile-tileset' -a "
454 "'(__porytiles_complete_tilesets managed)'\n";
455 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand dump-tileset-config' -a "
456 "'(__porytiles_complete_tilesets all)'\n";
457 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand import-tileset' -a "
458 "'(__porytiles_complete_tilesets unmanaged)'\n";
459 std::cout << "\n";
460
461 // Options for create-tileset subcommand
462 std::cout << "# Options for create-tileset subcommand\n";
463 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand create-tileset' -l secondary -d "
464 "'Create a secondary tileset instead of a primary'\n";
465 std::cout << "\n";
466
467 // Options for list-tilesets subcommand
468 std::cout << "# Options for list-tilesets subcommand\n";
469 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand list-tilesets' -l filter -d "
470 "'Filter mode' -xa 'all managed unmanaged'\n";
471 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand list-tilesets' -l prefix -d "
472 "'Only show tilesets starting with this prefix'\n";
473 std::cout << "\n";
474
475 // Completion for the 'completion' subcommand
476 std::cout << "# Shell type completion for 'completion' subcommand\n";
477 std::cout << "complete -c porytiles -f -n '__porytiles_using_subcommand completion' -a 'bash zsh fish'\n";
478 std::cout << "\n";
479
480 // Config options
481 std::cout << "# Config options\n";
482
484 for (const auto &opt : meta) {
485 std::cout << "complete -c porytiles -f -l " << opt.long_name << " -d '" << opt.description << "'";
486 if (!opt.choices.empty()) {
487 std::cout << " -xa '";
488 for (std::size_t i = 0; i < opt.choices.size(); ++i) {
489 if (i > 0) {
490 std::cout << " ";
491 }
492 std::cout << opt.choices[i];
493 }
494 std::cout << "'";
495 }
496 std::cout << "\n";
497 if (opt.has_negation) {
498 std::cout << "complete -c porytiles -f -l no-" << opt.long_name << " -d 'Disable " << opt.description
499 << "'\n";
500 }
501 }
502 }
503};
Command is an abstract class that provides basic command functionality for the Porytiles CLI driver.
Definition command.hpp:18
CLI::App & get_app() const
Definition command.hpp:44
Command that outputs shell completion scripts.
CompletionCommand(CLI::App &parent_app)
std::vector< CliOptionMeta > get_cli_option_metadata()
Get all CLI option metadata for completion generation.