feat: Introduce Autocomplete Engine and TUI for enhanced command input and suggestions
- Added `AutocompleteEngine` class for managing command and parameter suggestions with fuzzy matching. - Implemented TUI components for an interactive command input interface with real-time suggestions. - Enhanced user experience with a quick action menu for common tasks related to ROM manipulation. - Established a structured approach for contextual help based on user input, improving command discoverability.
This commit is contained in:
127
src/cli/tui/autocomplete_ui.cc
Normal file
127
src/cli/tui/autocomplete_ui.cc
Normal file
@@ -0,0 +1,127 @@
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include "cli/util/autocomplete.h"
|
||||
#include "cli/tui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
Component CreateAutocompleteInput(std::string* input_str,
|
||||
AutocompleteEngine* engine) {
|
||||
auto input = Input(input_str, "Type command...");
|
||||
|
||||
return Renderer(input, [=] {
|
||||
auto suggestions = engine->GetSuggestions(*input_str);
|
||||
|
||||
Elements suggestion_list;
|
||||
for (size_t i = 0; i < std::min(suggestions.size(), size_t(5)); ++i) {
|
||||
const auto& s = suggestions[i];
|
||||
suggestion_list.push_back(
|
||||
hbox({
|
||||
text("→ ") | color(Color::Cyan),
|
||||
text(s.text) | bold,
|
||||
text(" ") | flex,
|
||||
text(s.description) | dim,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return vbox({
|
||||
hbox({
|
||||
text("❯ ") | color(Color::GreenLight),
|
||||
input->Render() | flex,
|
||||
}),
|
||||
|
||||
(suggestions.empty() ? text("") :
|
||||
vbox({
|
||||
separator(),
|
||||
text("Suggestions:") | dim,
|
||||
vbox(suggestion_list),
|
||||
}) | size(HEIGHT, LESS_THAN, 8)
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Component CreateQuickActionMenu(ScreenInteractive& screen) {
|
||||
static int selected = 0;
|
||||
static const std::vector<std::string> actions = {
|
||||
"📖 Read hex at address",
|
||||
"🎨 View palette colors",
|
||||
"🔍 Search hex pattern",
|
||||
"📊 Analyze palette",
|
||||
"💾 ROM info",
|
||||
"⬅️ Back",
|
||||
};
|
||||
|
||||
auto menu = Menu(&actions, &selected);
|
||||
|
||||
return CatchEvent(menu, [&](Event e) {
|
||||
if (e == Event::Return) {
|
||||
switch (selected) {
|
||||
case 0: {
|
||||
// Quick hex read
|
||||
std::cout << "\n📖 Quick Hex Read\n";
|
||||
std::cout << "Address (hex): ";
|
||||
std::string addr;
|
||||
std::getline(std::cin, addr);
|
||||
if (!addr.empty()) {
|
||||
agent::HandleHexRead({"--address=" + addr, "--length=16", "--format=both"},
|
||||
&app_context.rom);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
std::cout << "\n🎨 Quick Palette View\n";
|
||||
std::cout << "Group (0-7): ";
|
||||
std::string group;
|
||||
std::getline(std::cin, group);
|
||||
std::cout << "Palette (0-7): ";
|
||||
std::string pal;
|
||||
std::getline(std::cin, pal);
|
||||
agent::HandlePaletteGetColors({"--group=" + group, "--palette=" + pal, "--format=hex"},
|
||||
&app_context.rom);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
std::cout << "\n🔍 Hex Pattern Search\n";
|
||||
std::cout << "Pattern (e.g. FF 00 ?? 12): ";
|
||||
std::string pattern;
|
||||
std::getline(std::cin, pattern);
|
||||
agent::HandleHexSearch({"--pattern=" + pattern}, &app_context.rom);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
std::cout << "\n📊 Palette Analysis\n";
|
||||
std::cout << "Group/Palette (e.g. 0/0): ";
|
||||
std::string id;
|
||||
std::getline(std::cin, id);
|
||||
agent::HandlePaletteAnalyze({"--type=palette", "--id=" + id}, &app_context.rom);
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
if (app_context.rom.is_loaded()) {
|
||||
std::cout << "\n💾 ROM Information\n";
|
||||
std::cout << "Title: " << app_context.rom.title() << "\n";
|
||||
std::cout << "Size: " << app_context.rom.size() << " bytes\n";
|
||||
} else {
|
||||
std::cout << "\n⚠️ No ROM loaded\n";
|
||||
}
|
||||
std::cout << "\nPress Enter to continue...";
|
||||
std::cin.get();
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
app_context.current_layout = LayoutID::kMainMenu;
|
||||
screen.ExitLoopClosure()();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
92
src/cli/util/autocomplete.cc
Normal file
92
src/cli/util/autocomplete.cc
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "cli/util/autocomplete.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
void AutocompleteEngine::RegisterCommand(const std::string& cmd, const std::string& desc,
|
||||
const std::vector<std::string>& examples) {
|
||||
CommandDef def;
|
||||
def.name = cmd;
|
||||
def.description = desc;
|
||||
def.examples = examples;
|
||||
commands_.push_back(def);
|
||||
}
|
||||
|
||||
void AutocompleteEngine::RegisterParameter(const std::string& param, const std::string& desc,
|
||||
const std::vector<std::string>& values) {
|
||||
// TODO: Store parameter definitions
|
||||
}
|
||||
|
||||
int AutocompleteEngine::FuzzyScore(const std::string& text, const std::string& query) {
|
||||
if (query.empty()) return 0;
|
||||
|
||||
std::string t = text, q = query;
|
||||
std::transform(t.begin(), t.end(), t.begin(), ::tolower);
|
||||
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
|
||||
|
||||
if (t == q) return 1000;
|
||||
if (t.find(q) == 0) return 500;
|
||||
if (t.find(q) != std::string::npos) return 250;
|
||||
|
||||
// Fuzzy char matching
|
||||
size_t ti = 0, qi = 0;
|
||||
int score = 0;
|
||||
while (ti < t.length() && qi < q.length()) {
|
||||
if (t[ti] == q[qi]) {
|
||||
score += 10;
|
||||
qi++;
|
||||
}
|
||||
ti++;
|
||||
}
|
||||
|
||||
return (qi == q.length()) ? score : 0;
|
||||
}
|
||||
|
||||
std::vector<Suggestion> AutocompleteEngine::GetSuggestions(const std::string& input) {
|
||||
std::vector<Suggestion> results;
|
||||
|
||||
for (const auto& cmd : commands_) {
|
||||
int score = FuzzyScore(cmd.name, input);
|
||||
if (score > 0) {
|
||||
Suggestion s;
|
||||
s.text = cmd.name;
|
||||
s.description = cmd.description;
|
||||
s.example = cmd.examples.empty() ? "" : cmd.examples[0];
|
||||
s.score = score;
|
||||
results.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(results.begin(), results.end(),
|
||||
[](const Suggestion& a, const Suggestion& b) {
|
||||
return a.score > b.score;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<Suggestion> AutocompleteEngine::GetContextualHelp(const std::string& partial_cmd) {
|
||||
std::vector<Suggestion> help;
|
||||
|
||||
// Parse command and suggest parameters
|
||||
if (partial_cmd.find("hex-") == 0) {
|
||||
help.push_back({"--address=0xXXXXXX", "ROM address in hex", ""});
|
||||
help.push_back({"--length=16", "Number of bytes", ""});
|
||||
help.push_back({"--format=both", "Output format (hex/ascii/both)", ""});
|
||||
} else if (partial_cmd.find("palette-") == 0) {
|
||||
help.push_back({"--group=0", "Palette group (0-7)", ""});
|
||||
help.push_back({"--palette=0", "Palette index (0-7)", ""});
|
||||
help.push_back({"--format=hex", "Color format (snes/rgb/hex)", ""});
|
||||
} else if (partial_cmd.find("dungeon-") == 0) {
|
||||
help.push_back({"--room_id=5", "Room ID (0-296)", ""});
|
||||
help.push_back({"--include_sprites=true", "Include sprite data", ""});
|
||||
}
|
||||
|
||||
return help;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
49
src/cli/util/autocomplete.h
Normal file
49
src/cli/util/autocomplete.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef YAZE_CLI_UTIL_AUTOCOMPLETE_H_
|
||||
#define YAZE_CLI_UTIL_AUTOCOMPLETE_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
struct Suggestion {
|
||||
std::string text;
|
||||
std::string description;
|
||||
std::string example;
|
||||
int score;
|
||||
};
|
||||
|
||||
class AutocompleteEngine {
|
||||
public:
|
||||
void RegisterCommand(const std::string& cmd, const std::string& desc,
|
||||
const std::vector<std::string>& examples = {});
|
||||
|
||||
void RegisterParameter(const std::string& param, const std::string& desc,
|
||||
const std::vector<std::string>& values = {});
|
||||
|
||||
std::vector<Suggestion> GetSuggestions(const std::string& input);
|
||||
|
||||
std::vector<Suggestion> GetContextualHelp(const std::string& partial_cmd);
|
||||
|
||||
void SetRomContext(bool has_rom) { has_rom_ = has_rom; }
|
||||
|
||||
private:
|
||||
struct CommandDef {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::vector<std::string> params;
|
||||
std::vector<std::string> examples;
|
||||
};
|
||||
|
||||
std::vector<CommandDef> commands_;
|
||||
bool has_rom_ = false;
|
||||
|
||||
int FuzzyScore(const std::string& text, const std::string& query);
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_CLI_UTIL_AUTOCOMPLETE_H_
|
||||
Reference in New Issue
Block a user