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:
scawful
2025-10-10 20:34:12 -04:00
parent 1435c15400
commit c77ca503ca
18 changed files with 1327 additions and 718 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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()) {

View File

@@ -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;
});
}

View File

@@ -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

View File

@@ -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
View 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
View 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_

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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_;