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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,58 @@ absl::Status HandlePaletteSetColor(const std::vector<std::string>& arg_vec,
|
||||
absl::Status HandlePaletteAnalyze(const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
|
||||
// Dungeon editing commands
|
||||
absl::Status HandleDungeonExportRoomCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleDungeonListObjectsCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleDungeonGetRoomTilesCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleDungeonSetRoomPropertyCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
|
||||
// Emulator & Debugger commands
|
||||
absl::Status HandleEmulatorStepCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorRunCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorPauseCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorResetCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorGetStateCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorSetBreakpointCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorClearBreakpointCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorListBreakpointsCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorReadMemoryCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorWriteMemoryCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorGetRegistersCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
absl::Status HandleEmulatorGetMetricsCommand(
|
||||
const std::vector<std::string>& arg_vec,
|
||||
Rom* rom_context = nullptr);
|
||||
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
352
src/cli/handlers/agent/dungeon_emulator_tool_commands.cc
Normal file
352
src/cli/handlers/agent/dungeon_emulator_tool_commands.cc
Normal file
@@ -0,0 +1,352 @@
|
||||
#include "cli/handlers/agent/commands.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#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<std::string, std::string> ParseArgs(const std::vector<std::string>& args) {
|
||||
std::map<std::string, std::string> 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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
|
||||
|
||||
@@ -87,6 +87,38 @@ absl::StatusOr<std::string> 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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<std::string> symbols_list;
|
||||
static bool show_symbols = false;
|
||||
struct AsarState {
|
||||
std::string patch_file;
|
||||
std::string output_message;
|
||||
std::vector<std::string> symbols_list;
|
||||
bool show_symbols = false;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<AsarState>();
|
||||
|
||||
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<Component> 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<Element> 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<Element> 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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#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<std::string> actions = {
|
||||
"Quick Actions Menu - Not Yet Implemented",
|
||||
"⬅️ Back",
|
||||
struct MenuState {
|
||||
int selected = 0;
|
||||
std::vector<std::string> actions = {
|
||||
"Quick Actions Menu - Not Yet Implemented",
|
||||
"⬅️ Back",
|
||||
};
|
||||
};
|
||||
|
||||
auto menu = Menu(&actions, &selected);
|
||||
auto state = std::make_shared<MenuState>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<std::string>();
|
||||
|
||||
// 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<Element> 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<std::vector<std::string>> table_rows;
|
||||
table_rows.reserve(msg.table_data->rows.size() + 1);
|
||||
Element body;
|
||||
if (msg.table_data.has_value()) {
|
||||
std::vector<std::vector<std::string>> 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()) {
|
||||
|
||||
@@ -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<absl::Status()> exec;
|
||||
int score = 0;
|
||||
};
|
||||
|
||||
auto state = std::make_shared<PaletteState>();
|
||||
|
||||
static std::vector<Cmd> 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<int> 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<int> 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<int>(i) == state->selected) {
|
||||
item = item | inverted | focus;
|
||||
}
|
||||
items.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
auto search_input = Input(&query, "Search...");
|
||||
|
||||
std::vector<std::string> 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<int>(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<int> 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<int>(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<int>(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<int> 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<int>(filtered_idx.size())) {
|
||||
int idx = filtered_idx[selected];
|
||||
Element details = text("Select a command to see details.") | dim;
|
||||
if (state->selected < static_cast<int>(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<int>(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<int> 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<int>(filtered_idx.size()) - 1) state->selected++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
#include "cli/tui/tui.h"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#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<std::string> 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<std::string> 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<std::string> 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<std::string> 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
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#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);
|
||||
};
|
||||
}
|
||||
|
||||
99
src/cli/tui/hex_viewer.cc
Normal file
99
src/cli/tui/hex_viewer.cc
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "cli/tui/hex_viewer.h"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
HexViewerComponent::HexViewerComponent(Rom* rom, std::function<void()> 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<Element> rows;
|
||||
for (int i = 0; i < lines_to_show_; ++i) {
|
||||
int current_offset = offset_ + (i * 16);
|
||||
if (current_offset >= static_cast<int>(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<int>(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<int>(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<int>(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<int>(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
|
||||
28
src/cli/tui/hex_viewer.h
Normal file
28
src/cli/tui/hex_viewer.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef YAZE_SRC_CLI_TUI_HEX_VIEWER_H_
|
||||
#define YAZE_SRC_CLI_TUI_HEX_VIEWER_H_
|
||||
|
||||
#include <functional>
|
||||
#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<void()> on_back);
|
||||
ftxui::Component Render() override;
|
||||
|
||||
private:
|
||||
Rom* rom_;
|
||||
std::function<void()> 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_
|
||||
@@ -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<Element> rows;
|
||||
for (int i = 0; i < lines_to_show; ++i) {
|
||||
int current_offset = offset + (i * 16);
|
||||
if (current_offset >= static_cast<int>(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<int>(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<int>(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<int>(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<int>(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;
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <ftxui/screen/terminal.hpp>
|
||||
|
||||
#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<std::string> items = {
|
||||
"🔍 Hex Viewer",
|
||||
"🎨 Palette Editor",
|
||||
"📝 TODO Manager",
|
||||
"🔧 ROM Tools",
|
||||
"🎮 Graphics Tools",
|
||||
"⚙️ Settings",
|
||||
"❓ Help",
|
||||
"🚪 Exit"
|
||||
};
|
||||
};
|
||||
|
||||
auto state = std::make_shared<MenuState>();
|
||||
|
||||
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<std::string>();
|
||||
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<std::string> tools = {
|
||||
"🔧 ROM Tools",
|
||||
"🎨 Graphics Tools",
|
||||
"📝 TODO Manager",
|
||||
"⚙️ Settings",
|
||||
"❓ Help"
|
||||
struct ToolsState {
|
||||
int selected = 0;
|
||||
std::vector<std::string> items = {
|
||||
"🔧 ROM Tools",
|
||||
"🎨 Graphics Tools",
|
||||
"📝 TODO Manager",
|
||||
"⚙️ Settings",
|
||||
"❓ Help"
|
||||
};
|
||||
};
|
||||
|
||||
auto menu = Menu(&tools, &selected);
|
||||
auto state = std::make_shared<ToolsState>();
|
||||
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<int>(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<Element> 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<int>(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<std::string> tools = {
|
||||
"Apply Asar Patch",
|
||||
"Apply BPS Patch",
|
||||
"Extract Symbols",
|
||||
"Validate Assembly",
|
||||
"Generate Save File",
|
||||
"Back"
|
||||
struct ToolsState {
|
||||
int selected = 0;
|
||||
std::vector<std::string> 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<ToolsState>();
|
||||
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<std::string> tools = {
|
||||
"Palette Editor",
|
||||
"Hex Viewer",
|
||||
"Back"
|
||||
struct ToolsState {
|
||||
int selected = 0;
|
||||
std::vector<std::string> items = {
|
||||
"Palette Editor",
|
||||
"Hex Viewer",
|
||||
"Back"
|
||||
};
|
||||
};
|
||||
|
||||
auto menu = Menu(&tools, &selected);
|
||||
auto state = std::make_shared<ToolsState>();
|
||||
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<SettingsState>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<tui::ChatTUI> chat_tui_;
|
||||
std::unique_ptr<HexViewerComponent> 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<bool(const ftxui::Event&)> global_event_handler_;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user