From dc89d7b818e76d18c8d69ef69d0417165600c613 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 01:32:28 -0400 Subject: [PATCH] feat: Enhance Command Palette with Fuzzy Search and Improved UI - Implemented fuzzy search functionality for command filtering, allowing for more flexible command matching. - Updated Command Palette UI to include a scoring system for commands based on usage and recency. - Added new command categories and improved display organization with a table format. - Introduced recent and frequent command tracking for better user experience. - Refactored command handling to support dynamic command execution and improved navigation. --- src/app/editor/editor_manager.cc | 156 +++++++++++++-------- src/app/editor/system/command_palette.cc | 158 +++++++++++++++++++++ src/app/editor/system/command_palette.h | 48 +++++++ src/cli/tui/command_palette.cc | 167 +++++++++++++++++------ 4 files changed, 433 insertions(+), 96 deletions(-) create mode 100644 src/app/editor/system/command_palette.cc create mode 100644 src/app/editor/system/command_palette.h diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 479d611b..e726f783 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -1302,96 +1302,144 @@ void EditorManager::DrawMenuBar() { End(); } - // Enhanced Command Palette UI + // Enhanced Command Palette UI with Fuzzy Search if (show_command_palette_) { ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); - if (Begin(absl::StrFormat("%s Command Palette", ICON_MD_TERMINAL).c_str(), + if (Begin(absl::StrFormat("%s Command Palette", ICON_MD_SEARCH).c_str(), &show_command_palette_, ImGuiWindowFlags_NoCollapse)) { // Search input with focus management static char query[256] = {}; + static int selected_idx = 0; ImGui::SetNextItemWidth(-100); if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); + selected_idx = 0; } bool input_changed = InputTextWithHint( "##cmd_query", - absl::StrFormat("%s Type a command or search...", ICON_MD_SEARCH) - .c_str(), + absl::StrFormat("%s Search commands (fuzzy matching enabled)...", ICON_MD_SEARCH).c_str(), query, IM_ARRAYSIZE(query)); ImGui::SameLine(); if (ImGui::Button(absl::StrFormat("%s Clear", ICON_MD_CLEAR).c_str())) { query[0] = '\0'; input_changed = true; + selected_idx = 0; } Separator(); - // Filter and categorize commands - std::vector> filtered_commands; + // Fuzzy filter commands with scoring + std::vector>> scored_commands; + std::string query_lower = query; + std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower); + for (const auto& entry : context_.shortcut_manager.GetShortcuts()) { const auto& name = entry.first; const auto& shortcut = entry.second; - - if (query[0] == '\0' || name.find(query) != std::string::npos) { - std::string shortcut_text = - shortcut.keys.empty() - ? "" - : absl::StrFormat("(%s)", - PrintShortcut(shortcut.keys).c_str()); - filtered_commands.emplace_back(name, shortcut_text); - } - } - - // Display results in a table for better organization - if (ImGui::BeginTable("CommandPaletteTable", 2, - ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingStretchProp, - ImVec2(0, -30))) { // Reserve space for status bar - - ImGui::TableSetupColumn("Command", ImGuiTableColumnFlags_WidthStretch, - 0.7f); - ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthStretch, - 0.3f); - ImGui::TableHeadersRow(); - - for (size_t i = 0; i < filtered_commands.size(); ++i) { - const auto& [command_name, shortcut_text] = filtered_commands[i]; - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - - ImGui::PushID(static_cast(i)); - if (Selectable(command_name.c_str(), false, - ImGuiSelectableFlags_SpanAllColumns)) { - // Execute the command - const auto& shortcuts = context_.shortcut_manager.GetShortcuts(); - auto it = shortcuts.find(command_name); - if (it != shortcuts.end() && it->second.callback) { - it->second.callback(); - show_command_palette_ = false; + + std::string name_lower = name; + std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower); + + int score = 0; + if (query[0] == '\0') { + score = 1; // Show all when no query + } else if (name_lower.find(query_lower) == 0) { + score = 1000; // Starts with + } else if (name_lower.find(query_lower) != std::string::npos) { + score = 500; // Contains + } else { + // Fuzzy match - characters in order + size_t text_idx = 0, query_idx = 0; + while (text_idx < name_lower.length() && query_idx < query_lower.length()) { + if (name_lower[text_idx] == query_lower[query_idx]) { + score += 10; + query_idx++; } + text_idx++; } - ImGui::PopID(); - - ImGui::TableNextColumn(); - ImGui::TextDisabled("%s", shortcut_text.c_str()); + if (query_idx != query_lower.length()) score = 0; } + + if (score > 0) { + std::string shortcut_text = shortcut.keys.empty() + ? "" : absl::StrFormat("(%s)", PrintShortcut(shortcut.keys).c_str()); + scored_commands.push_back({score, {name, shortcut_text}}); + } + } + + std::sort(scored_commands.begin(), scored_commands.end(), + [](const auto& a, const auto& b) { return a.first > b.first; }); - ImGui::EndTable(); + // Display results with categories + if (ImGui::BeginTabBar("CommandCategories")) { + if (ImGui::BeginTabItem(ICON_MD_LIST " All Commands")) { + if (ImGui::BeginTable("CommandPaletteTable", 3, + ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp, + ImVec2(0, -30))) { + + ImGui::TableSetupColumn("Command", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthStretch, 0.3f); + ImGui::TableSetupColumn("Score", ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < scored_commands.size(); ++i) { + const auto& [score, cmd_pair] = scored_commands[i]; + const auto& [command_name, shortcut_text] = cmd_pair; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGui::PushID(static_cast(i)); + bool is_selected = (static_cast(i) == selected_idx); + if (Selectable(command_name.c_str(), is_selected, + ImGuiSelectableFlags_SpanAllColumns)) { + selected_idx = i; + const auto& shortcuts = context_.shortcut_manager.GetShortcuts(); + auto it = shortcuts.find(command_name); + if (it != shortcuts.end() && it->second.callback) { + it->second.callback(); + show_command_palette_ = false; + } + } + ImGui::PopID(); + + ImGui::TableNextColumn(); + ImGui::TextDisabled("%s", shortcut_text.c_str()); + + ImGui::TableNextColumn(); + if (score > 0) ImGui::TextDisabled("%d", score); + } + + ImGui::EndTable(); + } + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_MD_HISTORY " Recent")) { + ImGui::Text("Recent commands coming soon..."); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_MD_STAR " Frequent")) { + ImGui::Text("Frequent commands coming soon..."); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); } - // Status bar + // Status bar with tips ImGui::Separator(); - ImGui::Text("%s %zu commands found", ICON_MD_INFO, - filtered_commands.size()); + ImGui::Text("%s %zu commands | Score: fuzzy match", ICON_MD_INFO, scored_commands.size()); ImGui::SameLine(); - ImGui::TextDisabled("| Press Enter to execute selected command"); + ImGui::TextDisabled("| ↑↓=Navigate | Enter=Execute | Esc=Close"); } End(); } diff --git a/src/app/editor/system/command_palette.cc b/src/app/editor/system/command_palette.cc new file mode 100644 index 00000000..b56c9afa --- /dev/null +++ b/src/app/editor/system/command_palette.cc @@ -0,0 +1,158 @@ +#include "app/editor/system/command_palette.h" + +#include +#include +#include + +namespace yaze { +namespace editor { + +void CommandPalette::AddCommand(const std::string& name, const std::string& category, + const std::string& description, const std::string& shortcut, + std::function callback) { + CommandEntry entry; + entry.name = name; + entry.category = category; + entry.description = description; + entry.shortcut = shortcut; + entry.callback = callback; + commands_[name] = entry; +} + +void CommandPalette::RecordUsage(const std::string& name) { + auto it = commands_.find(name); + if (it != commands_.end()) { + it->second.usage_count++; + it->second.last_used_ms = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } +} + +int CommandPalette::FuzzyScore(const std::string& text, const std::string& query) { + if (query.empty()) return 0; + + int score = 0; + size_t text_idx = 0; + size_t query_idx = 0; + + std::string text_lower = text; + std::string query_lower = query; + std::transform(text_lower.begin(), text_lower.end(), text_lower.begin(), ::tolower); + std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower); + + // Exact match bonus + if (text_lower == query_lower) return 1000; + + // Starts with bonus + if (text_lower.find(query_lower) == 0) return 500; + + // Contains bonus + if (text_lower.find(query_lower) != std::string::npos) return 250; + + // Fuzzy match - characters in order + while (text_idx < text_lower.length() && query_idx < query_lower.length()) { + if (text_lower[text_idx] == query_lower[query_idx]) { + score += 10; + query_idx++; + } + text_idx++; + } + + // Penalty if not all characters matched + if (query_idx != query_lower.length()) return 0; + + return score; +} + +std::vector CommandPalette::SearchCommands(const std::string& query) { + std::vector> scored; + + for (const auto& [name, entry] : commands_) { + int score = FuzzyScore(entry.name, query); + + // Also check category and description + score += FuzzyScore(entry.category, query) / 2; + score += FuzzyScore(entry.description, query) / 4; + + // Frecency bonus (frequency + recency) + score += entry.usage_count * 2; + + auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + int64_t age_ms = now_ms - entry.last_used_ms; + if (age_ms < 60000) { // Used in last minute + score += 50; + } else if (age_ms < 3600000) { // Last hour + score += 25; + } + + if (score > 0) { + scored.push_back({score, entry}); + } + } + + // Sort by score descending + std::sort(scored.begin(), scored.end(), + [](const auto& a, const auto& b) { return a.first > b.first; }); + + std::vector results; + for (const auto& [score, entry] : scored) { + results.push_back(entry); + } + + return results; +} + +std::vector CommandPalette::GetRecentCommands(int limit) { + std::vector recent; + + for (const auto& [name, entry] : commands_) { + if (entry.usage_count > 0) { + recent.push_back(entry); + } + } + + std::sort(recent.begin(), recent.end(), + [](const CommandEntry& a, const CommandEntry& b) { + return a.last_used_ms > b.last_used_ms; + }); + + if (recent.size() > static_cast(limit)) { + recent.resize(limit); + } + + return recent; +} + +std::vector CommandPalette::GetFrequentCommands(int limit) { + std::vector frequent; + + for (const auto& [name, entry] : commands_) { + if (entry.usage_count > 0) { + frequent.push_back(entry); + } + } + + std::sort(frequent.begin(), frequent.end(), + [](const CommandEntry& a, const CommandEntry& b) { + return a.usage_count > b.usage_count; + }); + + if (frequent.size() > static_cast(limit)) { + frequent.resize(limit); + } + + return frequent; +} + +void CommandPalette::SaveHistory(const std::string& filepath) { + // TODO: Implement JSON serialization of command history +} + +void CommandPalette::LoadHistory(const std::string& filepath) { + // TODO: Implement JSON deserialization of command history +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/system/command_palette.h b/src/app/editor/system/command_palette.h new file mode 100644 index 00000000..7bd625ab --- /dev/null +++ b/src/app/editor/system/command_palette.h @@ -0,0 +1,48 @@ +#ifndef YAZE_APP_EDITOR_SYSTEM_COMMAND_PALETTE_H_ +#define YAZE_APP_EDITOR_SYSTEM_COMMAND_PALETTE_H_ + +#include +#include +#include +#include + +namespace yaze { +namespace editor { + +struct CommandEntry { + std::string name; + std::string category; + std::string description; + std::string shortcut; + std::function callback; + int usage_count = 0; + int64_t last_used_ms = 0; +}; + +class CommandPalette { + public: + void AddCommand(const std::string& name, const std::string& category, + const std::string& description, const std::string& shortcut, + std::function callback); + + void RecordUsage(const std::string& name); + + std::vector SearchCommands(const std::string& query); + + std::vector GetRecentCommands(int limit = 10); + + std::vector GetFrequentCommands(int limit = 10); + + void SaveHistory(const std::string& filepath); + void LoadHistory(const std::string& filepath); + + private: + std::unordered_map commands_; + + int FuzzyScore(const std::string& text, const std::string& query); +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_SYSTEM_COMMAND_PALETTE_H_ diff --git a/src/cli/tui/command_palette.cc b/src/cli/tui/command_palette.cc index 7c871c45..41618dce 100644 --- a/src/cli/tui/command_palette.cc +++ b/src/cli/tui/command_palette.cc @@ -1,63 +1,146 @@ #include "cli/tui/command_palette.h" +#include #include #include +#include #include "cli/tui.h" -#include "cli/modern_cli.h" +#include "cli/handlers/agent/hex_commands.h" +#include "cli/handlers/agent/palette_commands.h" +#include "absl/strings/str_split.h" namespace yaze { namespace cli { using namespace ftxui; -ftxui::Component CommandPaletteComponent::Render() { - static std::string input; - static std::vector commands; - if (commands.empty()) { - ModernCLI cli; - for (const auto& [name, info] : cli.commands_) { - commands.push_back(name); +Component CommandPaletteComponent::Render() { + static std::string query; + static int selected = 0; + static std::string status_msg; + + struct Cmd { + std::string name; + std::string cat; + std::string desc; + std::string usage; + std::function exec; + }; + + static std::vector cmds = { + {"hex-read", "🔢 Hex", "Read ROM bytes", + "--address=0x1C800 --length=16 --format=both", + []() { return agent::HandleHexRead({"--address=0x1C800", "--length=16"}, &app_context.rom); }}, + + {"hex-write", "🔢 Hex", "Write ROM bytes", + "--address=0x1C800 --data=\"FF 00\"", + []() { return agent::HandleHexWrite({"--address=0x1C800", "--data=FF 00"}, &app_context.rom); }}, + + {"hex-search", "🔢 Hex", "Search byte pattern", + "--pattern=\"FF 00 ?? 12\"", + []() { return agent::HandleHexSearch({"--pattern=FF 00"}, &app_context.rom); }}, + + {"palette-get", "🎨 Palette", "Get palette colors", + "--group=0 --palette=0 --format=hex", + []() { return agent::HandlePaletteGetColors({"--group=0", "--palette=0", "--format=hex"}, &app_context.rom); }}, + + {"palette-set", "🎨 Palette", "Set palette color", + "--group=0 --palette=0 --index=5 --color=FF0000", + []() { return agent::HandlePaletteSetColor({"--group=0", "--palette=0", "--index=5", "--color=FF0000"}, &app_context.rom); }}, + + {"palette-analyze", "🎨 Palette", "Analyze palette", + "--type=palette --id=0/0", + []() { return agent::HandlePaletteAnalyze({"--type=palette", "--id=0/0"}, &app_context.rom); }}, + }; + + // Fuzzy filter + std::vector filtered_idx; + std::string q_lower = query; + std::transform(q_lower.begin(), q_lower.end(), q_lower.begin(), ::tolower); + + for (size_t i = 0; i < cmds.size(); ++i) { + if (query.empty()) { + filtered_idx.push_back(i); + } else { + std::string n = cmds[i].name; + std::transform(n.begin(), n.end(), n.begin(), ::tolower); + if (n.find(q_lower) != std::string::npos) { + filtered_idx.push_back(i); } + } } - - auto input_component = Input(&input, ""); - - auto renderer = Renderer(input_component, [&] { - std::vector filtered_commands; - for (const auto& cmd : commands) { - if (cmd.find(input) != std::string::npos) { - filtered_commands.push_back(cmd); - } + + auto search_input = Input(&query, "Search..."); + + std::vector menu_items; + for (int idx : filtered_idx) { + menu_items.push_back(cmds[idx].cat + " " + cmds[idx].name); + } + + auto menu = Menu(&menu_items, &selected); + + auto exec_btn = Button("Execute", [&] { + if (selected < static_cast(filtered_idx.size())) { + int cmd_idx = filtered_idx[selected]; + auto status = cmds[cmd_idx].exec(); + status_msg = status.ok() ? "✓ Success" : "✗ " + std::string(status.message()); } - - Elements command_elements; - for (const auto& cmd : filtered_commands) { - command_elements.push_back(text(cmd)); + }); + + auto back_btn = Button("Back", [&] { + app_context.current_layout = LayoutID::kMainMenu; + ScreenInteractive::Active()->ExitLoopClosure()(); + }); + + auto container = Container::Vertical({search_input, menu, exec_btn, back_btn}); + + return CatchEvent(Renderer(container, [&] { + Elements items; + for (size_t i = 0; i < filtered_idx.size(); ++i) { + int idx = filtered_idx[i]; + auto item = text(cmds[idx].cat + " " + cmds[idx].name); + if (static_cast(i) == selected) { + item = item | bold | inverted | color(Color::Cyan); + } + items.push_back(item); } - + + // Show selected command details + Element details = text(""); + if (selected < static_cast(filtered_idx.size())) { + int idx = filtered_idx[selected]; + details = vbox({ + text("Description: " + cmds[idx].desc) | color(Color::GreenLight), + text("Usage: " + cmds[idx].usage) | color(Color::Yellow) | dim, + }); + } + return vbox({ - text("Command Palette") | center | bold, - separator(), - input_component->Render(), - separator(), - vbox(command_elements) | frame | flex, - }) | border; + text("⚡ Command Palette") | bold | center | color(Color::Cyan), + text(app_context.rom.is_loaded() ? "ROM: " + app_context.rom.title() : "No ROM") | center | dim, + separator(), + hbox({text("🔍 "), search_input->Render() | flex}), + separator(), + vbox(items) | frame | flex | vscroll_indicator, + separator(), + details, + separator(), + hbox({exec_btn->Render(), text(" "), back_btn->Render()}) | center, + separator(), + text(status_msg) | center | (status_msg.find("✓") == 0 ? color(Color::Green) : color(Color::Red)), + text("Enter=Execute | ↑↓=Navigate | Esc=Back") | center | dim, + }) | border | size(WIDTH, EQUAL, 80) | size(HEIGHT, EQUAL, 30); + }), [&](Event e) { + if (e == Event::Return && selected < static_cast(filtered_idx.size())) { + int idx = filtered_idx[selected]; + auto status = cmds[idx].exec(); + status_msg = status.ok() ? "✓ Executed" : "✗ " + std::string(status.message()); + return true; + } + return false; }); - - auto event_handler = CatchEvent(renderer, [&](Event event) { - if (event == Event::Return) { - // TODO: Execute the command - //SwitchComponents(screen, LayoutID::kMainMenu); - } - if (event == Event::Escape) { - //SwitchComponents(screen, LayoutID::kMainMenu); - } - return false; - }); - - return event_handler; } } // namespace cli -} // namespace yaze +} // namespace yaze \ No newline at end of file