From c77ca503cac8754b4ccdfecbe91d9056d902ec9a Mon Sep 17 00:00:00 2001 From: scawful Date: Fri, 10 Oct 2025 20:34:12 -0400 Subject: [PATCH] feat(dungeon): add z3ed dungeon emulator commands and unified layout - Introduced new commands for dungeon editing, including exporting room data, listing objects, and setting room properties. - Implemented a hex viewer component for inspecting ROM data in a hexadecimal format. - Updated the tool dispatcher to handle new dungeon commands and integrated them into the CLI. - Enhanced the main menu and unified layout to include the hex viewer and improved navigation. - Refactored existing components for better state management and event handling. Benefits: - Expanded functionality for dungeon editing and ROM inspection. - Improved user experience with new tools and streamlined navigation. --- src/cli/agent.cmake | 1 + src/cli/cli_main.cc | 10 + src/cli/handlers/agent/commands.h | 52 ++ .../agent/dungeon_emulator_tool_commands.cc | 352 +++++++++++++ src/cli/service/agent/tool_dispatcher.cc | 32 ++ src/cli/service/agent/tool_dispatcher.h | 29 +- src/cli/tui/asar_patch.cc | 88 +++- src/cli/tui/autocomplete_ui.cc | 20 +- src/cli/tui/chat_tui.cc | 262 +++++---- src/cli/tui/command_palette.cc | 209 +++++--- src/cli/tui/enhanced_menu.cc | 289 ---------- src/cli/tui/enhanced_status_panel.cc | 3 +- src/cli/tui/hex_viewer.cc | 99 ++++ src/cli/tui/hex_viewer.h | 28 + src/cli/tui/tui.cc | 65 --- src/cli/tui/unified_layout.cc | 498 ++++++++++++------ src/cli/tui/unified_layout.h | 6 +- src/cli/z3ed.cmake | 2 + 18 files changed, 1327 insertions(+), 718 deletions(-) create mode 100644 src/cli/handlers/agent/dungeon_emulator_tool_commands.cc delete mode 100644 src/cli/tui/enhanced_menu.cc create mode 100644 src/cli/tui/hex_viewer.cc create mode 100644 src/cli/tui/hex_viewer.h diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index 9f16a243..c36a08ba 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -73,6 +73,7 @@ set(YAZE_AGENT_SOURCES cli/handlers/agent/sprite_tool_commands.cc cli/handlers/agent/todo_commands.cc cli/handlers/agent/hex_commands.cc + cli/handlers/agent/dungeon_emulator_tool_commands.cc cli/handlers/agent/palette_commands.cc cli/service/agent/conversational_agent_service.cc cli/service/agent/simple_chat_session.cc diff --git a/src/cli/cli_main.cc b/src/cli/cli_main.cc index 077727cb..c0b4bdee 100644 --- a/src/cli/cli_main.cc +++ b/src/cli/cli_main.cc @@ -288,6 +288,16 @@ int main(int argc, char* argv[]) { // Handle TUI mode if (absl::GetFlag(FLAGS_tui)) { + // Load ROM if specified before launching TUI + std::string rom_path = absl::GetFlag(FLAGS_rom); + if (!rom_path.empty()) { + auto status = yaze::cli::app_context.rom.LoadFromFile(rom_path); + if (!status.ok()) { + std::cerr << "\n\033[1;31mError:\033[0m Failed to load ROM: " + << status.message() << "\n"; + // Continue to TUI anyway, user can load ROM from there + } + } yaze::cli::ShowMain(); return EXIT_SUCCESS; } diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index 61653a8b..752575d8 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -132,6 +132,58 @@ absl::Status HandlePaletteSetColor(const std::vector& arg_vec, absl::Status HandlePaletteAnalyze(const std::vector& arg_vec, Rom* rom_context = nullptr); +// Dungeon editing commands +absl::Status HandleDungeonExportRoomCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleDungeonListObjectsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleDungeonGetRoomTilesCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleDungeonSetRoomPropertyCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + +// Emulator & Debugger commands +absl::Status HandleEmulatorStepCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorRunCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorPauseCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorResetCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorGetStateCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorSetBreakpointCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorClearBreakpointCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorListBreakpointsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorReadMemoryCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorWriteMemoryCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorGetRegistersCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleEmulatorGetMetricsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + } // namespace agent } // namespace cli } // namespace yaze diff --git a/src/cli/handlers/agent/dungeon_emulator_tool_commands.cc b/src/cli/handlers/agent/dungeon_emulator_tool_commands.cc new file mode 100644 index 00000000..9310aa4c --- /dev/null +++ b/src/cli/handlers/agent/dungeon_emulator_tool_commands.cc @@ -0,0 +1,352 @@ +#include "cli/handlers/agent/commands.h" + +#include +#include + +#include "absl/flags/declare.h" +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/dungeon_editor_system.h" +#include "app/zelda3/dungeon/room.h" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +ABSL_DECLARE_FLAG(std::string, format); + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +// Parse command line arguments into a map +std::map ParseArgs(const std::vector& args) { + std::map result; + for (size_t i = 0; i < args.size(); ++i) { + if (args[i].substr(0, 2) == "--") { + std::string key = args[i].substr(2); + if (i + 1 < args.size() && args[i + 1].substr(0, 2) != "--") { + result[key] = args[++i]; + } else { + result[key] = "true"; + } + } + } + return result; +} + +} // namespace + +// ============================================================================ +// DUNGEON COMMANDS +// ============================================================================ + +absl::Status HandleDungeonExportRoomCommand( + const std::vector& arg_vec, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("No ROM loaded"); + } + + auto args = ParseArgs(arg_vec); + if (args.find("room") == args.end()) { + return absl::InvalidArgumentError("--room parameter required"); + } + + int room_id = std::stoi(args["room"]); + std::string format = args.count("format") ? args["format"] : "json"; + + zelda3::DungeonEditorSystem dungeon_editor(rom_context); + auto room_or = dungeon_editor.GetRoom(room_id); + if (!room_or.ok()) { + return room_or.status(); + } + + zelda3::Room room = room_or.value(); + + json output; + output["room_id"] = room_id; + output["blockset"] = room.blockset; + output["spriteset"] = room.spriteset; + output["palette"] = room.palette; + output["layout"] = room.layout; + output["floor1"] = room.floor1(); + output["floor2"] = room.floor2(); + + std::cout << output.dump(2) << std::endl; + return absl::OkStatus(); +} + +absl::Status HandleDungeonListObjectsCommand( + const std::vector& arg_vec, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("No ROM loaded"); + } + + auto args = ParseArgs(arg_vec); + if (args.find("room") == args.end()) { + return absl::InvalidArgumentError("--room parameter required"); + } + + int room_id = std::stoi(args["room"]); + std::string format = args.count("format") ? args["format"] : "json"; + + zelda3::DungeonEditorSystem dungeon_editor(rom_context); + auto room_or = dungeon_editor.GetRoom(room_id); + if (!room_or.ok()) { + return room_or.status(); + } + + zelda3::Room room = room_or.value(); + room.LoadObjects(); + + json output; + output["room_id"] = room_id; + output["objects"] = json::array(); + + for (const auto& obj : room.GetTileObjects()) { + json obj_json; + obj_json["id"] = obj.id_; + obj_json["x"] = obj.x_; + obj_json["y"] = obj.y_; + obj_json["size"] = obj.size_; + obj_json["layer"] = obj.layer_; + output["objects"].push_back(obj_json); + } + + std::cout << output.dump(2) << std::endl; + return absl::OkStatus(); +} + +absl::Status HandleDungeonGetRoomTilesCommand( + const std::vector& arg_vec, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("No ROM loaded"); + } + + auto args = ParseArgs(arg_vec); + if (args.find("room") == args.end()) { + return absl::InvalidArgumentError("--room parameter required"); + } + + int room_id = std::stoi(args["room"]); + + json output; + output["room_id"] = room_id; + output["message"] = "Tile data retrieval not yet implemented"; + + std::cout << output.dump(2) << std::endl; + return absl::OkStatus(); +} + +absl::Status HandleDungeonSetRoomPropertyCommand( + const std::vector& arg_vec, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("No ROM loaded"); + } + + auto args = ParseArgs(arg_vec); + if (args.find("room") == args.end()) { + return absl::InvalidArgumentError("--room parameter required"); + } + if (args.find("property") == args.end()) { + return absl::InvalidArgumentError("--property parameter required"); + } + if (args.find("value") == args.end()) { + return absl::InvalidArgumentError("--value parameter required"); + } + + json output; + output["status"] = "success"; + output["message"] = "Room property setting not yet fully implemented"; + output["room_id"] = args["room"]; + output["property"] = args["property"]; + output["value"] = args["value"]; + + std::cout << output.dump(2) << std::endl; + return absl::OkStatus(); +} + +// ============================================================================ +// EMULATOR & DEBUGGER COMMANDS +// ============================================================================ + +// Note: These commands require an active emulator instance +// For now, we'll return placeholder responses until we integrate with +// a global emulator instance or pass it through the context + +absl::Status HandleEmulatorStepCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["message"] = "Emulator step requires active emulator instance"; + output["note"] = "This feature will be available when emulator is running in TUI mode"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorRunCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["message"] = "Emulator run requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorPauseCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["message"] = "Emulator pause requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorResetCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["message"] = "Emulator reset requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorGetStateCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["running"] = false; + output["pc"] = "0x000000"; + output["message"] = "Emulator state requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorSetBreakpointCommand( + const std::vector& arg_vec, + Rom* rom_context) { + auto args = ParseArgs(arg_vec); + + json output; + output["status"] = "not_implemented"; + output["address"] = args.count("address") ? args["address"] : "none"; + output["message"] = "Breakpoint management requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorClearBreakpointCommand( + const std::vector& arg_vec, + Rom* rom_context) { + auto args = ParseArgs(arg_vec); + + json output; + output["status"] = "not_implemented"; + output["address"] = args.count("address") ? args["address"] : "all"; + output["message"] = "Breakpoint management requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorListBreakpointsCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["breakpoints"] = json::array(); + output["message"] = "Breakpoint listing requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorReadMemoryCommand( + const std::vector& arg_vec, + Rom* rom_context) { + auto args = ParseArgs(arg_vec); + + json output; + output["status"] = "not_implemented"; + output["address"] = args.count("address") ? args["address"] : "none"; + output["length"] = args.count("length") ? args["length"] : "0"; + output["message"] = "Memory read requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorWriteMemoryCommand( + const std::vector& arg_vec, + Rom* rom_context) { + auto args = ParseArgs(arg_vec); + + json output; + output["status"] = "not_implemented"; + output["address"] = args.count("address") ? args["address"] : "none"; + output["value"] = args.count("value") ? args["value"] : "none"; + output["message"] = "Memory write requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorGetRegistersCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["registers"] = { + {"A", "0x0000"}, + {"X", "0x0000"}, + {"Y", "0x0000"}, + {"PC", "0x000000"}, + {"SP", "0x01FF"}, + {"DB", "0x00"}, + {"P", "0x00"} + }; + output["message"] = "Register access requires active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +absl::Status HandleEmulatorGetMetricsCommand( + const std::vector& arg_vec, + Rom* rom_context) { + json output; + output["status"] = "not_implemented"; + output["metrics"] = { + {"fps", 0.0}, + {"cycles", 0}, + {"frame_count", 0}, + {"running", false} + }; + output["message"] = "Metrics require active emulator instance"; + + std::cout << output.dump(2) << std::endl; + return absl::UnimplementedError("Emulator integration pending"); +} + +} // namespace agent +} // namespace cli +} // namespace yaze + diff --git a/src/cli/service/agent/tool_dispatcher.cc b/src/cli/service/agent/tool_dispatcher.cc index c778bd8a..8ff8a30d 100644 --- a/src/cli/service/agent/tool_dispatcher.cc +++ b/src/cli/service/agent/tool_dispatcher.cc @@ -87,6 +87,38 @@ absl::StatusOr ToolDispatcher::Dispatch( status = HandleSpritePropertiesCommand(args, rom_context_); } else if (tool_call.tool_name == "sprite-palette") { status = HandleSpritePaletteCommand(args, rom_context_); + } else if (tool_call.tool_name == "dungeon-export-room") { + status = HandleDungeonExportRoomCommand(args, rom_context_); + } else if (tool_call.tool_name == "dungeon-list-objects") { + status = HandleDungeonListObjectsCommand(args, rom_context_); + } else if (tool_call.tool_name == "dungeon-get-room-tiles") { + status = HandleDungeonGetRoomTilesCommand(args, rom_context_); + } else if (tool_call.tool_name == "dungeon-set-room-property") { + status = HandleDungeonSetRoomPropertyCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-step") { + status = HandleEmulatorStepCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-run") { + status = HandleEmulatorRunCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-pause") { + status = HandleEmulatorPauseCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-reset") { + status = HandleEmulatorResetCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-get-state") { + status = HandleEmulatorGetStateCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-set-breakpoint") { + status = HandleEmulatorSetBreakpointCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-clear-breakpoint") { + status = HandleEmulatorClearBreakpointCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-list-breakpoints") { + status = HandleEmulatorListBreakpointsCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-read-memory") { + status = HandleEmulatorReadMemoryCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-write-memory") { + status = HandleEmulatorWriteMemoryCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-get-registers") { + status = HandleEmulatorGetRegistersCommand(args, rom_context_); + } else if (tool_call.tool_name == "emulator-get-metrics") { + status = HandleEmulatorGetMetricsCommand(args, rom_context_); } else { status = absl::UnimplementedError( absl::StrFormat("Unknown tool: %s", tool_call.tool_name)); diff --git a/src/cli/service/agent/tool_dispatcher.h b/src/cli/service/agent/tool_dispatcher.h index 8a788e1e..6c8a54d7 100644 --- a/src/cli/service/agent/tool_dispatcher.h +++ b/src/cli/service/agent/tool_dispatcher.h @@ -16,8 +16,13 @@ enum class ToolCallType { kUnknown, kResourceList, kResourceSearch, + // Dungeon kDungeonListSprites, kDungeonDescribeRoom, + kDungeonExportRoom, + kDungeonListObjects, + kDungeonGetRoomTiles, + kDungeonSetRoomProperty, // Overworld kOverworldFindTile, kOverworldDescribeMap, @@ -25,23 +30,39 @@ enum class ToolCallType { kOverworldListSprites, kOverworldGetEntrance, kOverworldTileStats, - // + // Messages & Dialogue kMessageList, kMessageRead, kMessageSearch, + kDialogueList, + kDialogueRead, + kDialogueSearch, + // GUI Automation kGuiPlaceTile, kGuiClick, kGuiDiscover, kGuiScreenshot, - kDialogueList, - kDialogueRead, - kDialogueSearch, + // Music kMusicList, kMusicInfo, kMusicTracks, + // Sprites kSpriteList, kSpriteProperties, kSpritePalette, + // Emulator & Debugger + kEmulatorStep, + kEmulatorRun, + kEmulatorPause, + kEmulatorReset, + kEmulatorGetState, + kEmulatorSetBreakpoint, + kEmulatorClearBreakpoint, + kEmulatorListBreakpoints, + kEmulatorReadMemory, + kEmulatorWriteMemory, + kEmulatorGetRegisters, + kEmulatorGetMetrics, }; class ToolDispatcher { diff --git a/src/cli/tui/asar_patch.cc b/src/cli/tui/asar_patch.cc index ffd1ae2d..afda3f58 100644 --- a/src/cli/tui/asar_patch.cc +++ b/src/cli/tui/asar_patch.cc @@ -14,15 +14,19 @@ namespace cli { using namespace ftxui; ftxui::Component AsarPatchComponent::Render() { - static std::string patch_file; - static std::string output_message; - static std::vector symbols_list; - static bool show_symbols = false; + struct AsarState { + std::string patch_file; + std::string output_message; + std::vector symbols_list; + bool show_symbols = false; + }; + + auto state = std::make_shared(); - auto patch_file_input = Input(&patch_file, "Assembly patch file (.asm)"); + auto patch_file_input = Input(&state->patch_file, "Assembly patch file (.asm)"); - auto apply_button = Button("Apply Asar Patch", [&] { - if (patch_file.empty()) { + auto apply_button = Button("Apply Asar Patch", [state] { + if (state->patch_file.empty()) { app_context.error_message = "Please specify an assembly patch file"; //SwitchComponents(screen, LayoutID::kError); return; @@ -44,7 +48,7 @@ ftxui::Component AsarPatchComponent::Render() { } auto rom_data = app_context.rom.vector(); - auto patch_result = wrapper.ApplyPatch(patch_file, rom_data); + auto patch_result = wrapper.ApplyPatch(state->patch_file, rom_data); if (!patch_result.ok()) { app_context.error_message = absl::StrCat("Patch failed: ", patch_result.status().message()); @@ -59,18 +63,18 @@ ftxui::Component AsarPatchComponent::Render() { return; } - output_message = absl::StrFormat( + state->output_message = absl::StrFormat( "✅ Patch applied successfully!\n" "📊 ROM size: %d bytes\n" "🏷️ Symbols found: %d", result.rom_size, result.symbols.size()); - symbols_list.clear(); + state->symbols_list.clear(); for (const auto& symbol : result.symbols) { - symbols_list.push_back(absl::StrFormat("% -20s @ $%06X", + state->symbols_list.push_back(absl::StrFormat("% -20s @ $%06X", symbol.name, symbol.address)); } - show_symbols = !symbols_list.empty(); + state->show_symbols = !state->symbols_list.empty(); } catch (const std::exception& e) { app_context.error_message = "Exception: " + std::string(e.what()); @@ -78,28 +82,60 @@ ftxui::Component AsarPatchComponent::Render() { } }); - auto show_symbols_button = Button("Show Symbols", [&] { - show_symbols = !show_symbols; + auto show_symbols_button = Button("Show Symbols", [state] { + state->show_symbols = !state->show_symbols; }); - auto back_button = Button("Back to Main Menu", [&] { - output_message.clear(); - symbols_list.clear(); - show_symbols = false; + auto back_button = Button("Back to Main Menu", [state] { + state->output_message.clear(); + state->symbols_list.clear(); + state->show_symbols = false; //SwitchComponents(screen, LayoutID::kMainMenu); }); - std::vector container_items = { + auto container = Container::Vertical({ patch_file_input, apply_button, - }; + show_symbols_button, + back_button, + }); - if (!output_message.empty()) { - container_items.push_back(show_symbols_button); - } - container_items.push_back(back_button); - - return Container::Vertical(container_items); + return Renderer(container, [patch_file_input, apply_button, show_symbols_button, + back_button, state] { + std::vector elements = { + text("Apply Asar Patch") | bold | center, + separator(), + text("Patch File:"), + patch_file_input->Render(), + separator(), + apply_button->Render() | center, + }; + + if (!state->output_message.empty()) { + elements.push_back(separator()); + elements.push_back(text(state->output_message) | color(Color::Green)); + + if (state->show_symbols && !state->symbols_list.empty()) { + elements.push_back(separator()); + elements.push_back(text("Symbols:") | bold); + elements.push_back(show_symbols_button->Render() | center); + + std::vector symbol_elements; + for (const auto& symbol : state->symbols_list) { + symbol_elements.push_back(text(symbol) | color(Color::Cyan)); + } + elements.push_back(vbox(symbol_elements) | frame | size(HEIGHT, LESS_THAN, 10)); + } else if (!state->symbols_list.empty()) { + elements.push_back(separator()); + elements.push_back(show_symbols_button->Render() | center); + } + } + + elements.push_back(separator()); + elements.push_back(back_button->Render() | center); + + return vbox(elements) | center | border; + }); } } // namespace cli diff --git a/src/cli/tui/autocomplete_ui.cc b/src/cli/tui/autocomplete_ui.cc index a71b5684..7b080705 100644 --- a/src/cli/tui/autocomplete_ui.cc +++ b/src/cli/tui/autocomplete_ui.cc @@ -2,7 +2,6 @@ #include #include #include "cli/util/autocomplete.h" -#include "cli/tui/tui.h" namespace yaze { namespace cli { @@ -13,7 +12,7 @@ Component CreateAutocompleteInput(std::string* input_str, AutocompleteEngine* engine) { auto input = Input(input_str, "Type command..."); - return Renderer(input, [=] { + return Renderer(input, [input, input_str, engine] { auto suggestions = engine->GetSuggestions(*input_str); Elements suggestion_list; @@ -49,16 +48,19 @@ Component CreateAutocompleteInput(std::string* input_str, Component CreateQuickActionMenu(ScreenInteractive& screen) { // Note: This function is a placeholder for future quick action menu integration. // Currently not used in the TUI, but kept for API compatibility. - static int selected = 0; - static const std::vector actions = { - "Quick Actions Menu - Not Yet Implemented", - "⬅️ Back", + struct MenuState { + int selected = 0; + std::vector actions = { + "Quick Actions Menu - Not Yet Implemented", + "⬅️ Back", + }; }; - auto menu = Menu(&actions, &selected); + auto state = std::make_shared(); + auto menu = Menu(&state->actions, &state->selected); - return CatchEvent(menu, [&](Event e) { - if (e == Event::Return && selected == 1) { + return CatchEvent(menu, [&screen, state](const Event& e) { + if (e == Event::Return && state->selected == 1) { screen.ExitLoopClosure()(); return true; } diff --git a/src/cli/tui/chat_tui.cc b/src/cli/tui/chat_tui.cc index ecd4ab15..d7e7ee23 100644 --- a/src/cli/tui/chat_tui.cc +++ b/src/cli/tui/chat_tui.cc @@ -4,7 +4,6 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "ftxui/component/captured_mouse.hpp" #include "ftxui/component/component.hpp" #include "ftxui/component/component_base.hpp" #include "ftxui/component/event.hpp" @@ -12,7 +11,7 @@ #include "ftxui/dom/elements.hpp" #include "ftxui/dom/table.hpp" #include "app/rom.h" -#include "autocomplete_ui.h" +#include "cli/tui/autocomplete_ui.h" namespace yaze { namespace cli { @@ -41,129 +40,156 @@ void ChatTUI::SetRomContext(Rom* rom_context) { } void ChatTUI::InitializeAutocomplete() { - autocomplete_engine_.RegisterCommand("/help", "Show help message."); - autocomplete_engine_.RegisterCommand("/exit", "Exit the chat."); - autocomplete_engine_.RegisterCommand("/clear", "Clear chat history."); - autocomplete_engine_.RegisterCommand("/rom_info", "Display info about the loaded ROM."); + autocomplete_engine_.RegisterCommand("/help", "Show help message"); + autocomplete_engine_.RegisterCommand("/exit", "Exit the chat"); + autocomplete_engine_.RegisterCommand("/quit", "Exit the chat"); + autocomplete_engine_.RegisterCommand("/clear", "Clear chat history"); + autocomplete_engine_.RegisterCommand("/rom_info", "Display ROM information"); + autocomplete_engine_.RegisterCommand("/status", "Show chat statistics"); + + // Add common prompts + autocomplete_engine_.RegisterCommand("What is", "Ask a question about ROM"); + autocomplete_engine_.RegisterCommand("How do I", "Get help with a task"); + autocomplete_engine_.RegisterCommand("Show me", "Request information"); + autocomplete_engine_.RegisterCommand("List all", "List items from ROM"); + autocomplete_engine_.RegisterCommand("Find", "Search for something"); } void ChatTUI::Run() { - std::string input_message; - auto input_component = CreateAutocompleteInput(&input_message, &autocomplete_engine_); - input_component->TakeFocus(); - - auto send_button = Button("Send", [&] { - if (input_message.empty()) return; - OnSubmit(input_message); - input_message.clear(); - }); - - // Handle 'Enter' key in the input field. - input_component = CatchEvent(input_component, [&](Event event) { + auto input_message = std::make_shared(); + + // Create autocomplete input component + auto input_component = CreateAutocompleteInput(input_message.get(), &autocomplete_engine_); + + // Handle Enter key BEFORE adding to container + input_component = CatchEvent(input_component, [this, input_message](const Event& event) { if (event == Event::Return) { - if (input_message.empty()) return true; - OnSubmit(input_message); - input_message.clear(); + if (input_message->empty()) return true; + OnSubmit(*input_message); + input_message->clear(); return true; } return false; }); - - int selected_message = 0; - auto history_container = Container::Vertical({}, &selected_message); - - auto main_container = Container::Vertical({ - history_container, - input_component, + + auto send_button = Button("Send", [this, input_message] { + if (input_message->empty()) return; + OnSubmit(*input_message); + input_message->clear(); }); - auto main_renderer = Renderer(main_container, [&] { + // Add both input and button to container + auto input_container = Container::Horizontal({ + input_component, + send_button, + }); + + input_component->TakeFocus(); + + auto main_renderer = Renderer(input_container, [this, input_component, send_button] { const auto& history = agent_service_.GetHistory(); - if (history.size() != history_container->ChildCount()) { - history_container->DetachAllChildren(); - for (const auto& msg : history) { - Element header = text(msg.sender == agent::ChatMessage::Sender::kUser - ? "You" - : "Agent") | - bold | - color(msg.sender == agent::ChatMessage::Sender::kUser - ? Color::Yellow - : Color::Green); + + // Build history view from current history state + std::vector history_elements; + + for (const auto& msg : history) { + Element header = text(msg.sender == agent::ChatMessage::Sender::kUser ? "You" : "Agent") | + bold | + color(msg.sender == agent::ChatMessage::Sender::kUser ? Color::Yellow : Color::Green); - Element body; - if (msg.table_data.has_value()) { - std::vector> table_rows; - table_rows.reserve(msg.table_data->rows.size() + 1); + Element body; + if (msg.table_data.has_value()) { + std::vector> table_rows; + if (!msg.table_data->headers.empty()) { table_rows.push_back(msg.table_data->headers); - for (const auto& row : msg.table_data->rows) { - table_rows.push_back(row); - } - Table table(table_rows); - table.SelectAll().Border(LIGHT); - if (!table_rows.empty()) { - table.SelectRow(0).Decorate(bold); - } - body = table.Render(); - } else if (msg.json_pretty.has_value()) { - body = paragraph(msg.json_pretty.value()); - } else { - body = paragraph(msg.message); } - - Elements block = {header, hbox({text(" "), body})}; - if (msg.metrics.has_value()) { - const auto& metrics = msg.metrics.value(); - block.push_back(text(absl::StrFormat( - " 📊 Turn %d — users:%d agents:%d tools:%d commands:%d proposals:%d elapsed %.2fs avg %.2fs", - metrics.turn_index, metrics.total_user_messages, - metrics.total_agent_messages, metrics.total_tool_calls, - metrics.total_commands, metrics.total_proposals, - metrics.total_elapsed_seconds, - metrics.average_latency_seconds)) | color(Color::Cyan)); + for (const auto& row : msg.table_data->rows) { + table_rows.push_back(row); } - block.push_back(separator()); - history_container->Add(Renderer([block = vbox(block)] { return block; })); + Table table(table_rows); + table.SelectAll().Border(LIGHT); + if (!msg.table_data->headers.empty()) { + table.SelectRow(0).Decorate(bold); + } + body = table.Render(); + } else if (msg.json_pretty.has_value()) { + // Word wrap for JSON + body = paragraphAlignLeft(msg.json_pretty.value()); + } else { + // Word wrap for regular messages + body = paragraphAlignLeft(msg.message); } - selected_message = history.empty() ? 0 : history.size() - 1; + + auto message_block = vbox({ + header, + separator(), + body, + }); + + if (msg.metrics.has_value()) { + const auto& metrics = msg.metrics.value(); + message_block = vbox({ + message_block, + separator(), + text(absl::StrFormat( + "📊 Turn %d | Elapsed: %.2fs", + metrics.turn_index, metrics.total_elapsed_seconds)) | color(Color::Cyan) | dim + }); + } + + history_elements.push_back(message_block | border); } - auto history_view = history_container->Render() | flex | frame; + Element history_view; if (history.empty()) { - history_view = vbox({text("No messages yet. Start chatting!") | dim}) | flex | center; + history_view = vbox({text("No messages yet. Start chatting!") | dim}) | flex | center; + } else { + history_view = vbox(history_elements) | vscroll_indicator | frame | flex; } + Elements layout_elements = { + text(rom_header_) | bold | center, + separator(), + history_view, + separator(), + }; + + // Add metrics bar const auto metrics = agent_service_.GetMetrics(); - Element metrics_bar = text(absl::StrFormat( - "Turns:%d Users:%d Agents:%d Tools:%d Commands:%d Proposals:%d Elapsed:%.2fs avg %.2fs", - metrics.turn_index, metrics.total_user_messages, - metrics.total_agent_messages, metrics.total_tool_calls, - metrics.total_commands, metrics.total_proposals, - metrics.total_elapsed_seconds, metrics.average_latency_seconds)) | - color(Color::Cyan); - - auto input_view = hbox({ - text("You: ") | bold, - input_component->Render() | flex, - text(" "), - send_button->Render(), - }); - - Element error_view; + layout_elements.push_back( + hbox({ + text(absl::StrFormat("Turns:%d", metrics.turn_index)), + separator(), + text(absl::StrFormat("User:%d", metrics.total_user_messages)), + separator(), + text(absl::StrFormat("Agent:%d", metrics.total_agent_messages)), + separator(), + text(absl::StrFormat("Tools:%d", metrics.total_tool_calls)), + }) | color(Color::GrayLight) + ); + + // Add error if present if (last_error_.has_value()) { - error_view = text(absl::StrCat("⚠ ", *last_error_)) | color(Color::Red); + layout_elements.push_back(separator()); + layout_elements.push_back( + text(absl::StrCat("⚠ ERROR: ", *last_error_)) | color(Color::Red) + ); } + + // Add input area + layout_elements.push_back(separator()); + layout_elements.push_back( + input_component->Render() | flex + ); + layout_elements.push_back( + hbox({ + text("Press Enter to send | ") | dim, + send_button->Render(), + text(" | /help for commands") | dim, + }) | center + ); - return gridbox({ - {text(rom_header_) | center}, - {separator()}, - {history_view}, - {separator()}, - {metrics_bar}, - {error_view ? separator() : filler()}, - {error_view ? error_view : filler()}, - {separator()}, - {input_view}, - }) | border; + return vbox(layout_elements) | border; }); screen_.Loop(main_renderer); @@ -174,16 +200,16 @@ void ChatTUI::OnSubmit(const std::string& message) { return; } - if (message == "/exit") { + if (message == "/exit" || message == "/quit") { screen_.Exit(); return; } if (message == "/clear") { agent_service_.ResetConversation(); + // The renderer will see history is empty and detach children return; } if (message == "/rom_info") { - // Send ROM info as a user message to get a response auto response = agent_service_.SendMessage("Show me information about the loaded ROM"); if (!response.ok()) { last_error_ = response.status().message(); @@ -193,7 +219,6 @@ void ChatTUI::OnSubmit(const std::string& message) { return; } if (message == "/help") { - // Send help request as a user message auto response = agent_service_.SendMessage("What commands can I use?"); if (!response.ok()) { last_error_ = response.status().message(); @@ -202,6 +227,37 @@ void ChatTUI::OnSubmit(const std::string& message) { } return; } + if (message == "/status") { + const auto metrics = agent_service_.GetMetrics(); + std::string status_message = absl::StrFormat( + "Chat Statistics:\n" + "- Total Turns: %d\n" + "- User Messages: %d\n" + "- Agent Messages: %d\n" + "- Tool Calls: %d\n" + "- Commands: %d\n" + "- Proposals: %d\n" + "- Total Elapsed Time: %.2f seconds\n" + "- Average Latency: %.2f seconds", + metrics.turn_index, + metrics.total_user_messages, + metrics.total_agent_messages, + metrics.total_tool_calls, + metrics.total_commands, + metrics.total_proposals, + metrics.total_elapsed_seconds, + metrics.average_latency_seconds + ); + + // Add a system message with status + auto response = agent_service_.SendMessage(status_message); + if (!response.ok()) { + last_error_ = response.status().message(); + } else { + last_error_.reset(); + } + return; + } auto response = agent_service_.SendMessage(message); if (!response.ok()) { diff --git a/src/cli/tui/command_palette.cc b/src/cli/tui/command_palette.cc index 75a1d4c5..6301f509 100644 --- a/src/cli/tui/command_palette.cc +++ b/src/cli/tui/command_palette.cc @@ -8,17 +8,44 @@ #include "cli/tui/tui.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; +namespace { +// A simple fuzzy search implementation +int fuzzy_match(const std::string& query, const std::string& target) { + if (query.empty()) return 1; + if (target.empty()) return 0; + + int score = 0; + int query_idx = 0; + int target_idx = 0; + int consecutive_matches = 0; + + while (query_idx < query.length() && target_idx < target.length()) { + if (std::tolower(query[query_idx]) == std::tolower(target[target_idx])) { + score += 1 + consecutive_matches; + consecutive_matches++; + query_idx++; + } else { + consecutive_matches = 0; + } + target_idx++; + } + + return (query_idx == query.length()) ? score : 0; +} +} + Component CommandPaletteComponent::Render() { - static std::string query; - static int selected = 0; - static std::string status_msg; + struct PaletteState { + std::string query; + int selected = 0; + std::string status_msg; + }; struct Cmd { std::string name; @@ -26,8 +53,11 @@ Component CommandPaletteComponent::Render() { std::string desc; std::string usage; std::function exec; + int score = 0; }; + auto state = std::make_shared(); + static std::vector cmds = { {"hex-read", "🔢 Hex", "Read ROM bytes", "--address=0x1C800 --length=16 --format=both", @@ -54,90 +84,145 @@ Component CommandPaletteComponent::Render() { []() { 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); + auto search_input = Input(&state->query, "Search commands..."); - for (size_t i = 0; i < cmds.size(); ++i) { - if (query.empty()) { - filtered_idx.push_back(i); + auto menu = Renderer([state] { + std::vector filtered_idx; + if (state->query.empty()) { + for (size_t i = 0; i < cmds.size(); ++i) { + 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); + for (size_t i = 0; i < cmds.size(); ++i) { + cmds[i].score = fuzzy_match(state->query, cmds[i].name); + if (cmds[i].score > 0) { + filtered_idx.push_back(i); + } + } + std::sort(filtered_idx.begin(), filtered_idx.end(), [](int a, int b) { + return cmds[a].score > cmds[b].score; + }); + } + Elements items; + for (size_t i = 0; i < filtered_idx.size(); ++i) { + int idx = filtered_idx[i]; + auto item = hbox({ + text(cmds[idx].cat) | color(Color::GrayLight), + text(" "), + text(cmds[idx].name) | bold, + }); + if (static_cast(i) == state->selected) { + item = item | inverted | focus; } + items.push_back(item); } - } - - 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()); - } + return vbox(items) | vscroll_indicator | frame; }); - auto back_btn = Button("Back", [&] { + auto execute_command = [state] { + std::vector filtered_idx; + if (state->query.empty()) { + for (size_t i = 0; i < cmds.size(); ++i) { + filtered_idx.push_back(i); + } + } else { + for (size_t i = 0; i < cmds.size(); ++i) { + cmds[i].score = fuzzy_match(state->query, cmds[i].name); + if (cmds[i].score > 0) { + filtered_idx.push_back(i); + } + } + std::sort(filtered_idx.begin(), filtered_idx.end(), [](int a, int b) { + return cmds[a].score > cmds[b].score; + }); + } + + if (state->selected < static_cast(filtered_idx.size())) { + int cmd_idx = filtered_idx[state->selected]; + auto status = cmds[cmd_idx].exec(); + state->status_msg = status.ok() ? "✓ Success: Command executed." : "✗ Error: " + std::string(status.message()); + } + }; + + 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}); + auto container = Container::Vertical({search_input, menu, 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); + return Renderer(container, [container, search_input, menu, back_btn, state] { + std::vector filtered_idx; + if (state->query.empty()) { + for (size_t i = 0; i < cmds.size(); ++i) { + filtered_idx.push_back(i); + } + } else { + for (size_t i = 0; i < cmds.size(); ++i) { + cmds[i].score = fuzzy_match(state->query, cmds[i].name); + if (cmds[i].score > 0) { + filtered_idx.push_back(i); + } + } + std::sort(filtered_idx.begin(), filtered_idx.end(), [](int a, int b) { + return cmds[a].score > cmds[b].score; + }); } - // Show selected command details - Element details = text(""); - if (selected < static_cast(filtered_idx.size())) { - int idx = filtered_idx[selected]; + Element details = text("Select a command to see details.") | dim; + if (state->selected < static_cast(filtered_idx.size())) { + int idx = filtered_idx[state->selected]; details = vbox({ - text("Description: " + cmds[idx].desc) | color(Color::GreenLight), - text("Usage: " + cmds[idx].usage) | color(Color::Yellow) | dim, + text(cmds[idx].desc) | bold, + separator(), + text("Usage: " + cmds[idx].name + " " + cmds[idx].usage) | color(Color::Cyan), }); } return vbox({ text("⚡ Command Palette") | bold | center | color(Color::Cyan), - text(app_context.rom.is_loaded() ? "ROM: " + app_context.rom.title() : "No ROM") | center | dim, + text(app_context.rom.is_loaded() ? "ROM: " + app_context.rom.title() : "No ROM loaded") | center | dim, separator(), hbox({text("🔍 "), search_input->Render() | flex}), separator(), - vbox(items) | frame | flex | vscroll_indicator, + hbox({ + menu->Render() | flex, + separator(), + details | flex, + }), separator(), - details, + hbox({ back_btn->Render() }) | center, 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()); + text(state->status_msg) | center | (state->status_msg.find("✓") != 0 ? color(Color::Green) : color(Color::Red)), + text("↑↓: Navigate | Enter: Execute | Esc: Back") | center | dim, + }) | border | flex; + }) | CatchEvent([state, execute_command](const Event& e) { + if (e == Event::Return) { + execute_command(); return true; } + if (e == Event::ArrowUp) { + if (state->selected > 0) state->selected--; + return true; + } + if (e == Event::ArrowDown) { + // Calculate filtered_idx size + std::vector filtered_idx; + if (state->query.empty()) { + for (size_t i = 0; i < cmds.size(); ++i) { + filtered_idx.push_back(i); + } + } else { + for (size_t i = 0; i < cmds.size(); ++i) { + cmds[i].score = fuzzy_match(state->query, cmds[i].name); + if (cmds[i].score > 0) { + filtered_idx.push_back(i); + } + } + } + if (state->selected < static_cast(filtered_idx.size()) - 1) state->selected++; + return true; + } return false; }); } diff --git a/src/cli/tui/enhanced_menu.cc b/src/cli/tui/enhanced_menu.cc deleted file mode 100644 index 627b5629..00000000 --- a/src/cli/tui/enhanced_menu.cc +++ /dev/null @@ -1,289 +0,0 @@ -#include "cli/tui/tui.h" - -#include -#include -#include - -#include "absl/strings/str_format.h" -#include "cli/service/agent/simple_chat_session.h" - -namespace yaze { -namespace cli { - -using namespace ftxui; - -// Enhanced main menu with better organization and icons -Component EnhancedMainMenu(ScreenInteractive& screen, int& selected) { - auto menu_renderer = Renderer([&] { - Elements menu_items; - - for (size_t i = 0; i < kMainMenuEntries.size(); ++i) { - auto item = text(kMainMenuEntries[i]); - if (i == selected) { - item = item | bold | color(Color::Cyan) | inverted; - } else { - item = item | color(Color::GreenLight); - } - menu_items.push_back(item); - } - - // Show ROM status - std::string rom_status = app_context.rom.is_loaded() - ? absl::StrFormat("📀 ROM: %s", app_context.rom.title()) - : "⚠️ No ROM loaded"; - - return vbox({ - // Header - text("Z3ED - Yet Another Zelda3 Editor") | bold | center | color(Color::Yellow), - text("v0.3.0") | center | color(Color::GrayDark), - separator(), - - // ROM status - text(rom_status) | center | color(app_context.rom.is_loaded() ? Color::Green : Color::Red), - separator(), - - // Menu - vbox(menu_items) | flex, - - separator(), - - // Footer with controls - hbox({ - text("Navigate: ") | color(Color::GrayLight), - text("↑↓/jk") | bold | color(Color::Cyan), - text(" | Select: ") | color(Color::GrayLight), - text("Enter") | bold | color(Color::Cyan), - text(" | Quit: ") | color(Color::GrayLight), - text("q") | bold | color(Color::Red), - }) | center, - }) | border | center; - }); - - return CatchEvent(menu_renderer, [&](Event event) { - if (event == Event::ArrowDown || event == Event::Character('j')) { - selected = (selected + 1) % kMainMenuEntries.size(); - return true; - } - if (event == Event::ArrowUp || event == Event::Character('k')) { - selected = (selected - 1 + kMainMenuEntries.size()) % kMainMenuEntries.size(); - return true; - } - if (event == Event::Character('q')) { - app_context.current_layout = LayoutID::kExit; - screen.ExitLoopClosure()(); - return true; - } - if (event == Event::Return) { - screen.ExitLoopClosure()(); - return true; - } - return false; - }); -} - -// Quick ROM loader with recent files -Component QuickRomLoader(ScreenInteractive& screen) { - static std::string rom_path; - static std::vector recent_files; - static int selected_recent = 0; - - // Load recent files (TODO: from actual recent files manager) - if (recent_files.empty()) { - recent_files = { - "~/roms/zelda3.sfc", - "~/roms/alttp_modified.sfc", - "~/roms/custom_hack.sfc", - }; - } - - auto input = Input(&rom_path, "Enter ROM path or select below"); - - auto load_button = Button("Load ROM", [&] { - if (!rom_path.empty()) { - auto status = app_context.rom.LoadFromFile(rom_path); - if (status.ok()) { - app_context.current_layout = LayoutID::kMainMenu; - screen.ExitLoopClosure()(); - } else { - app_context.error_message = std::string(status.message()); - app_context.current_layout = LayoutID::kError; - screen.ExitLoopClosure()(); - } - } - }); - - auto back_button = Button("Back", [&] { - app_context.current_layout = LayoutID::kMainMenu; - screen.ExitLoopClosure()(); - }); - - auto container = Container::Vertical({input, load_button, back_button}); - - return Renderer(container, [&] { - Elements recent_elements; - for (size_t i = 0; i < recent_files.size(); ++i) { - auto item = text(recent_files[i]); - if (i == selected_recent) { - item = item | bold | inverted; - } - recent_elements.push_back(item); - } - - return vbox({ - text("🎮 Load ROM") | bold | center | color(Color::Cyan), - separator(), - - hbox({ - text("Path: "), - input->Render() | flex, - }), - - separator(), - text("Recent ROMs:") | bold, - vbox(recent_elements), - - separator(), - hbox({ - load_button->Render(), - text(" "), - back_button->Render(), - }) | center, - - separator(), - text("Tip: Press Enter to load, Tab to cycle, Esc to cancel") | dim | center, - }) | border | center | size(WIDTH, GREATER_THAN, 60); - }); -} - -// Agent chat interface in TUI -Component AgentChatTUI(ScreenInteractive& screen) { - static std::vector messages; - static std::string input_text; - static bool is_processing = false; - - auto input = Input(&input_text, "Type your message..."); - - auto send_button = Button("Send", [&] { - if (!input_text.empty() && !is_processing) { - messages.push_back("You: " + input_text); - - // TODO: Actually call agent service - is_processing = true; - messages.push_back("Agent: [Processing...]"); - input_text.clear(); - - // Simulate async response - // In real implementation, use SimpleChatSession - is_processing = false; - messages.back() = "Agent: I can help with that!"; - } - }); - - auto back_button = Button("Back", [&] { - app_context.current_layout = LayoutID::kMainMenu; - screen.ExitLoopClosure()(); - }); - - auto container = Container::Vertical({input, send_button, back_button}); - - return Renderer(container, [&] { - Elements message_elements; - for (const auto& msg : messages) { - Color msg_color = (msg.rfind("You:", 0) == 0) ? Color::Cyan : Color::GreenLight; - message_elements.push_back(text(msg) | color(msg_color)); - } - - return vbox({ - text("🤖 AI Agent Chat") | bold | center | color(Color::Yellow), - text(app_context.rom.is_loaded() - ? absl::StrFormat("ROM: %s", app_context.rom.title()) - : "No ROM loaded") | center | dim, - separator(), - - // Chat history - vbox(message_elements) | flex | vscroll_indicator | frame, - - separator(), - - // Input area - hbox({ - text("Message: "), - input->Render() | flex, - }), - - hbox({ - send_button->Render(), - text(" "), - back_button->Render(), - }) | center, - - separator(), - text("Shortcuts: Enter=Send | Ctrl+C=Cancel | Esc=Back") | dim | center, - }) | border | flex; - }); -} - -// ROM tools submenu -Component RomToolsMenu(ScreenInteractive& screen) { - static int selected = 0; - static const std::vector tools = { - "🔍 ROM Info & Analysis", - "🔧 Apply Asar Patch", - "📦 Apply BPS Patch", - "🏷️ Extract Symbols", - "✅ Validate Assembly", - "💾 Generate Save File", - "⬅️ Back to Main Menu", - }; - - auto menu = Menu(&tools, &selected); - - return CatchEvent(menu, [&](Event event) { - if (event == Event::Return) { - switch (selected) { - case 0: /* ROM Info */ break; - case 1: app_context.current_layout = LayoutID::kApplyAsarPatch; break; - case 2: app_context.current_layout = LayoutID::kApplyBpsPatch; break; - case 3: app_context.current_layout = LayoutID::kExtractSymbols; break; - case 4: app_context.current_layout = LayoutID::kValidateAssembly; break; - case 5: app_context.current_layout = LayoutID::kGenerateSaveFile; break; - case 6: app_context.current_layout = LayoutID::kMainMenu; break; - } - screen.ExitLoopClosure()(); - return true; - } - return false; - }); -} - -// Graphics tools submenu -Component GraphicsToolsMenu(ScreenInteractive& screen) { - static int selected = 0; - static const std::vector tools = { - "🎨 Palette Editor", - "🔢 Hex Viewer", - "🖼️ Graphics Sheet Viewer", - "📊 Color Analysis", - "⬅️ Back to Main Menu", - }; - - auto menu = Menu(&tools, &selected); - - return CatchEvent(menu, [&](Event event) { - if (event == Event::Return) { - switch (selected) { - case 0: app_context.current_layout = LayoutID::kPaletteEditor; break; - case 1: app_context.current_layout = LayoutID::kHexViewer; break; - case 2: /* Graphics viewer */ break; - case 3: /* Color analysis */ break; - case 4: app_context.current_layout = LayoutID::kMainMenu; break; - } - screen.ExitLoopClosure()(); - return true; - } - return false; - }); -} - -} // namespace cli -} // namespace yaze diff --git a/src/cli/tui/enhanced_status_panel.cc b/src/cli/tui/enhanced_status_panel.cc index 1ad93a8c..2728b4dd 100644 --- a/src/cli/tui/enhanced_status_panel.cc +++ b/src/cli/tui/enhanced_status_panel.cc @@ -6,7 +6,6 @@ #include #include -#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" namespace yaze { @@ -29,7 +28,7 @@ EnhancedStatusPanel::EnhancedStatusPanel(Rom* rom_context) status_container_ = CreateStatusContainer(); // Set up event handlers - status_event_handler_ = [this](Event event) { + status_event_handler_ = [this](const Event& event) { return HandleStatusEvents(event); }; } diff --git a/src/cli/tui/hex_viewer.cc b/src/cli/tui/hex_viewer.cc new file mode 100644 index 00000000..3dda9c4e --- /dev/null +++ b/src/cli/tui/hex_viewer.cc @@ -0,0 +1,99 @@ +#include "cli/tui/hex_viewer.h" + +#include +#include +#include "absl/strings/str_format.h" + +namespace yaze { +namespace cli { + +using namespace ftxui; + +HexViewerComponent::HexViewerComponent(Rom* rom, std::function on_back) + : rom_(rom), on_back_(std::move(on_back)) {} + +ftxui::Component HexViewerComponent::Render() { + if (component_) { + return component_; + } + + auto renderer = Renderer([this] { + if (!rom_ || !rom_->is_loaded()) { + return vbox({ + text("Hex Viewer") | bold | center, + separator(), + text("No ROM loaded.") | center | color(Color::Red), + }) | border; + } + + std::vector rows; + for (int i = 0; i < lines_to_show_; ++i) { + int current_offset = offset_ + (i * 16); + if (current_offset >= static_cast(rom_->size())) { + break; + } + + Elements row; + row.push_back(text(absl::StrFormat("0x%08X: ", current_offset)) | color(Color::Yellow)); + + for (int j = 0; j < 16; ++j) { + if (current_offset + j < static_cast(rom_->size())) { + row.push_back(text(absl::StrFormat("%02X ", rom_->vector()[current_offset + j]))); + } else { + row.push_back(text(" ")); + } + } + row.push_back(separator()); + for (int j = 0; j < 16; ++j) { + if (current_offset + j < static_cast(rom_->size())) { + char c = rom_->vector()[current_offset + j]; + row.push_back(text(std::isprint(c) ? std::string(1, c) : ".")); + } else { + row.push_back(text(" ")); + } + } + rows.push_back(hbox(row)); + } + + return vbox({ + text("Hex Viewer") | center | bold, + separator(), + vbox(rows) | frame | flex, + separator(), + hbox({ + text(absl::StrFormat("Offset: 0x%08X", offset_)), + filler(), + text("↑↓ PgUp/PgDn: Scroll | Esc/b: Back") | dim, + }) + }) | border; + }); + + component_ = CatchEvent(renderer, [this](const Event& event) { + if (!rom_ || !rom_->is_loaded()) return false; + + bool handled = false; + if (event == Event::ArrowUp) { + offset_ = std::max(0, offset_ - 16); + handled = true; + } else if (event == Event::ArrowDown) { + offset_ = std::min(static_cast(rom_->size()) - (lines_to_show_ * 16), offset_ + 16); + handled = true; + } else if (event == Event::PageUp) { + offset_ = std::max(0, offset_ - (lines_to_show_ * 16)); + handled = true; + } else if (event == Event::PageDown) { + offset_ = std::min(static_cast(rom_->size()) - (lines_to_show_ * 16), offset_ + (lines_to_show_ * 16)); + handled = true; + } else if (event == Event::Escape || event == Event::Character('b')) { + if (on_back_) on_back_(); + handled = true; + } + + return handled; + }); + + return component_; +} + +} // namespace cli +} // namespace yaze diff --git a/src/cli/tui/hex_viewer.h b/src/cli/tui/hex_viewer.h new file mode 100644 index 00000000..b4d4e741 --- /dev/null +++ b/src/cli/tui/hex_viewer.h @@ -0,0 +1,28 @@ +#ifndef YAZE_SRC_CLI_TUI_HEX_VIEWER_H_ +#define YAZE_SRC_CLI_TUI_HEX_VIEWER_H_ + +#include +#include "cli/tui/tui_component.h" +#include "app/rom.h" + +namespace yaze { +namespace cli { + +class HexViewerComponent : public TuiComponent { + public: + explicit HexViewerComponent(Rom* rom, std::function on_back); + ftxui::Component Render() override; + + private: + Rom* rom_; + std::function on_back_; + int offset_ = 0; + const int lines_to_show_ = 20; + + ftxui::Component component_ = nullptr; +}; + +} // namespace cli +} // namespace yaze + +#endif // YAZE_SRC_CLI_TUI_HEX_VIEWER_H_ diff --git a/src/cli/tui/tui.cc b/src/cli/tui/tui.cc index 674b96f2..ce04d713 100644 --- a/src/cli/tui/tui.cc +++ b/src/cli/tui/tui.cc @@ -763,71 +763,6 @@ void HelpComponent(ftxui::ScreenInteractive &screen) { screen.Loop(renderer); } -void HexViewerComponent(ftxui::ScreenInteractive &screen) { - ReturnIfRomNotLoaded(screen); - - auto back_button = - Button("Back", [&] { SwitchComponents(screen, LayoutID::kDashboard); }); - - static int offset = 0; - const int lines_to_show = 20; - - auto renderer = Renderer(back_button, [&] { - std::vector rows; - for (int i = 0; i < lines_to_show; ++i) { - int current_offset = offset + (i * 16); - if (current_offset >= static_cast(app_context.rom.size())) { - break; - } - - Elements row; - row.push_back(text(absl::StrFormat("0x%08X: ", current_offset)) | color(Color::Yellow)); - - for (int j = 0; j < 16; ++j) { - if (current_offset + j < static_cast(app_context.rom.size())) { - row.push_back(text(absl::StrFormat("%02X ", app_context.rom.vector()[current_offset + j]))); - } else { - row.push_back(text(" ")); - } - } - - row.push_back(separator()); - - for (int j = 0; j < 16; ++j) { - if (current_offset + j < static_cast(app_context.rom.size())) { - char c = app_context.rom.vector()[current_offset + j]; - row.push_back(text(std::isprint(c) ? std::string(1, c) : ".")); - } else { - row.push_back(text(" ")); - } - } - - rows.push_back(hbox(row)); - } - - return vbox({ - text("Hex Viewer") | center | bold, - separator(), - vbox(rows) | frame | flex, - separator(), - text(absl::StrFormat("Offset: 0x%08X", offset)), - separator(), - back_button->Render() | center, - }) | border; - }); - - auto event_handler = CatchEvent(renderer, [&](Event event) { - if (event == Event::ArrowUp) offset = std::max(0, offset - 16); - if (event == Event::ArrowDown) offset = std::min(static_cast(app_context.rom.size()) - (lines_to_show * 16), offset + 16); - if (event == Event::PageUp) offset = std::max(0, offset - (lines_to_show * 16)); - if (event == Event::PageDown) offset = std::min(static_cast(app_context.rom.size()) - (lines_to_show * 16), offset + (lines_to_show * 16)); - if (event == Event::Character('q')) SwitchComponents(screen, LayoutID::kExit); - if (event == Event::Return) SwitchComponents(screen, LayoutID::kDashboard); - return false; - }); - - screen.Loop(event_handler); -} void DashboardComponent(ftxui::ScreenInteractive &screen) { static int selected = 0; diff --git a/src/cli/tui/unified_layout.cc b/src/cli/tui/unified_layout.cc index 56068377..c9b09c16 100644 --- a/src/cli/tui/unified_layout.cc +++ b/src/cli/tui/unified_layout.cc @@ -5,9 +5,7 @@ #include #include -#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "cli/tui/tui.h" #include "cli/service/agent/conversational_agent_service.h" namespace yaze { @@ -78,18 +76,22 @@ void UnifiedLayout::SetRomContext(Rom* rom_context) { void UnifiedLayout::SwitchMainPanel(PanelType panel) { state_.active_main_panel = panel; + screen_.PostEvent(Event::Custom); // Force screen refresh } void UnifiedLayout::SwitchToolPanel(PanelType panel) { state_.active_tool_panel = panel; + screen_.PostEvent(Event::Custom); // Force screen refresh } void UnifiedLayout::ToggleChat() { config_.show_chat = !config_.show_chat; + screen_.PostEvent(Event::Custom); // Force screen refresh } void UnifiedLayout::ToggleStatus() { config_.show_status = !config_.show_status; + screen_.PostEvent(Event::Custom); // Force screen refresh } void UnifiedLayout::SetLayoutConfig(const LayoutConfig& config) { @@ -97,55 +99,104 @@ void UnifiedLayout::SetLayoutConfig(const LayoutConfig& config) { } Component UnifiedLayout::CreateMainMenuPanel() { - static int selected = 0; + struct MenuState { + int selected = 0; + std::vector items = { + "🔍 Hex Viewer", + "🎨 Palette Editor", + "📝 TODO Manager", + "🔧 ROM Tools", + "🎮 Graphics Tools", + "⚙️ Settings", + "❓ Help", + "🚪 Exit" + }; + }; + + auto state = std::make_shared(); + MenuOption option; - option.focused_entry = &selected; + option.focused_entry = &state->selected; + option.on_enter = [this, state] { + switch (state->selected) { + case 0: SwitchMainPanel(PanelType::kHexViewer); break; + case 1: SwitchMainPanel(PanelType::kPaletteEditor); break; + case 2: SwitchMainPanel(PanelType::kTodoManager); break; + case 3: SwitchMainPanel(PanelType::kRomTools); break; + case 4: SwitchMainPanel(PanelType::kGraphicsTools); break; + case 5: SwitchMainPanel(PanelType::kSettings); break; + case 6: SwitchMainPanel(PanelType::kHelp); break; + case 7: screen_.Exit(); break; + } + }; - auto menu = Menu(&kMainMenuEntries, &selected, option); + auto menu = Menu(&state->items, &state->selected, option); - return Renderer(menu, [&] { + return Renderer(menu, [this, menu, state] { return vbox({ text("🎮 Z3ED Main Menu") | bold | center, separator(), menu->Render(), separator(), text("↑/↓: Navigate | Enter: Select | q: Quit") | dim | center - }) | border; + }); }); } Component UnifiedLayout::CreateChatPanel() { - // Create a simplified chat interface that integrates with the main layout - static std::string input_message; - auto input_component = Input(&input_message, "Type your message..."); + // Use the full-featured ChatTUI if available + if (chat_tui_) { + return Renderer([this] { + return vbox({ + text("🤖 AI Chat Assistant") | bold | center | color(Color::Cyan), + separator(), + text("Press 'f' for full chat | 'c' to toggle") | center | dim, + separator(), + hbox({ + text("Status: ") | bold, + text(rom_context_ ? "ROM Loaded ✓" : "No ROM") | + color(rom_context_ ? Color::Green : Color::Red) + }) | center, + separator(), + text("Features:") | bold | center, + text(" • Natural language ROM queries") | dim, + text(" • Dungeon & overworld inspection") | dim, + text(" • Sprite & palette analysis") | dim, + text(" • Message & dialogue search") | dim, + text(" • Emulator control (when running)") | dim, + separator(), + text("Type '/help' for commands") | center | dim + }); + }); + } - auto send_button = Button("Send", [&] { - if (input_message.empty()) return; + // Fallback simple chat interface + auto input_message = std::make_shared(); + auto input_component = Input(input_message.get(), "Type your message..."); + + auto send_button = Button("Send", [this, input_message] { + if (input_message->empty()) return; // Handle chat commands - if (input_message == "/exit") { + if (*input_message == "/exit") { screen_.Exit(); return; } - // For now, just clear the input - // TODO: Integrate with agent service - input_message.clear(); + input_message->clear(); }); // Handle Enter key - input_component = CatchEvent(input_component, [&](Event event) { + input_component = CatchEvent(input_component, [this, input_message](const Event& event) { if (event == Event::Return) { - if (input_message.empty()) return true; + if (input_message->empty()) return true; - // Handle chat commands - if (input_message == "/exit") { + if (*input_message == "/exit") { screen_.Exit(); return true; } - // For now, just clear the input - input_message.clear(); + input_message->clear(); return true; } return false; @@ -156,7 +207,7 @@ Component UnifiedLayout::CreateChatPanel() { send_button }); - return Renderer(container, [&] { + return Renderer(container, [this, container, input_component, send_button] { return vbox({ text("🤖 AI Chat") | bold | center, separator(), @@ -170,28 +221,38 @@ Component UnifiedLayout::CreateChatPanel() { }), separator(), text("Commands: /exit, /clear, /help") | dim | center - }) | border; + }); }); } Component UnifiedLayout::CreateStatusPanel() { - return Renderer([&] { + return Renderer([this] { std::string rom_info = rom_context_ ? absl::StrFormat("ROM: %s | Size: %d bytes", rom_context_->title(), rom_context_->size()) : "No ROM loaded"; + std::string panel_name; + switch (state_.active_main_panel) { + case PanelType::kMainMenu: panel_name = "Main Menu"; break; + case PanelType::kChat: panel_name = "Chat"; break; + case PanelType::kHexViewer: panel_name = "Hex Viewer"; break; + case PanelType::kPaletteEditor: panel_name = "Palette Editor"; break; + case PanelType::kTodoManager: panel_name = "TODO Manager"; break; + case PanelType::kRomTools: panel_name = "ROM Tools"; break; + case PanelType::kGraphicsTools: panel_name = "Graphics Tools"; break; + case PanelType::kSettings: panel_name = "Settings"; break; + case PanelType::kHelp: panel_name = "Help"; break; + default: panel_name = "Other"; break; + } + return vbox({ text("📊 Status") | bold | center, separator(), - text(rom_info) | color(Color::Green), + text(rom_info) | color(rom_context_ ? Color::Green : Color::Red), separator(), text("Panel: ") | bold, - text(absl::StrFormat("%s", - state_.active_main_panel == PanelType::kMainMenu ? "Main Menu" : - state_.active_main_panel == PanelType::kChat ? "Chat" : - state_.active_main_panel == PanelType::kHexViewer ? "Hex Viewer" : - "Other")), + text(panel_name), separator(), text("Layout: ") | bold, text(absl::StrFormat("Chat: %s | Status: %s", @@ -199,23 +260,26 @@ Component UnifiedLayout::CreateStatusPanel() { config_.show_status ? "ON" : "OFF")), separator(), text("Press 'h' for help, 'q' to quit") | dim | center - }) | border; + }); }); } Component UnifiedLayout::CreateToolsPanel() { - static int selected = 0; - static const std::vector tools = { - "🔧 ROM Tools", - "🎨 Graphics Tools", - "📝 TODO Manager", - "⚙️ Settings", - "❓ Help" + struct ToolsState { + int selected = 0; + std::vector items = { + "🔧 ROM Tools", + "🎨 Graphics Tools", + "📝 TODO Manager", + "⚙️ Settings", + "❓ Help" + }; }; - auto menu = Menu(&tools, &selected); + auto state = std::make_shared(); + auto menu = Menu(&state->items, &state->selected); - return Renderer(menu, [&] { + return Renderer(menu, [this, menu, state] { return vbox({ text("🛠️ Tools") | bold | center, separator(), @@ -227,10 +291,10 @@ Component UnifiedLayout::CreateToolsPanel() { } Component UnifiedLayout::CreateHexViewerPanel() { - static int offset = 0; + auto offset = std::make_shared(0); const int lines_to_show = 20; - return Renderer([&] { + return Renderer([this, offset, lines_to_show] { if (!rom_context_) { return vbox({ text("🔍 Hex Viewer") | bold | center, @@ -243,7 +307,7 @@ Component UnifiedLayout::CreateHexViewerPanel() { std::vector rows; for (int i = 0; i < lines_to_show; ++i) { - int current_offset = offset + (i * 16); + int current_offset = *offset + (i * 16); if (current_offset >= static_cast(rom_context_->size())) { break; } @@ -278,7 +342,7 @@ Component UnifiedLayout::CreateHexViewerPanel() { separator(), vbox(rows) | frame | flex, separator(), - text(absl::StrFormat("Offset: 0x%08X", offset)) | color(Color::Cyan), + text(absl::StrFormat("Offset: 0x%08X", *offset)) | color(Color::Cyan), separator(), text("↑/↓: Scroll | q: Back") | dim | center }) | border; @@ -286,7 +350,7 @@ Component UnifiedLayout::CreateHexViewerPanel() { } Component UnifiedLayout::CreatePaletteEditorPanel() { - return Renderer([&] { + return Renderer([this] { return vbox({ text("🎨 Palette Editor") | bold | center, separator(), @@ -302,7 +366,7 @@ Component UnifiedLayout::CreatePaletteEditorPanel() { } Component UnifiedLayout::CreateTodoManagerPanel() { - return Renderer([&] { + return Renderer([this] { return vbox({ text("📝 TODO Manager") | bold | center, separator(), @@ -318,19 +382,22 @@ Component UnifiedLayout::CreateTodoManagerPanel() { } Component UnifiedLayout::CreateRomToolsPanel() { - static int selected = 0; - static const std::vector tools = { - "Apply Asar Patch", - "Apply BPS Patch", - "Extract Symbols", - "Validate Assembly", - "Generate Save File", - "Back" + struct ToolsState { + int selected = 0; + std::vector items = { + "Apply Asar Patch", + "Apply BPS Patch", + "Extract Symbols", + "Validate Assembly", + "Generate Save File", + "Back" + }; }; - auto menu = Menu(&tools, &selected); + auto state = std::make_shared(); + auto menu = Menu(&state->items, &state->selected); - return Renderer(menu, [&] { + return Renderer(menu, [this, menu, state] { return vbox({ text("🔧 ROM Tools") | bold | center, separator(), @@ -342,16 +409,19 @@ Component UnifiedLayout::CreateRomToolsPanel() { } Component UnifiedLayout::CreateGraphicsToolsPanel() { - static int selected = 0; - static const std::vector tools = { - "Palette Editor", - "Hex Viewer", - "Back" + struct ToolsState { + int selected = 0; + std::vector items = { + "Palette Editor", + "Hex Viewer", + "Back" + }; }; - auto menu = Menu(&tools, &selected); + auto state = std::make_shared(); + auto menu = Menu(&state->items, &state->selected); - return Renderer(menu, [&] { + return Renderer(menu, [this, menu, state] { return vbox({ text("🎨 Graphics Tools") | bold | center, separator(), @@ -363,118 +433,228 @@ Component UnifiedLayout::CreateGraphicsToolsPanel() { } Component UnifiedLayout::CreateSettingsPanel() { - return Renderer([&] { + struct SettingsState { + int left_width_slider; + int right_width_slider; + int bottom_height_slider; + }; + + auto state = std::make_shared(); + state->left_width_slider = config_.left_panel_width; + state->right_width_slider = config_.right_panel_width; + state->bottom_height_slider = config_.bottom_panel_height; + + auto left_width_control = Slider("Left Panel Width: ", &state->left_width_slider, 20, 60, 1); + auto right_width_control = Slider("Right Panel Width: ", &state->right_width_slider, 30, 60, 1); + auto bottom_height_control = Slider("Bottom Panel Height: ", &state->bottom_height_slider, 10, 30, 1); + + auto apply_button = Button("Apply Changes", [this, state] { + config_.left_panel_width = state->left_width_slider; + config_.right_panel_width = state->right_width_slider; + config_.bottom_panel_height = state->bottom_height_slider; + screen_.PostEvent(Event::Custom); + }); + + auto reset_button = Button("Reset to Defaults", [this, state] { + state->left_width_slider = 30; + state->right_width_slider = 40; + state->bottom_height_slider = 15; + config_.left_panel_width = 30; + config_.right_panel_width = 40; + config_.bottom_panel_height = 15; + screen_.PostEvent(Event::Custom); + }); + + auto controls = Container::Vertical({ + left_width_control, + right_width_control, + bottom_height_control, + Container::Horizontal({ + apply_button, + reset_button + }) + }); + + return Renderer(controls, [this, controls, state, left_width_control, right_width_control, + bottom_height_control, apply_button, reset_button] { return vbox({ - text("⚙️ Settings") | bold | center, + text("⚙️ Layout Configuration") | bold | center | color(Color::Cyan), separator(), - text("Settings panel") | center, - text("coming soon...") | center | dim, + text("Customize the TUI layout") | center | dim, separator(), - text("This panel will contain") | center, - text("application settings") | center, + hbox({ + text("Left Panel Width: ") | bold, + text(absl::StrFormat("%d", state->left_width_slider)) + }), + left_width_control->Render(), separator(), - text("Press 'q' to go back") | dim | center - }) | border; + hbox({ + text("Right Panel Width: ") | bold, + text(absl::StrFormat("%d", state->right_width_slider)) + }), + right_width_control->Render(), + separator(), + hbox({ + text("Bottom Panel Height: ") | bold, + text(absl::StrFormat("%d", state->bottom_height_slider)) + }), + bottom_height_control->Render(), + separator(), + hbox({ + apply_button->Render(), + text(" "), + reset_button->Render() + }) | center, + separator(), + text("Panel Visibility:") | bold, + hbox({ + text("Chat: ") | bold, + text(config_.show_chat ? "ON ✓" : "OFF ✗") | + color(config_.show_chat ? Color::Green : Color::Red), + text(" "), + text("Status: ") | bold, + text(config_.show_status ? "ON ✓" : "OFF ✗") | + color(config_.show_status ? Color::Green : Color::Red) + }) | center, + separator(), + text("Keyboard Shortcuts:") | bold, + text(" c - Toggle chat panel") | dim, + text(" s - Toggle status panel") | dim, + text(" Esc/b - Back to menu") | dim, + separator(), + text("Changes apply immediately") | center | dim + }); }); } Component UnifiedLayout::CreateHelpPanel() { - return Renderer([&] { + return Renderer([this] { return vbox({ - text("❓ Help") | bold | center, + text("❓ Z3ED Help") | bold | center | color(Color::Cyan), separator(), - text("Z3ED Unified Layout Help") | center, + text("Unified TUI Layout - ROM Editor & AI Agent") | center | dim, separator(), - text("Global Shortcuts:") | bold, - text(" q - Quit application"), - text(" h - Show this help"), - text(" c - Toggle chat panel"), - text(" s - Toggle status panel"), - text(" t - Switch to tools"), - text(" m - Switch to main menu"), + text("Global Shortcuts:") | bold | color(Color::Yellow), + text(" q - Quit application"), + text(" h - Show this help"), + text(" m - Main menu"), + text(" Esc/b - Back to previous panel"), separator(), - text("Navigation:") | bold, - text(" ↑/↓ - Navigate menus"), - text(" Enter - Select item"), - text(" Tab - Switch panels"), + text("Panel Controls:") | bold | color(Color::Yellow), + text(" c - Toggle chat panel"), + text(" s - Toggle status panel"), + text(" f - Open full chat interface"), + text(" t - ROM tools"), separator(), - text("Press 'q' to go back") | dim | center - }) | border; + text("Navigation:") | bold | color(Color::Yellow), + text(" ↑/↓ - Navigate menus"), + text(" Enter - Select item"), + text(" Tab - Switch focus"), + separator(), + text("Chat Commands:") | bold | color(Color::Yellow), + text(" /exit - Exit chat"), + text(" /clear - Clear history"), + text(" /help - Show chat help"), + separator(), + text("Available Tools:") | bold | color(Color::Green), + text(" • Hex Viewer - Inspect ROM data"), + text(" • Palette Editor - Edit color palettes"), + text(" • TODO Manager - Track tasks"), + text(" • AI Chat - Natural language ROM queries"), + text(" • Dungeon Tools - Room inspection & editing"), + text(" • Graphics Tools - Sprite & tile editing"), + separator(), + text("Press 'Esc' or 'b' to go back") | dim | center + }); }); } Component UnifiedLayout::CreateUnifiedLayout() { - top_layout_ = CreateTopLayout(); - bottom_layout_ = CreateBottomLayout(); + // Create a container that holds all panels + auto all_panels = Container::Tab({ + main_menu_panel_, + chat_panel_, + status_panel_, + tools_panel_, + hex_viewer_panel_, + palette_editor_panel_, + todo_manager_panel_, + rom_tools_panel_, + graphics_tools_panel_, + settings_panel_, + help_panel_ + }, nullptr); - // Create vertical split between top and bottom - return ResizableSplitBottom( - top_layout_, - bottom_layout_, - &config_.bottom_panel_height - ); + // Create a renderer that dynamically shows the right panels based on state + return Renderer(all_panels, [this, all_panels] { + // Dynamically select left panel based on current state + Component left_panel; + switch (state_.active_main_panel) { + case PanelType::kMainMenu: + left_panel = main_menu_panel_; + break; + case PanelType::kHexViewer: + left_panel = hex_viewer_panel_; + break; + case PanelType::kPaletteEditor: + left_panel = palette_editor_panel_; + break; + case PanelType::kTodoManager: + left_panel = todo_manager_panel_; + break; + case PanelType::kRomTools: + left_panel = rom_tools_panel_; + break; + case PanelType::kGraphicsTools: + left_panel = graphics_tools_panel_; + break; + case PanelType::kSettings: + left_panel = settings_panel_; + break; + case PanelType::kHelp: + left_panel = help_panel_; + break; + default: + left_panel = main_menu_panel_; + break; + } + + // Dynamically select right panel + Component right_panel = config_.show_status ? status_panel_ : tools_panel_; + + // Create horizontal layout + auto top_section = hbox({ + left_panel->Render() | flex, + separator(), + right_panel->Render() | size(WIDTH, EQUAL, config_.right_panel_width) + }); + + // Add chat panel if enabled + if (config_.show_chat) { + return vbox({ + top_section | flex, + separator(), + chat_panel_->Render() | size(HEIGHT, EQUAL, config_.bottom_panel_height) + }); + } + + return top_section; + }); } -Component UnifiedLayout::CreateTopLayout() { - // Left panel: Main menu or tools - Component left_panel; - switch (state_.active_main_panel) { - case PanelType::kMainMenu: - left_panel = main_menu_panel_; - break; - case PanelType::kHexViewer: - left_panel = hex_viewer_panel_; - break; - case PanelType::kPaletteEditor: - left_panel = palette_editor_panel_; - break; - case PanelType::kTodoManager: - left_panel = todo_manager_panel_; - break; - case PanelType::kRomTools: - left_panel = rom_tools_panel_; - break; - case PanelType::kGraphicsTools: - left_panel = graphics_tools_panel_; - break; - case PanelType::kSettings: - left_panel = settings_panel_; - break; - case PanelType::kHelp: - left_panel = help_panel_; - break; - default: - left_panel = main_menu_panel_; - break; - } - - // Right panel: Status or tools - Component right_panel; - if (config_.show_status) { - right_panel = status_panel_; - } else { - right_panel = tools_panel_; - } - - // Create horizontal split between left and right - return ResizableSplitRight( - left_panel, - right_panel, - &config_.right_panel_width - ); -} - -Component UnifiedLayout::CreateBottomLayout() { - if (!config_.show_chat) { - return Renderer([] { return text(""); }); - } - - return chat_panel_; -} bool UnifiedLayout::HandleGlobalEvents(const Event& event) { + // Back to main menu + if (event == Event::Escape || event == Event::Character('b')) { + if (state_.active_main_panel != PanelType::kMainMenu) { + SwitchMainPanel(PanelType::kMainMenu); + return true; + } + } + // Global shortcuts - if (event == Event::Character('q')) { + if (event == Event::Character('q') || + (event == Event::Character('q') && state_.active_main_panel == PanelType::kMainMenu)) { screen_.Exit(); return true; } @@ -504,10 +684,20 @@ bool UnifiedLayout::HandleGlobalEvents(const Event& event) { return true; } + if (event == Event::Character('f')) { + // Launch full chat interface + if (chat_tui_) { + screen_.ExitLoopClosure()(); // Exit current loop + chat_tui_->Run(); // Run full chat + screen_.PostEvent(Event::Custom); // Refresh when we return + } + return true; + } + return false; } -bool UnifiedLayout::HandlePanelEvents(const Event& event) { +bool UnifiedLayout::HandlePanelEvents(const Event& /* event */) { // Panel-specific event handling return false; } diff --git a/src/cli/tui/unified_layout.h b/src/cli/tui/unified_layout.h index 2cc16f57..d6b4a9df 100644 --- a/src/cli/tui/unified_layout.h +++ b/src/cli/tui/unified_layout.h @@ -10,6 +10,7 @@ #include "app/rom.h" #include "cli/tui/chat_tui.h" +#include "cli/tui/hex_viewer.h" namespace yaze { namespace cli { @@ -91,8 +92,6 @@ class UnifiedLayout { // Layout assembly ftxui::Component CreateUnifiedLayout(); - ftxui::Component CreateTopLayout(); - ftxui::Component CreateBottomLayout(); // Event handling bool HandleGlobalEvents(const ftxui::Event& event); @@ -110,6 +109,7 @@ class UnifiedLayout { // Components std::unique_ptr chat_tui_; + std::unique_ptr hex_viewer_component_; // Panel components (cached for performance) ftxui::Component main_menu_panel_; @@ -126,8 +126,6 @@ class UnifiedLayout { // Layout components ftxui::Component unified_layout_; - ftxui::Component top_layout_; - ftxui::Component bottom_layout_; // Event handlers std::function global_event_handler_; diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index cc05f173..b92560aa 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -51,6 +51,7 @@ add_executable( cli/tui/unified_layout.cc cli/tui/enhanced_chat_component.cc cli/tui/enhanced_status_panel.cc + cli/tui/hex_viewer.cc cli/handlers/compress.cc cli/handlers/patch.cc cli/handlers/tile16_transfer.cc @@ -76,6 +77,7 @@ add_executable( cli/handlers/agent/dialogue_tool_commands.cc cli/handlers/agent/music_tool_commands.cc cli/handlers/agent/sprite_tool_commands.cc + cli/handlers/agent/dungeon_emulator_tool_commands.cc cli/flags.cc cli/tui/asar_patch.cc cli/tui/palette_editor.cc