From e117e5aaffc01969f5fa396a6decd8bd6dbe740e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 09:31:31 -0700 Subject: [PATCH] refactor(editor): extract CLI introspection handlers into cli_introspect.cpp Moves five self-discovery handlers (--list-commands, --info-cli-stats, --info-cli-categories, --info-cli-help, --gen-completion) out of main.cpp into a new cli_introspect.{hpp,cpp} module. All five auto-discover commands by parsing printUsage's stdout via tmpfile capture, so the surface stays self-describing as new flags are added. Useful for shell completion scripts that re-exec the binary at completion time, IDE plugins, and 'is there a flag for X?' search workflows. main.cpp shrinks by 276 lines (2,298 to 2,022). --validate-cli-help stays inline because it needs direct access to the static-local kArgRequired array. --- CMakeLists.txt | 1 + tools/editor/cli_introspect.cpp | 337 ++++++++++++++++++++++++++++++++ tools/editor/cli_introspect.hpp | 23 +++ tools/editor/main.cpp | 285 +-------------------------- 4 files changed, 365 insertions(+), 281 deletions(-) create mode 100644 tools/editor/cli_introspect.cpp create mode 100644 tools/editor/cli_introspect.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cb998d27..8bf07278 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1355,6 +1355,7 @@ add_executable(wowee_editor tools/editor/cli_deps.cpp tools/editor/cli_for_each.cpp tools/editor/cli_check.cpp + tools/editor/cli_introspect.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_introspect.cpp b/tools/editor/cli_introspect.cpp new file mode 100644 index 00000000..0040ff2f --- /dev/null +++ b/tools/editor/cli_introspect.cpp @@ -0,0 +1,337 @@ +#include "cli_introspect.hpp" +#include "cli_help.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleListCommands(int& i, int argc, char** argv) { + // Capture printUsage's stdout and grep for '--flag' tokens at + // the start of each line. This auto-tracks the help text as + // commands are added — no parallel list to maintain. Result + // is a sorted, deduped, one-per-line list of recognized flags. + FILE* old = stdout; + // Temp file lets us read printUsage's output back. fmemopen + // would be cleaner but isn't available on Windows; tmpfile is + // portable. + FILE* tmp = std::tmpfile(); + if (!tmp) { std::fprintf(stderr, "list-commands: tmpfile failed\n"); return 1; } + stdout = tmp; + wowee::editor::cli::printUsage(argv[0]); + stdout = old; + std::fseek(tmp, 0, SEEK_SET); + std::set commands; + char line[512]; + while (std::fgets(line, sizeof(line), tmp)) { + // Match leading whitespace then '--' then [a-z-]+ + const char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (p[0] != '-' || p[1] != '-') continue; + std::string flag; + while (*p && (std::isalnum(static_cast(*p)) || + *p == '-' || *p == '_')) { + flag += *p++; + } + if (flag.size() > 2) commands.insert(flag); + } + std::fclose(tmp); + // Always include the meta-flags that printUsage describes + // alongside others (-h/-v aliases) since the regex above only + // captures double-dash forms. + commands.insert("--help"); + commands.insert("--version"); + for (const auto& c : commands) std::printf("%s\n", c.c_str()); + return 0; +} + +int handleInfoCliStats(int& i, int argc, char** argv) { + // Meta-stats on the CLI surface: total command count + per- + // category breakdown by prefix verb (--info-*, --validate-*, + // --diff-*, etc.). Useful for tracking growth over time and + // spotting category imbalances. + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + // Re-use --list-commands' parser. Capture printUsage stdout. + FILE* old = stdout; + FILE* tmp = std::tmpfile(); + if (!tmp) { std::fprintf(stderr, "info-cli-stats: tmpfile failed\n"); return 1; } + stdout = tmp; + wowee::editor::cli::printUsage(argv[0]); + stdout = old; + std::fseek(tmp, 0, SEEK_SET); + std::set commands; + char line[512]; + while (std::fgets(line, sizeof(line), tmp)) { + const char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (p[0] != '-' || p[1] != '-') continue; + std::string flag; + while (*p && (std::isalnum(static_cast(*p)) || + *p == '-' || *p == '_')) { flag += *p++; } + if (flag.size() > 2) commands.insert(flag); + } + std::fclose(tmp); + commands.insert("--help"); + commands.insert("--version"); + // Bucket by category — verb is the second token after '--', + // up to the next dash. So '--info-zone-tree' -> 'info'. + std::map byCategory; + int maxLen = 0; + for (const auto& c : commands) { + if (static_cast(c.size()) > maxLen) maxLen = static_cast(c.size()); + size_t verbStart = 2; // skip '--' + size_t verbEnd = c.find('-', verbStart); + std::string verb = (verbEnd == std::string::npos) + ? c.substr(verbStart) + : c.substr(verbStart, verbEnd - verbStart); + byCategory[verb]++; + } + if (jsonOut) { + nlohmann::json j; + j["totalCommands"] = commands.size(); + j["maxFlagLength"] = maxLen; + nlohmann::json cats = nlohmann::json::object(); + for (const auto& [v, c] : byCategory) cats[v] = c; + j["byCategory"] = cats; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("CLI surface stats\n"); + std::printf(" total commands : %zu\n", commands.size()); + std::printf(" longest flag : %d chars\n", maxLen); + std::printf("\n Categories (by verb prefix, sorted by count):\n"); + // Sort by count descending for the table. + std::vector> sorted( + byCategory.begin(), byCategory.end()); + std::sort(sorted.begin(), sorted.end(), + [](const auto& a, const auto& b) { + return a.second > b.second; + }); + for (const auto& [verb, count] : sorted) { + std::printf(" --%-12s %4d\n", verb.c_str(), count); + } + return 0; +} + +int handleInfoCliCategories(int& i, int argc, char** argv) { + // Discovery view of every CLI flag grouped by verb prefix. + // Where --info-cli-stats just counts per category, this + // lists every command in each category — handy for "I + // know I want to gen something but what shapes/textures + // are available?" + FILE* old = stdout; + FILE* tmp = std::tmpfile(); + if (!tmp) { + std::fprintf(stderr, "info-cli-categories: tmpfile failed\n"); + return 1; + } + stdout = tmp; + wowee::editor::cli::printUsage(argv[0]); + stdout = old; + std::fseek(tmp, 0, SEEK_SET); + std::set commands; + char line[512]; + while (std::fgets(line, sizeof(line), tmp)) { + const char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (p[0] != '-' || p[1] != '-') continue; + std::string flag; + while (*p && (std::isalnum(static_cast(*p)) || + *p == '-' || *p == '_')) { flag += *p++; } + if (flag.size() > 2) commands.insert(flag); + } + std::fclose(tmp); + commands.insert("--help"); + commands.insert("--version"); + std::map> byCategory; + for (const auto& c : commands) { + size_t verbStart = 2; + size_t verbEnd = c.find('-', verbStart); + std::string verb = (verbEnd == std::string::npos) + ? c.substr(verbStart) + : c.substr(verbStart, verbEnd - verbStart); + byCategory[verb].push_back(c); + } + std::printf("CLI commands by category (%zu total):\n\n", + commands.size()); + // Sort categories by count descending, commands within + // each alphabetically. + std::vector>> sorted( + byCategory.begin(), byCategory.end()); + std::sort(sorted.begin(), sorted.end(), + [](const auto& a, const auto& b) { + if (a.second.size() != b.second.size()) + return a.second.size() > b.second.size(); + return a.first < b.first; + }); + for (const auto& [verb, cmds] : sorted) { + std::printf("--%s (%zu):\n", verb.c_str(), cmds.size()); + for (const auto& c : cmds) { + std::printf(" %s\n", c.c_str()); + } + std::printf("\n"); + } + return 0; +} + +int handleInfoCliHelp(int& i, int argc, char** argv) { + // Substring search through the help text. With 130+ commands, + // 'is there a thing for X?' is a common ask — this answers it + // without making the user scroll the full --help output: + // + // wowee_editor --info-cli-help quest + // wowee_editor --info-cli-help validate + // wowee_editor --info-cli-help glb + std::string pattern = argv[++i]; + // Lowercase the pattern for case-insensitive match. + std::string patLower = pattern; + for (auto& c : patLower) c = std::tolower(static_cast(c)); + // Capture printUsage stdout, walk line-by-line, print every + // line containing the pattern (case-insensitive). Continuation + // lines (the indented description on the line after a flag) + // are emitted along with the flag line for context. + FILE* old = stdout; + FILE* tmp = std::tmpfile(); + if (!tmp) { + std::fprintf(stderr, "info-cli-help: tmpfile failed\n"); return 1; + } + stdout = tmp; + wowee::editor::cli::printUsage(argv[0]); + stdout = old; + std::fseek(tmp, 0, SEEK_SET); + std::vector lines; + char buf[1024]; + while (std::fgets(buf, sizeof(buf), tmp)) { + std::string s = buf; + if (!s.empty() && s.back() == '\n') s.pop_back(); + lines.push_back(std::move(s)); + } + std::fclose(tmp); + int matches = 0; + for (size_t k = 0; k < lines.size(); ++k) { + std::string lower = lines[k]; + for (auto& c : lower) c = std::tolower(static_cast(c)); + if (lower.find(patLower) == std::string::npos) continue; + std::printf("%s\n", lines[k].c_str()); + // Look ahead for a continuation line (indented and not + // starting with '--'). Print it for context. + if (k + 1 < lines.size()) { + const auto& next = lines[k + 1]; + if (!next.empty() && next[0] == ' ' && + next.find("--") == std::string::npos) { + std::printf("%s\n", next.c_str()); + } + } + matches++; + } + if (matches == 0) { + std::fprintf(stderr, "info-cli-help: no matches for '%s'\n", + pattern.c_str()); + return 1; + } + std::fprintf(stderr, "\n%d line(s) matched '%s'\n", matches, pattern.c_str()); + return 0; +} + +int handleGenCompletion(int& i, int argc, char** argv) { + // Emit a bash or zsh completion script. Re-execs the editor's + // own --list-commands at completion time so newly-added flags + // light up automatically without regenerating the script. + std::string shell = argv[++i]; + if (shell != "bash" && shell != "zsh") { + std::fprintf(stderr, + "gen-completion: shell must be 'bash' or 'zsh', got '%s'\n", + shell.c_str()); + return 1; + } + // Use argv[0] as the binary name in the completion so it + // works whether the user installed it as 'wowee_editor' or + // a custom alias. Strip directory components for the + // completion-name registration (bash 'complete -F' expects + // a basename). + std::string self = argv[0]; + auto slash = self.find_last_of('/'); + std::string baseName = (slash != std::string::npos) + ? self.substr(slash + 1) + : self; + if (shell == "bash") { + std::printf( + "# wowee_editor bash completion — source from ~/.bashrc:\n" + "# source <(%s --gen-completion bash)\n" + "_wowee_editor_complete() {\n" + " local cur prev cmds\n" + " COMPREPLY=()\n" + " cur=\"${COMP_WORDS[COMP_CWORD]}\"\n" + " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n" + " # Cache the command list per shell session.\n" + " if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n" + " _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n" + " fi\n" + " if [[ \"$cur\" == --* ]]; then\n" + " COMPREPLY=( $(compgen -W \"$_WOWEE_EDITOR_CMDS\" -- \"$cur\") )\n" + " return 0\n" + " fi\n" + " # Default: complete file paths for arg slots.\n" + " COMPREPLY=( $(compgen -f -- \"$cur\") )\n" + "}\n" + "complete -F _wowee_editor_complete %s\n", + self.c_str(), self.c_str(), baseName.c_str()); + } else { + // zsh — simpler descriptor-based completion. + std::printf( + "# wowee_editor zsh completion — source from ~/.zshrc:\n" + "# source <(%s --gen-completion zsh)\n" + "_wowee_editor_complete() {\n" + " local -a cmds\n" + " if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n" + " export _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n" + " fi\n" + " cmds=( ${(f)_WOWEE_EDITOR_CMDS} )\n" + " _arguments \"*: :($cmds)\"\n" + "}\n" + "compdef _wowee_editor_complete %s\n", + self.c_str(), self.c_str(), baseName.c_str()); + } + return 0; +} + + +} // namespace + +bool handleIntrospect(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--list-commands") == 0) { + outRc = handleListCommands(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-cli-stats") == 0) { + outRc = handleInfoCliStats(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-cli-categories") == 0) { + outRc = handleInfoCliCategories(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-cli-help") == 0 && i + 1 < argc) { + outRc = handleInfoCliHelp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-completion") == 0 && i + 1 < argc) { + outRc = handleGenCompletion(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_introspect.hpp b/tools/editor/cli_introspect.hpp new file mode 100644 index 00000000..2896d06e --- /dev/null +++ b/tools/editor/cli_introspect.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the CLI introspection / discoverability handlers — +// auto-discover commands by parsing printUsage's output so the +// surface stays self-describing as new flags are added. Useful +// for shell completion, IDE plugins, and 'is there a flag for X?' +// search workflows. +// --list-commands flat sorted/deduped flag list +// --info-cli-stats per-category counts (--info-* / --validate-*) +// --info-cli-categories per-category command listing +// --info-cli-help substring search through help text +// --gen-completion re-exec'd at completion time +// +// Returns true if matched; outRc holds the exit code. +bool handleIntrospect(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 016a9556..24ca947a 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -56,6 +56,7 @@ #include "cli_deps.hpp" #include "cli_for_each.hpp" #include "cli_check.hpp" +#include "cli_introspect.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -565,6 +566,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleCheck(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleIntrospect(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1944,227 +1948,6 @@ int main(int argc, char* argv[]) { std::printf("Open formats: WOT/WHM/WOM/WOB/WOC/WCP + PNG/JSON (all novel)\n"); std::printf("By Kelsi Davis\n"); return 0; - } else if (std::strcmp(argv[i], "--list-commands") == 0) { - // Capture printUsage's stdout and grep for '--flag' tokens at - // the start of each line. This auto-tracks the help text as - // commands are added — no parallel list to maintain. Result - // is a sorted, deduped, one-per-line list of recognized flags. - FILE* old = stdout; - // Temp file lets us read printUsage's output back. fmemopen - // would be cleaner but isn't available on Windows; tmpfile is - // portable. - FILE* tmp = std::tmpfile(); - if (!tmp) { std::fprintf(stderr, "list-commands: tmpfile failed\n"); return 1; } - stdout = tmp; - wowee::editor::cli::printUsage(argv[0]); - stdout = old; - std::fseek(tmp, 0, SEEK_SET); - std::set commands; - char line[512]; - while (std::fgets(line, sizeof(line), tmp)) { - // Match leading whitespace then '--' then [a-z-]+ - const char* p = line; - while (*p == ' ' || *p == '\t') ++p; - if (p[0] != '-' || p[1] != '-') continue; - std::string flag; - while (*p && (std::isalnum(static_cast(*p)) || - *p == '-' || *p == '_')) { - flag += *p++; - } - if (flag.size() > 2) commands.insert(flag); - } - std::fclose(tmp); - // Always include the meta-flags that printUsage describes - // alongside others (-h/-v aliases) since the regex above only - // captures double-dash forms. - commands.insert("--help"); - commands.insert("--version"); - for (const auto& c : commands) std::printf("%s\n", c.c_str()); - return 0; - } else if (std::strcmp(argv[i], "--info-cli-stats") == 0) { - // Meta-stats on the CLI surface: total command count + per- - // category breakdown by prefix verb (--info-*, --validate-*, - // --diff-*, etc.). Useful for tracking growth over time and - // spotting category imbalances. - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - // Re-use --list-commands' parser. Capture printUsage stdout. - FILE* old = stdout; - FILE* tmp = std::tmpfile(); - if (!tmp) { std::fprintf(stderr, "info-cli-stats: tmpfile failed\n"); return 1; } - stdout = tmp; - wowee::editor::cli::printUsage(argv[0]); - stdout = old; - std::fseek(tmp, 0, SEEK_SET); - std::set commands; - char line[512]; - while (std::fgets(line, sizeof(line), tmp)) { - const char* p = line; - while (*p == ' ' || *p == '\t') ++p; - if (p[0] != '-' || p[1] != '-') continue; - std::string flag; - while (*p && (std::isalnum(static_cast(*p)) || - *p == '-' || *p == '_')) { flag += *p++; } - if (flag.size() > 2) commands.insert(flag); - } - std::fclose(tmp); - commands.insert("--help"); - commands.insert("--version"); - // Bucket by category — verb is the second token after '--', - // up to the next dash. So '--info-zone-tree' -> 'info'. - std::map byCategory; - int maxLen = 0; - for (const auto& c : commands) { - if (static_cast(c.size()) > maxLen) maxLen = static_cast(c.size()); - size_t verbStart = 2; // skip '--' - size_t verbEnd = c.find('-', verbStart); - std::string verb = (verbEnd == std::string::npos) - ? c.substr(verbStart) - : c.substr(verbStart, verbEnd - verbStart); - byCategory[verb]++; - } - if (jsonOut) { - nlohmann::json j; - j["totalCommands"] = commands.size(); - j["maxFlagLength"] = maxLen; - nlohmann::json cats = nlohmann::json::object(); - for (const auto& [v, c] : byCategory) cats[v] = c; - j["byCategory"] = cats; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("CLI surface stats\n"); - std::printf(" total commands : %zu\n", commands.size()); - std::printf(" longest flag : %d chars\n", maxLen); - std::printf("\n Categories (by verb prefix, sorted by count):\n"); - // Sort by count descending for the table. - std::vector> sorted( - byCategory.begin(), byCategory.end()); - std::sort(sorted.begin(), sorted.end(), - [](const auto& a, const auto& b) { - return a.second > b.second; - }); - for (const auto& [verb, count] : sorted) { - std::printf(" --%-12s %4d\n", verb.c_str(), count); - } - return 0; - } else if (std::strcmp(argv[i], "--info-cli-categories") == 0) { - // Discovery view of every CLI flag grouped by verb prefix. - // Where --info-cli-stats just counts per category, this - // lists every command in each category — handy for "I - // know I want to gen something but what shapes/textures - // are available?" - FILE* old = stdout; - FILE* tmp = std::tmpfile(); - if (!tmp) { - std::fprintf(stderr, "info-cli-categories: tmpfile failed\n"); - return 1; - } - stdout = tmp; - wowee::editor::cli::printUsage(argv[0]); - stdout = old; - std::fseek(tmp, 0, SEEK_SET); - std::set commands; - char line[512]; - while (std::fgets(line, sizeof(line), tmp)) { - const char* p = line; - while (*p == ' ' || *p == '\t') ++p; - if (p[0] != '-' || p[1] != '-') continue; - std::string flag; - while (*p && (std::isalnum(static_cast(*p)) || - *p == '-' || *p == '_')) { flag += *p++; } - if (flag.size() > 2) commands.insert(flag); - } - std::fclose(tmp); - commands.insert("--help"); - commands.insert("--version"); - std::map> byCategory; - for (const auto& c : commands) { - size_t verbStart = 2; - size_t verbEnd = c.find('-', verbStart); - std::string verb = (verbEnd == std::string::npos) - ? c.substr(verbStart) - : c.substr(verbStart, verbEnd - verbStart); - byCategory[verb].push_back(c); - } - std::printf("CLI commands by category (%zu total):\n\n", - commands.size()); - // Sort categories by count descending, commands within - // each alphabetically. - std::vector>> sorted( - byCategory.begin(), byCategory.end()); - std::sort(sorted.begin(), sorted.end(), - [](const auto& a, const auto& b) { - if (a.second.size() != b.second.size()) - return a.second.size() > b.second.size(); - return a.first < b.first; - }); - for (const auto& [verb, cmds] : sorted) { - std::printf("--%s (%zu):\n", verb.c_str(), cmds.size()); - for (const auto& c : cmds) { - std::printf(" %s\n", c.c_str()); - } - std::printf("\n"); - } - return 0; - } else if (std::strcmp(argv[i], "--info-cli-help") == 0 && i + 1 < argc) { - // Substring search through the help text. With 130+ commands, - // 'is there a thing for X?' is a common ask — this answers it - // without making the user scroll the full --help output: - // - // wowee_editor --info-cli-help quest - // wowee_editor --info-cli-help validate - // wowee_editor --info-cli-help glb - std::string pattern = argv[++i]; - // Lowercase the pattern for case-insensitive match. - std::string patLower = pattern; - for (auto& c : patLower) c = std::tolower(static_cast(c)); - // Capture printUsage stdout, walk line-by-line, print every - // line containing the pattern (case-insensitive). Continuation - // lines (the indented description on the line after a flag) - // are emitted along with the flag line for context. - FILE* old = stdout; - FILE* tmp = std::tmpfile(); - if (!tmp) { - std::fprintf(stderr, "info-cli-help: tmpfile failed\n"); return 1; - } - stdout = tmp; - wowee::editor::cli::printUsage(argv[0]); - stdout = old; - std::fseek(tmp, 0, SEEK_SET); - std::vector lines; - char buf[1024]; - while (std::fgets(buf, sizeof(buf), tmp)) { - std::string s = buf; - if (!s.empty() && s.back() == '\n') s.pop_back(); - lines.push_back(std::move(s)); - } - std::fclose(tmp); - int matches = 0; - for (size_t k = 0; k < lines.size(); ++k) { - std::string lower = lines[k]; - for (auto& c : lower) c = std::tolower(static_cast(c)); - if (lower.find(patLower) == std::string::npos) continue; - std::printf("%s\n", lines[k].c_str()); - // Look ahead for a continuation line (indented and not - // starting with '--'). Print it for context. - if (k + 1 < lines.size()) { - const auto& next = lines[k + 1]; - if (!next.empty() && next[0] == ' ' && - next.find("--") == std::string::npos) { - std::printf("%s\n", next.c_str()); - } - } - matches++; - } - if (matches == 0) { - std::fprintf(stderr, "info-cli-help: no matches for '%s'\n", - pattern.c_str()); - return 1; - } - std::fprintf(stderr, "\n%d line(s) matched '%s'\n", matches, pattern.c_str()); - return 0; } else if (std::strcmp(argv[i], "--validate-cli-help") == 0) { // Self-check: every flag we declare in kArgRequired (the list // of commands needing positional args) must appear in the @@ -2210,66 +1993,6 @@ int main(int argc, char* argv[]) { std::printf(" FAILED — %zu flag(s) missing from help text:\n", missing.size()); for (const auto& m : missing) std::printf(" - %s\n", m.c_str()); return 1; - } else if (std::strcmp(argv[i], "--gen-completion") == 0 && i + 1 < argc) { - // Emit a bash or zsh completion script. Re-execs the editor's - // own --list-commands at completion time so newly-added flags - // light up automatically without regenerating the script. - std::string shell = argv[++i]; - if (shell != "bash" && shell != "zsh") { - std::fprintf(stderr, - "gen-completion: shell must be 'bash' or 'zsh', got '%s'\n", - shell.c_str()); - return 1; - } - // Use argv[0] as the binary name in the completion so it - // works whether the user installed it as 'wowee_editor' or - // a custom alias. Strip directory components for the - // completion-name registration (bash 'complete -F' expects - // a basename). - std::string self = argv[0]; - auto slash = self.find_last_of('/'); - std::string baseName = (slash != std::string::npos) - ? self.substr(slash + 1) - : self; - if (shell == "bash") { - std::printf( - "# wowee_editor bash completion — source from ~/.bashrc:\n" - "# source <(%s --gen-completion bash)\n" - "_wowee_editor_complete() {\n" - " local cur prev cmds\n" - " COMPREPLY=()\n" - " cur=\"${COMP_WORDS[COMP_CWORD]}\"\n" - " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n" - " # Cache the command list per shell session.\n" - " if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n" - " _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n" - " fi\n" - " if [[ \"$cur\" == --* ]]; then\n" - " COMPREPLY=( $(compgen -W \"$_WOWEE_EDITOR_CMDS\" -- \"$cur\") )\n" - " return 0\n" - " fi\n" - " # Default: complete file paths for arg slots.\n" - " COMPREPLY=( $(compgen -f -- \"$cur\") )\n" - "}\n" - "complete -F _wowee_editor_complete %s\n", - self.c_str(), self.c_str(), baseName.c_str()); - } else { - // zsh — simpler descriptor-based completion. - std::printf( - "# wowee_editor zsh completion — source from ~/.zshrc:\n" - "# source <(%s --gen-completion zsh)\n" - "_wowee_editor_complete() {\n" - " local -a cmds\n" - " if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n" - " export _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n" - " fi\n" - " cmds=( ${(f)_WOWEE_EDITOR_CMDS} )\n" - " _arguments \"*: :($cmds)\"\n" - "}\n" - "compdef _wowee_editor_complete %s\n", - self.c_str(), self.c_str(), baseName.c_str()); - } - return 0; } else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) { wowee::editor::cli::printUsage(argv[0]); return 0;