60 static std::string patch_file;
61 static std::string base_file;
63 auto patch_file_input = Input(&patch_file,
"Patch file path");
64 auto base_file_input = Input(&base_file,
"Base file path");
67 auto apply_button = Button(
"Apply Patch", [&] {
68 std::vector<uint8_t> source = app_context.rom.vector();
72 std::vector<uint8_t> patch;
74 std::copy(patch_contents.begin(), patch_contents.end(),
75 std::back_inserter(patch));
76 std::vector<uint8_t> patched;
80 }
catch (
const std::runtime_error &e) {
81 app_context.error_message = e.what();
88 auto dot_pos = base_file.find_last_of(
'.');
89 auto patched_file = base_file.substr(0, dot_pos) +
"_patched" +
90 base_file.substr(dot_pos, base_file.size() - dot_pos);
91 std::ofstream file(patched_file, std::ios::binary);
92 if (!file.is_open()) {
93 app_context.error_message =
"Could not open file for writing.";
98 file.write(
reinterpret_cast<const char *
>(patched.data()), patched.size());
105 auto return_button = Button(
"Back to Main Menu", [&] {
110 auto container = Container::Vertical({
117 auto renderer = Renderer(container, [&] {
118 return vbox({text(
"Apply BPS Patch") | center, separator(),
119 text(
"Enter Patch File:"), patch_file_input->Render(),
120 text(
"Enter Base File:"), base_file_input->Render(),
123 apply_button->Render() | center,
125 return_button->Render() | center,
130 screen.Loop(renderer);
137 const static std::vector<std::string> items = {
"Bow",
162 constexpr size_t kNumItems = 28;
163 std::array<bool, kNumItems> values = {};
164 auto checkboxes = Container::Vertical({});
165 for (
size_t i = 0; i < items.size(); i += 4) {
166 auto row = Container::Horizontal({});
167 for (
size_t j = 0; j < 4 && (i + j) < items.size(); ++j) {
169 Checkbox(absl::StrCat(items[i + j],
" ").data(), &values[i + j]));
171 checkboxes->Add(row);
178 static int sword = 0;
179 static int shield = 0;
180 static int armor = 0;
182 const std::vector<std::string> sword_items = {
"Fighter",
"Master",
"Tempered",
184 const std::vector<std::string> shield_items = {
"Small",
"Fire",
"Mirror"};
185 const std::vector<std::string> armor_items = {
"Green",
"Blue",
"Red"};
187 auto sword_radiobox = Radiobox(&sword_items, &sword);
188 auto shield_radiobox = Radiobox(&shield_items, &shield);
189 auto armor_radiobox = Radiobox(&armor_items, &armor);
190 auto equipment_container = Container::Vertical({
196 auto save_button = Button(
"Generate Save File", [&] {
206 auto container = Container::Vertical({
213 auto renderer = Renderer(container, [&] {
214 return vbox({text(
"Generate Save File") | center, separator(),
215 text(
"Select items to include in the save file:"),
216 checkboxes->Render(), separator(),
217 equipment_container->Render(), separator(),
219 save_button->Render() | center,
221 back_button->Render() | center,
226 screen.Loop(renderer);
231 static bool initialized =
false;
237 static std::string new_todo_description;
238 static int selected_todo = 0;
240 auto refresh_todos = [&]() {
242 std::vector<std::string> entries;
243 for (
const auto& item : todos) {
244 std::string status_emoji;
245 switch (item.status) {
252 entries.push_back(absl::StrFormat(
"%s [%s] %s", status_emoji, item.id, item.description));
257 static std::vector<std::string> todo_entries = refresh_todos();
259 auto input_field = Input(&new_todo_description,
"New TODO description");
260 auto add_button = Button(
"Add", [&]() {
261 if (!new_todo_description.empty()) {
262 manager.CreateTodo(new_todo_description);
263 new_todo_description.clear();
264 todo_entries = refresh_todos();
268 auto complete_button = Button(
"Complete", [&]() {
270 if (selected_todo < todos.size()) {
271 manager.UpdateStatus(todos[selected_todo].id, agent::TodoItem::Status::COMPLETED);
272 todo_entries = refresh_todos();
276 auto delete_button = Button(
"Delete", [&]() {
278 if (selected_todo < todos.size()) {
279 manager.DeleteTodo(todos[selected_todo].id);
280 if (selected_todo >= todo_entries.size() -1) {
283 todo_entries = refresh_todos();
289 auto todo_menu = Menu(&todo_entries, &selected_todo);
291 auto container = Container::Vertical({
292 Container::Horizontal({input_field, add_button}),
294 Container::Horizontal({complete_button, delete_button, back_button}),
297 auto renderer = Renderer(container, [&] {
299 text(
"📝 TODO Manager") | bold | center,
301 hbox({text(
"New: "), input_field->Render(), add_button->Render()}),
303 todo_menu->Render() | vscroll_indicator | frame | flex,
305 hbox({complete_button->Render(), delete_button->Render(), back_button->Render()}) | center,
309 screen.Loop(renderer);
397 static std::string asm_file;
398 static std::vector<std::string> symbols_list;
399 static std::string output_message;
401 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
403 auto extract_button = Button(
"Extract Symbols", [&] {
404 if (asm_file.empty()) {
405 app_context.error_message =
"Please specify an assembly file";
406 SwitchComponents(screen, LayoutID::kError);
411 core::AsarWrapper wrapper;
412 auto init_status = wrapper.Initialize();
413 if (!init_status.ok()) {
414 app_context.error_message = absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
415 SwitchComponents(screen, LayoutID::kError);
419 auto symbols_result = wrapper.ExtractSymbols(asm_file);
420 if (!symbols_result.ok()) {
421 app_context.error_message = absl::StrCat(
"Symbol extraction failed: ", symbols_result.status().message());
426 const auto& symbols = symbols_result.value();
427 output_message = absl::StrFormat(
"✅ Extracted %d symbols from %s",
428 symbols.size(), asm_file);
430 symbols_list.clear();
431 for (
const auto& symbol : symbols) {
432 symbols_list.push_back(absl::StrFormat(
"%-20s @ $%06X",
433 symbol.name, symbol.address));
436 }
catch (
const std::exception& e) {
437 app_context.error_message =
"Exception: " + std::string(e.what());
442 auto back_button = Button(
"Back to Main Menu", [&] {
443 output_message.clear();
444 symbols_list.clear();
448 auto container = Container::Vertical({
454 auto renderer = Renderer(container, [&] {
455 std::vector<Element> elements = {
456 text(
"Extract Assembly Symbols") | center | bold,
458 text(
"Assembly File:"),
459 asm_file_input->Render(),
461 extract_button->Render() | center,
464 if (!output_message.empty()) {
465 elements.push_back(separator());
466 elements.push_back(text(output_message) | color(Color::Green));
468 if (!symbols_list.empty()) {
469 elements.push_back(separator());
470 elements.push_back(text(
"Symbols:") | bold);
472 std::vector<Element> symbol_elements;
473 for (
const auto& symbol : symbols_list) {
474 symbol_elements.push_back(text(symbol) | color(Color::Cyan));
476 elements.push_back(vbox(symbol_elements) | frame | size(HEIGHT, LESS_THAN, 15));
480 elements.push_back(separator());
481 elements.push_back(back_button->Render() | center);
483 return vbox(elements) | center | border;
486 screen.Loop(renderer);
490 static std::string asm_file;
491 static std::string output_message;
492 static Color output_color = Color::White;
494 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
496 auto validate_button = Button(
"Validate Assembly", [&] {
497 if (asm_file.empty()) {
498 app_context.error_message =
"Please specify an assembly file";
499 SwitchComponents(screen, LayoutID::kError);
504 core::AsarWrapper wrapper;
505 auto init_status = wrapper.Initialize();
506 if (!init_status.ok()) {
507 app_context.error_message = absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
508 SwitchComponents(screen, LayoutID::kError);
512 auto validation_status = wrapper.ValidateAssembly(asm_file);
513 if (validation_status.ok()) {
514 output_message =
"✅ Assembly file is valid!";
515 output_color = Color::Green;
517 output_message = absl::StrCat(
"❌ Validation failed:\n", validation_status.message());
518 output_color = Color::Red;
521 }
catch (
const std::exception& e) {
522 app_context.error_message =
"Exception: " + std::string(e.what());
527 auto back_button = Button(
"Back to Main Menu", [&] {
528 output_message.clear();
529 SwitchComponents(screen, LayoutID::kMainMenu);
532 auto container = Container::Vertical({
538 auto renderer = Renderer(container, [&] {
539 std::vector<Element> elements = {
540 text(
"Validate Assembly File") | center | bold,
542 text(
"Assembly File:"),
543 asm_file_input->Render(),
545 validate_button->Render() | center,
548 if (!output_message.empty()) {
549 elements.push_back(separator());
550 elements.push_back(text(output_message) | color(output_color));
553 elements.push_back(separator());
554 elements.push_back(back_button->Render() | center);
556 return vbox(elements) | center | border;
559 screen.Loop(renderer);
620 auto help_text = vbox({
622 text(
"╔══════════════════════════════════════════════════════════╗") | color(Color::Cyan1) | bold,
623 text(
"║ Z3ED v0.3.2 - AI-Powered CLI ║") | color(Color::Cyan1) | bold,
624 text(
"║ The Legend of Zelda: A Link to the Past Editor ║") | color(Color::White),
625 text(
"╚══════════════════════════════════════════════════════════╝") | color(Color::Cyan1) | bold,
628 text(
"✨ Author: ") | color(Color::Yellow1) | bold,
629 text(
"scawful") | color(Color::Magenta),
630 text(
" │ ") | color(Color::GrayDark),
631 text(
"🤖 AI: ") | color(Color::Green1) | bold,
632 text(
"Ollama + Gemini Integration") | color(Color::GreenLight),
639 text(
"🤖 AI AGENT COMMANDS") | bold | color(Color::Green1) | center,
640 text(
" Conversational AI for ROM inspection and modification") | color(Color::GreenLight) | center,
642 hbox({text(
" "), text(
"💬 Test Chat Mode") | bold | color(Color::Cyan), filler(),
643 text(
"agent test-conversation") | color(Color::White),
644 text(
" [--rom=<file>] [--verbose]") | color(Color::GrayLight)}),
645 hbox({text(
" "), text(
"→ Interactive AI testing with embedded labels") | color(Color::GrayLight)}),
647 hbox({text(
" "), text(
"📊 Chat with AI") | bold | color(Color::Cyan), filler(),
648 text(
"agent chat") | color(Color::White),
649 text(
" <prompt> [--host] [--port]") | color(Color::GrayLight)}),
650 hbox({text(
" "), text(
"→ Natural language ROM inspection (rooms, sprites, entrances)") | color(Color::GrayLight)}),
652 hbox({text(
" "), text(
"🎯 Simple Chat") | bold | color(Color::Cyan), filler(),
653 text(
"agent simple-chat") | color(Color::White),
654 text(
" <prompt> [--rom=<file>]") | color(Color::GrayLight)}),
655 hbox({text(
" "), text(
"→ Quick AI queries with automatic ROM loading") | color(Color::GrayLight)}),
660 text(
"🎯 ASAR 65816 ASSEMBLER") | bold | color(Color::Yellow1) | center,
661 text(
" Assemble and patch with Asar integration") | color(Color::YellowLight) | center,
663 hbox({text(
" "), text(
"⚡ Apply Patch") | bold | color(Color::Cyan), filler(),
664 text(
"patch apply-asar") | color(Color::White),
665 text(
" <patch.asm> [--rom=<file>]") | color(Color::GrayLight)}),
666 hbox({text(
" "), text(
"🔍 Extract Symbols") | bold | color(Color::Cyan), filler(),
667 text(
"patch extract-symbols") | color(Color::White),
668 text(
" <patch.asm>") | color(Color::GrayLight)}),
669 hbox({text(
" "), text(
"✓ Validate Assembly") | bold | color(Color::Cyan), filler(),
670 text(
"patch validate") | color(Color::White),
671 text(
" <patch.asm>") | color(Color::GrayLight)}),
676 text(
"📦 PATCH MANAGEMENT") | bold | color(Color::Blue) | center,
678 hbox({text(
" "), text(
"Apply BPS Patch") | color(Color::Cyan), filler(),
679 text(
"patch apply-bps") | color(Color::White),
680 text(
" <patch.bps> [--rom=<file>]") | color(Color::GrayLight)}),
681 hbox({text(
" "), text(
"Create BPS Patch") | color(Color::Cyan), filler(),
682 text(
"patch create") | color(Color::White),
683 text(
" <src> <modified>") | color(Color::GrayLight)}),
688 text(
"🗃️ ROM OPERATIONS") | bold | color(Color::Magenta) | center,
690 hbox({text(
" "), text(
"Show ROM Info") | color(Color::Cyan), filler(),
691 text(
"rom info") | color(Color::White),
692 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
693 hbox({text(
" "), text(
"Validate ROM") | color(Color::Cyan), filler(),
694 text(
"rom validate") | color(Color::White),
695 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
696 hbox({text(
" "), text(
"Compare ROMs") | color(Color::Cyan), filler(),
697 text(
"rom diff") | color(Color::White),
698 text(
" <rom_a> <rom_b>") | color(Color::GrayLight)}),
699 hbox({text(
" "), text(
"Backup ROM") | color(Color::Cyan), filler(),
700 text(
"rom backup") | color(Color::White),
701 text(
" <rom_file> [name]") | color(Color::GrayLight)}),
702 hbox({text(
" "), text(
"Expand ROM") | color(Color::Cyan), filler(),
703 text(
"rom expand") | color(Color::White),
704 text(
" <rom_file> <size>") | color(Color::GrayLight)}),
709 text(
"🏰 EMBEDDED RESOURCE LABELS") | bold | color(Color::Red1) | center,
710 text(
" All Zelda3 names built-in and always available to AI") | color(Color::RedLight) | center,
712 hbox({text(
" 📚 296+ Room Names") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
713 text(
"256 Sprite Names") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
714 text(
"133 Entrance Names") | color(Color::GreenLight)}),
715 hbox({text(
" 🎨 100 Item Names") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
716 text(
"160 Overworld Maps") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
717 text(
"48 Music Tracks") | color(Color::GreenLight)}),
718 hbox({text(
" 🔧 60 Tile Types") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
719 text(
"26 Overlord Names") | color(Color::GreenLight), text(
" │ ") | color(Color::GrayDark),
720 text(
"32 GFX Sheets") | color(Color::GreenLight)}),
724 text(
"🌐 GLOBAL FLAGS") | bold | color(Color::White) | center,
726 hbox({text(
" --tui") | color(Color::Cyan), filler(),
727 text(
"Launch Text User Interface") | color(Color::GrayLight)}),
728 hbox({text(
" --rom=<file>") | color(Color::Cyan), filler(),
729 text(
"Specify ROM file") | color(Color::GrayLight)}),
730 hbox({text(
" --output=<file>") | color(Color::Cyan), filler(),
731 text(
"Specify output file") | color(Color::GrayLight)}),
732 hbox({text(
" --verbose") | color(Color::Cyan), filler(),
733 text(
"Enable verbose output") | color(Color::GrayLight)}),
734 hbox({text(
" --dry-run") | color(Color::Cyan), filler(),
735 text(
"Test without changes") | color(Color::GrayLight)}),
738 text(
"Press 'q' to quit, '/' for command palette, 'h' for help") | center | color(Color::GrayLight),
744 auto container = Container::Vertical({
748 auto renderer = Renderer(container, [&] {
750 help_text | vscroll_indicator | frame | flex,
752 back_button->Render() | center,
757 screen.Loop(renderer);
762 static int selected = 0;
764 option.focused_entry = &selected;
767 auto content_renderer = ftxui::Renderer([&] {
771 text(
"Welcome to the z3ed Dashboard!") | center,
772 text(
"Select a tool from the menu to begin.") | center | dim,
776 auto main_container = Container::Horizontal({
781 auto layout = Renderer(main_container, [&] {
782 std::string rom_info = app_context.rom.is_loaded() ? app_context.rom.title() :
"No ROM";
785 menu->Render() | size(WIDTH, EQUAL, 30) | border,
786 (content_renderer->Render() | center | flex) | border
789 text(rom_info) | bold,
791 text(
"q: Quit | ↑/↓: Navigate | Enter: Select")
796 auto event_handler = CatchEvent(layout, [&](
const Event& event) {
797 if (event == Event::Character(
'q')) {
801 if (event == Event::Return) {
804 case MainMenuEntry::kLoadRom:
SwitchComponents(screen, LayoutID::kLoadRom);
break;
805 case MainMenuEntry::kAIAgentChat:
SwitchComponents(screen, LayoutID::kAIAgentChat);
break;
806 case MainMenuEntry::kTodoManager:
SwitchComponents(screen, LayoutID::kTodoManager);
break;
807 case MainMenuEntry::kRomTools:
SwitchComponents(screen, LayoutID::kRomTools);
break;
808 case MainMenuEntry::kGraphicsTools:
SwitchComponents(screen, LayoutID::kGraphicsTools);
break;
809 case MainMenuEntry::kTestingTools:
SwitchComponents(screen, LayoutID::kTestingTools);
break;
810 case MainMenuEntry::kSettings:
SwitchComponents(screen, LayoutID::kSettings);
break;
811 case MainMenuEntry::kHelp:
SwitchComponents(screen, LayoutID::kHelp);
break;
812 case MainMenuEntry::kExit:
SwitchComponents(screen, LayoutID::kExit);
break;
819 screen.Loop(event_handler);
824 static int selected = 0;
826 option.focused_entry = &selected;
829 menu, [&](Event event) {
return HandleInput(screen, event, selected); });
831 std::string rom_information =
"ROM not loaded";
832 if (app_context.rom.is_loaded()) {
833 rom_information = app_context.rom.title();
838 text(
" ███████╗██████╗ ███████╗██████╗ ") | color(Color::Cyan1) | bold,
839 text(
" ╚══███╔╝╚════██╗██╔════╝██╔══██╗") | color(Color::Cyan1) | bold,
840 text(
" ███╔╝ █████╔╝█████╗ ██║ ██║") | color(Color::Cyan1) | bold,
841 text(
" ███╔╝ ╚═══██╗██╔══╝ ██║ ██║") | color(Color::Cyan1) | bold,
842 text(
" ███████╗██████╔╝███████╗██████╔╝") | color(Color::Cyan1) | bold,
843 text(
" ╚══════╝╚═════╝ ╚══════╝╚═════╝ ") | color(Color::Cyan1) | bold,
846 text(
" ▲ ") | color(Color::Yellow1) | bold,
847 text(
"Zelda 3 Editor") | color(Color::White) | bold,
850 text(
" ▲ ▲ ") | color(Color::Yellow1) | bold,
851 text(
"AI-Powered CLI") | color(Color::GrayLight),
853 text(
" ▲▲▲▲▲ ") | color(Color::Yellow1) | bold | center,
856 auto title = border(hbox({
857 text(
"v0.3.2") | bold | color(Color::Green1),
859 text(rom_information) | bold | color(Color::Red1),
862 auto renderer = Renderer(menu, [&] {
869 menu->Render() | center,
874 auto main_component = CatchEvent(renderer, [&](
const Event& event) {
875 if (event == Event::Return) {
877 case MainMenuEntry::kLoadRom:
880 case MainMenuEntry::kAIAgentChat:
883 case MainMenuEntry::kTodoManager:
886 case MainMenuEntry::kRomTools:
889 case MainMenuEntry::kGraphicsTools:
892 case MainMenuEntry::kTestingTools:
895 case MainMenuEntry::kSettings:
898 case MainMenuEntry::kHelp:
901 case MainMenuEntry::kExit:
907 if (event == Event::Character(
'q')) {
914 screen.Loop(main_component);