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) {
262 entries.push_back(absl::StrFormat(
"%s [%s] %s", status_emoji, item.id,
268 static std::vector<std::string> todo_entries = refresh_todos();
270 auto input_field = Input(&new_todo_description,
"New TODO description");
271 auto add_button = Button(
"Add", [&]() {
272 if (!new_todo_description.empty()) {
273 manager.CreateTodo(new_todo_description);
274 new_todo_description.clear();
275 todo_entries = refresh_todos();
279 auto complete_button = Button(
"Complete", [&]() {
281 if (selected_todo < todos.size()) {
282 manager.UpdateStatus(todos[selected_todo].id,
283 agent::TodoItem::Status::COMPLETED);
284 todo_entries = refresh_todos();
288 auto delete_button = Button(
"Delete", [&]() {
290 if (selected_todo < todos.size()) {
291 manager.DeleteTodo(todos[selected_todo].id);
292 if (selected_todo >= todo_entries.size() - 1) {
295 todo_entries = refresh_todos();
302 auto todo_menu = Menu(&todo_entries, &selected_todo);
304 auto container = Container::Vertical({
305 Container::Horizontal({input_field, add_button}),
307 Container::Horizontal({complete_button, delete_button, back_button}),
310 auto renderer = Renderer(container, [&] {
312 text(
"📝 TODO Manager") | bold | center,
314 hbox({text(
"New: "), input_field->Render(),
315 add_button->Render()}),
317 todo_menu->Render() | vscroll_indicator | frame | flex,
319 hbox({complete_button->Render(), delete_button->Render(),
320 back_button->Render()}) |
326 screen.Loop(renderer);
332 static std::string asm_file;
333 static std::string output_message;
334 static Color output_color = Color::White;
336 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
338 auto apply_button = Button(
"Apply Asar Patch", [&] {
339 if (asm_file.empty()) {
340 app_context.error_message =
"Please specify an assembly file";
341 SwitchComponents(screen, LayoutID::kError);
349 "❌ AsarPatch not yet implemented in new CommandHandler system";
350 output_color = Color::Red;
351 } catch (
const std::exception& e) {
352 output_message =
"Exception: " + std::string(e.what());
353 output_color = Color::Red;
357 auto back_button = Button(
"Back to Main Menu", [&] {
358 output_message.clear();
362 auto container = Container::Vertical({
368 auto renderer = Renderer(container, [&] {
369 std::vector<Element> elements = {
370 text(
"Apply Asar Patch") | center | bold,
372 text(
"Assembly File:"),
373 asm_file_input->Render(),
375 apply_button->Render() | center,
378 if (!output_message.empty()) {
379 elements.push_back(separator());
380 elements.push_back(text(output_message) | color(output_color));
383 elements.push_back(separator());
384 elements.push_back(back_button->Render() | center);
386 return vbox(elements) | center | border;
389 screen.Loop(renderer);
414 static std::string asm_file;
415 static std::vector<std::string> symbols_list;
416 static std::string output_message;
418 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
420 auto extract_button = Button(
"Extract Symbols", [&] {
421 if (asm_file.empty()) {
422 app_context.error_message =
"Please specify an assembly file";
423 SwitchComponents(screen, LayoutID::kError);
428 core::AsarWrapper wrapper;
429 auto init_status = wrapper.Initialize();
430 if (!init_status.ok()) {
431 app_context.error_message =
432 absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
433 SwitchComponents(screen, LayoutID::kError);
437 auto symbols_result = wrapper.ExtractSymbols(asm_file);
438 if (!symbols_result.ok()) {
439 app_context.error_message = absl::StrCat(
440 "Symbol extraction failed: ", symbols_result.status().message());
445 const auto& symbols = symbols_result.value();
446 output_message = absl::StrFormat(
"✅ Extracted %d symbols from %s",
447 symbols.size(), asm_file);
449 symbols_list.clear();
450 for (
const auto& symbol : symbols) {
451 symbols_list.push_back(
452 absl::StrFormat(
"%-20s @ $%06X", symbol.name, symbol.address));
455 }
catch (
const std::exception& e) {
456 app_context.error_message =
"Exception: " + std::string(e.what());
461 auto back_button = Button(
"Back to Main Menu", [&] {
462 output_message.clear();
463 symbols_list.clear();
467 auto container = Container::Vertical({
473 auto renderer = Renderer(container, [&] {
474 std::vector<Element> elements = {
475 text(
"Extract Assembly Symbols") | center | bold,
477 text(
"Assembly File:"),
478 asm_file_input->Render(),
480 extract_button->Render() | center,
483 if (!output_message.empty()) {
484 elements.push_back(separator());
485 elements.push_back(text(output_message) | color(Color::Green));
487 if (!symbols_list.empty()) {
488 elements.push_back(separator());
489 elements.push_back(text(
"Symbols:") | bold);
491 std::vector<Element> symbol_elements;
492 for (
const auto& symbol : symbols_list) {
493 symbol_elements.push_back(text(symbol) | color(Color::Cyan));
495 elements.push_back(vbox(symbol_elements) | frame |
496 size(HEIGHT, LESS_THAN, 15));
500 elements.push_back(separator());
501 elements.push_back(back_button->Render() | center);
503 return vbox(elements) | center | border;
506 screen.Loop(renderer);
510 static std::string asm_file;
511 static std::string output_message;
512 static Color output_color = Color::White;
514 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
516 auto validate_button = Button(
"Validate Assembly", [&] {
517 if (asm_file.empty()) {
518 app_context.error_message =
"Please specify an assembly file";
519 SwitchComponents(screen, LayoutID::kError);
524 core::AsarWrapper wrapper;
525 auto init_status = wrapper.Initialize();
526 if (!init_status.ok()) {
527 app_context.error_message =
528 absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
529 SwitchComponents(screen, LayoutID::kError);
533 auto validation_status = wrapper.ValidateAssembly(asm_file);
534 if (validation_status.ok()) {
535 output_message =
"✅ Assembly file is valid!";
536 output_color = Color::Green;
538 output_message = absl::StrCat(
"❌ Validation failed:\n",
539 validation_status.message());
540 output_color = Color::Red;
543 }
catch (
const std::exception& e) {
544 app_context.error_message =
"Exception: " + std::string(e.what());
549 auto back_button = Button(
"Back to Main Menu", [&] {
550 output_message.clear();
551 SwitchComponents(screen, LayoutID::kMainMenu);
554 auto container = Container::Vertical({
560 auto renderer = Renderer(container, [&] {
561 std::vector<Element> elements = {
562 text(
"Validate Assembly File") | center | bold,
564 text(
"Assembly File:"),
565 asm_file_input->Render(),
567 validate_button->Render() | center,
570 if (!output_message.empty()) {
571 elements.push_back(separator());
572 elements.push_back(text(output_message) | color(output_color));
575 elements.push_back(separator());
576 elements.push_back(back_button->Render() | center);
578 return vbox(elements) | center | border;
581 screen.Loop(renderer);
642 auto help_text = vbox({
644 text(
"╔══════════════════════════════════════════════════════════╗") |
645 color(Color::Cyan1) | bold,
646 text(
"║ Z3ED v0.3.2 - AI-Powered CLI ║") |
647 color(Color::Cyan1) | bold,
648 text(
"║ The Legend of Zelda: A Link to the Past Editor ║") |
650 text(
"╚══════════════════════════════════════════════════════════╝") |
651 color(Color::Cyan1) | bold,
654 text(
"✨ Author: ") | color(Color::Yellow1) | bold,
655 text(
"scawful") | color(Color::Magenta),
656 text(
" │ ") | color(Color::GrayDark),
657 text(
"🤖 AI: ") | color(Color::Green1) | bold,
658 text(
"Ollama + Gemini Integration") | color(Color::GreenLight),
665 text(
"🤖 AI AGENT COMMANDS") | bold | color(Color::Green1) | center,
666 text(
" Conversational AI for ROM inspection and modification") |
667 color(Color::GreenLight) | center,
669 hbox({text(
" "), text(
"💬 Test Chat Mode") | bold | color(Color::Cyan),
670 filler(), text(
"agent test-conversation") | color(Color::White),
671 text(
" [--rom=<file>] [--verbose]") | color(Color::GrayLight)}),
673 text(
"→ Interactive AI testing with embedded labels") |
674 color(Color::GrayLight)}),
676 hbox({text(
" "), text(
"📊 Chat with AI") | bold | color(Color::Cyan),
677 filler(), text(
"agent chat") | color(Color::White),
678 text(
" <prompt> [--host] [--port]") | color(Color::GrayLight)}),
679 hbox({text(
" "), text(
"→ Natural language ROM inspection (rooms, "
680 "sprites, entrances)") |
681 color(Color::GrayLight)}),
683 hbox({text(
" "), text(
"🎯 Simple Chat") | bold | color(Color::Cyan),
684 filler(), text(
"agent simple-chat") | color(Color::White),
685 text(
" <prompt> [--rom=<file>]") | color(Color::GrayLight)}),
687 text(
"→ Quick AI queries with automatic ROM loading") |
688 color(Color::GrayLight)}),
693 text(
"🎯 ASAR 65816 ASSEMBLER") | bold | color(Color::Yellow1) | center,
694 text(
" Assemble and patch with Asar integration") |
695 color(Color::YellowLight) | center,
697 hbox({text(
" "), text(
"⚡ Apply Patch") | bold | color(Color::Cyan),
698 filler(), text(
"patch apply-asar") | color(Color::White),
699 text(
" <patch.asm> [--rom=<file>]") | color(Color::GrayLight)}),
700 hbox({text(
" "), text(
"🔍 Extract Symbols") | bold | color(Color::Cyan),
701 filler(), text(
"patch extract-symbols") | color(Color::White),
702 text(
" <patch.asm>") | color(Color::GrayLight)}),
703 hbox({text(
" "), text(
"✓ Validate Assembly") | bold | color(Color::Cyan),
704 filler(), text(
"patch validate") | color(Color::White),
705 text(
" <patch.asm>") | color(Color::GrayLight)}),
710 text(
"📦 PATCH MANAGEMENT") | bold | color(Color::Blue) | center,
712 hbox({text(
" "), text(
"Apply BPS Patch") | color(Color::Cyan), filler(),
713 text(
"patch apply-bps") | color(Color::White),
714 text(
" <patch.bps> [--rom=<file>]") | color(Color::GrayLight)}),
715 hbox({text(
" "), text(
"Create BPS Patch") | color(Color::Cyan), filler(),
716 text(
"patch create") | color(Color::White),
717 text(
" <src> <modified>") | color(Color::GrayLight)}),
722 text(
"🗃️ ROM OPERATIONS") | bold | color(Color::Magenta) | center,
724 hbox({text(
" "), text(
"Show ROM Info") | color(Color::Cyan), filler(),
725 text(
"rom info") | color(Color::White),
726 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
727 hbox({text(
" "), text(
"Validate ROM") | color(Color::Cyan), filler(),
728 text(
"rom validate") | color(Color::White),
729 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
730 hbox({text(
" "), text(
"Compare ROMs") | color(Color::Cyan), filler(),
731 text(
"rom diff") | color(Color::White),
732 text(
" <rom_a> <rom_b>") | color(Color::GrayLight)}),
733 hbox({text(
" "), text(
"Backup ROM") | color(Color::Cyan), filler(),
734 text(
"rom backup") | color(Color::White),
735 text(
" <rom_file> [name]") | color(Color::GrayLight)}),
736 hbox({text(
" "), text(
"Expand ROM") | color(Color::Cyan), filler(),
737 text(
"rom expand") | color(Color::White),
738 text(
" <rom_file> <size>") | color(Color::GrayLight)}),
743 text(
"🏰 EMBEDDED RESOURCE LABELS") | bold | color(Color::Red1) | center,
744 text(
" All Zelda3 names built-in and always available to AI") |
745 color(Color::RedLight) | center,
747 hbox({text(
" 📚 296+ Room Names") | color(Color::GreenLight),
748 text(
" │ ") | color(Color::GrayDark),
749 text(
"256 Sprite Names") | color(Color::GreenLight),
750 text(
" │ ") | color(Color::GrayDark),
751 text(
"133 Entrance Names") | color(Color::GreenLight)}),
752 hbox({text(
" 🎨 100 Item Names") | color(Color::GreenLight),
753 text(
" │ ") | color(Color::GrayDark),
754 text(
"160 Overworld Maps") | color(Color::GreenLight),
755 text(
" │ ") | color(Color::GrayDark),
756 text(
"48 Music Tracks") | color(Color::GreenLight)}),
757 hbox({text(
" 🔧 60 Tile Types") | color(Color::GreenLight),
758 text(
" │ ") | color(Color::GrayDark),
759 text(
"26 Overlord Names") | color(Color::GreenLight),
760 text(
" │ ") | color(Color::GrayDark),
761 text(
"32 GFX Sheets") | color(Color::GreenLight)}),
765 text(
"🌐 GLOBAL FLAGS") | bold | color(Color::White) | center,
767 hbox({text(
" --tui") | color(Color::Cyan), filler(),
768 text(
"Launch Text User Interface") | color(Color::GrayLight)}),
769 hbox({text(
" --rom=<file>") | color(Color::Cyan), filler(),
770 text(
"Specify ROM file") | color(Color::GrayLight)}),
771 hbox({text(
" --output=<file>") | color(Color::Cyan), filler(),
772 text(
"Specify output file") | color(Color::GrayLight)}),
773 hbox({text(
" --verbose") | color(Color::Cyan), filler(),
774 text(
"Enable verbose output") | color(Color::GrayLight)}),
775 hbox({text(
" --dry-run") | color(Color::Cyan), filler(),
776 text(
"Test without changes") | color(Color::GrayLight)}),
779 text(
"Press 'q' to quit, '/' for command palette, 'h' for help") |
780 center | color(Color::GrayLight),
786 auto container = Container::Vertical({
790 auto renderer = Renderer(container, [&] {
792 help_text | vscroll_indicator | frame | flex,
794 back_button->Render() | center,
799 screen.Loop(renderer);
803 static int selected = 0;
805 option.focused_entry = &selected;
808 auto content_renderer = ftxui::Renderer([&] {
812 text(
"Welcome to the z3ed Dashboard!") | center,
813 text(
"Select a tool from the menu to begin.") | center | dim,
817 auto main_container = Container::Horizontal({menu, content_renderer});
819 auto layout = Renderer(main_container, [&] {
820 std::string rom_info =
821 app_context.rom.is_loaded() ? app_context.rom.title() :
"No ROM";
822 return vbox({hbox({menu->Render() | size(WIDTH, EQUAL, 30) | border,
823 (content_renderer->Render() | center | flex) | border}),
824 hbox({text(rom_info) | bold, filler(),
825 text(
"q: Quit | ↑/↓: Navigate | Enter: Select")}) |
829 auto event_handler = CatchEvent(layout, [&](
const Event& event) {
830 if (event == Event::Character(
'q')) {
834 if (event == Event::Return) {
837 case MainMenuEntry::kLoadRom:
840 case MainMenuEntry::kAIAgentChat:
843 case MainMenuEntry::kTodoManager:
846 case MainMenuEntry::kRomTools:
849 case MainMenuEntry::kGraphicsTools:
852 case MainMenuEntry::kTestingTools:
855 case MainMenuEntry::kSettings:
858 case MainMenuEntry::kHelp:
861 case MainMenuEntry::kExit:
870 screen.Loop(event_handler);
875 static int selected = 0;
877 option.focused_entry = &selected;
882 std::string rom_information =
"ROM not loaded";
883 if (app_context.rom.is_loaded()) {
884 rom_information = app_context.rom.title();
889 text(
" ███████╗██████╗ ███████╗██████╗ ") | color(Color::Cyan1) | bold,
890 text(
" ╚══███╔╝╚════██╗██╔════╝██╔══██╗") | color(Color::Cyan1) | bold,
891 text(
" ███╔╝ █████╔╝█████╗ ██║ ██║") | color(Color::Cyan1) | bold,
892 text(
" ███╔╝ ╚═══██╗██╔══╝ ██║ ██║") | color(Color::Cyan1) | bold,
893 text(
" ███████╗██████╔╝███████╗██████╔╝") | color(Color::Cyan1) | bold,
894 text(
" ╚══════╝╚═════╝ ╚══════╝╚═════╝ ") | color(Color::Cyan1) | bold,
897 text(
" ▲ ") | color(Color::Yellow1) | bold,
898 text(
"Zelda 3 Editor") | color(Color::White) | bold,
901 text(
" ▲ ▲ ") | color(Color::Yellow1) | bold,
902 text(
"AI-Powered CLI") | color(Color::GrayLight),
904 text(
" ▲▲▲▲▲ ") | color(Color::Yellow1) | bold | center,
907 auto title = border(hbox({
908 text(
"v0.3.2") | bold | color(Color::Green1),
910 text(rom_information) | bold | color(Color::Red1),
913 auto renderer = Renderer(menu, [&] {
920 menu->Render() | center,
925 auto main_component = CatchEvent(renderer, [&](
const Event& event) {
926 if (event == Event::Return) {
928 case MainMenuEntry::kLoadRom:
931 case MainMenuEntry::kAIAgentChat:
934 case MainMenuEntry::kTodoManager:
937 case MainMenuEntry::kRomTools:
940 case MainMenuEntry::kGraphicsTools:
943 case MainMenuEntry::kTestingTools:
946 case MainMenuEntry::kSettings:
949 case MainMenuEntry::kHelp:
952 case MainMenuEntry::kExit:
958 if (event == Event::Character(
'q')) {
965 screen.Loop(main_component);