61 static std::string patch_file;
62 static std::string base_file;
64 auto patch_file_input = Input(&patch_file,
"Patch file path");
65 auto base_file_input = Input(&base_file,
"Base file path");
68 auto apply_button = Button(
"Apply Patch", [&] {
69 std::vector<uint8_t> source = app_context.rom.vector();
73 std::vector<uint8_t> patch;
75 std::copy(patch_contents.begin(), patch_contents.end(),
76 std::back_inserter(patch));
77 std::vector<uint8_t> patched;
81 }
catch (
const std::runtime_error& e) {
82 app_context.error_message = e.what();
89 auto dot_pos = base_file.find_last_of(
'.');
90 auto patched_file = base_file.substr(0, dot_pos) +
"_patched" +
91 base_file.substr(dot_pos, base_file.size() - dot_pos);
92 std::ofstream file(patched_file, std::ios::binary);
93 if (!file.is_open()) {
94 app_context.error_message =
"Could not open file for writing.";
99 file.write(
reinterpret_cast<const char*
>(patched.data()), patched.size());
106 auto return_button = Button(
"Back to Main Menu", [&] {
111 auto container = Container::Vertical({
118 auto renderer = Renderer(container, [&] {
119 return vbox({text(
"Apply BPS Patch") | center, separator(),
120 text(
"Enter Patch File:"), patch_file_input->Render(),
121 text(
"Enter Base File:"), base_file_input->Render(),
124 apply_button->Render() | center,
126 return_button->Render() | center,
131 screen.Loop(renderer);
138 const static std::vector<std::string> items = {
"Bow",
163 constexpr size_t kNumItems = 28;
164 std::array<bool, kNumItems> values = {};
165 auto checkboxes = Container::Vertical({});
166 for (
size_t i = 0; i < items.size(); i += 4) {
167 auto row = Container::Horizontal({});
168 for (
size_t j = 0; j < 4 && (i + j) < items.size(); ++j) {
170 Checkbox(absl::StrCat(items[i + j],
" ").data(), &values[i + j]));
172 checkboxes->Add(row);
179 static int sword = 0;
180 static int shield = 0;
181 static int armor = 0;
183 const std::vector<std::string> sword_items = {
"Fighter",
"Master",
"Tempered",
185 const std::vector<std::string> shield_items = {
"Small",
"Fire",
"Mirror"};
186 const std::vector<std::string> armor_items = {
"Green",
"Blue",
"Red"};
188 auto sword_radiobox = Radiobox(&sword_items, &sword);
189 auto shield_radiobox = Radiobox(&shield_items, &shield);
190 auto armor_radiobox = Radiobox(&armor_items, &armor);
191 auto equipment_container = Container::Vertical({
197 auto save_button = Button(
"Generate Save File", [&] {
207 auto container = Container::Vertical({
214 auto renderer = Renderer(container, [&] {
215 return vbox({text(
"Generate Save File") | center, separator(),
216 text(
"Select items to include in the save file:"),
217 checkboxes->Render(), separator(),
218 equipment_container->Render(), separator(),
220 save_button->Render() | center,
222 back_button->Render() | center,
227 screen.Loop(renderer);
232 static bool initialized =
false;
238 static std::string new_todo_description;
239 static int selected_todo = 0;
241 auto refresh_todos = [&]() {
243 std::vector<std::string> entries;
244 for (
const auto& item : todos) {
245 std::string status_emoji;
246 switch (item.status) {
263 entries.push_back(absl::StrFormat(
"%s [%s] %s", status_emoji, item.id,
269 static std::vector<std::string> todo_entries = refresh_todos();
271 auto input_field = Input(&new_todo_description,
"New TODO description");
272 auto add_button = Button(
"Add", [&]() {
273 if (!new_todo_description.empty()) {
274 manager.CreateTodo(new_todo_description);
275 new_todo_description.clear();
276 todo_entries = refresh_todos();
280 auto complete_button = Button(
"Complete", [&]() {
282 if (selected_todo < todos.size()) {
283 manager.UpdateStatus(todos[selected_todo].id,
284 agent::TodoItem::Status::COMPLETED);
285 todo_entries = refresh_todos();
289 auto delete_button = Button(
"Delete", [&]() {
291 if (selected_todo < todos.size()) {
292 manager.DeleteTodo(todos[selected_todo].id);
293 if (selected_todo >= todo_entries.size() - 1) {
296 todo_entries = refresh_todos();
303 auto todo_menu = Menu(&todo_entries, &selected_todo);
305 auto container = Container::Vertical({
306 Container::Horizontal({input_field, add_button}),
308 Container::Horizontal({complete_button, delete_button, back_button}),
311 auto renderer = Renderer(container, [&] {
313 text(
"📝 TODO Manager") | bold | center,
315 hbox({text(
"New: "), input_field->Render(),
316 add_button->Render()}),
318 todo_menu->Render() | vscroll_indicator | frame | flex,
320 hbox({complete_button->Render(), delete_button->Render(),
321 back_button->Render()}) |
327 screen.Loop(renderer);
333 static std::string asm_file;
334 static std::string output_message;
335 static Color output_color = Color::White;
337 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
339 auto apply_button = Button(
"Apply Asar Patch", [&] {
340 if (asm_file.empty()) {
341 app_context.error_message =
"Please specify an assembly file";
342 SwitchComponents(screen, LayoutID::kError);
350 "❌ AsarPatch not yet implemented in new CommandHandler system";
351 output_color = Color::Red;
352 } catch (
const std::exception& e) {
353 output_message =
"Exception: " + std::string(e.what());
354 output_color = Color::Red;
358 auto back_button = Button(
"Back to Main Menu", [&] {
359 output_message.clear();
363 auto container = Container::Vertical({
369 auto renderer = Renderer(container, [&] {
370 std::vector<Element> elements = {
371 text(
"Apply Asar Patch") | center | bold,
373 text(
"Assembly File:"),
374 asm_file_input->Render(),
376 apply_button->Render() | center,
379 if (!output_message.empty()) {
380 elements.push_back(separator());
381 elements.push_back(text(output_message) | color(output_color));
384 elements.push_back(separator());
385 elements.push_back(back_button->Render() | center);
387 return vbox(elements) | center | border;
390 screen.Loop(renderer);
415 static std::string asm_file;
416 static std::vector<std::string> symbols_list;
417 static std::string output_message;
419 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
421 auto extract_button = Button(
"Extract Symbols", [&] {
422 if (asm_file.empty()) {
423 app_context.error_message =
"Please specify an assembly file";
424 SwitchComponents(screen, LayoutID::kError);
429 core::AsarWrapper wrapper;
430 auto init_status = wrapper.Initialize();
431 if (!init_status.ok()) {
432 app_context.error_message =
433 absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
434 SwitchComponents(screen, LayoutID::kError);
438 auto symbols_result = wrapper.ExtractSymbols(asm_file);
439 if (!symbols_result.ok()) {
440 app_context.error_message = absl::StrCat(
441 "Symbol extraction failed: ", symbols_result.status().message());
446 const auto& symbols = symbols_result.value();
447 output_message = absl::StrFormat(
"✅ Extracted %d symbols from %s",
448 symbols.size(), asm_file);
450 symbols_list.clear();
451 for (
const auto& symbol : symbols) {
452 symbols_list.push_back(
453 absl::StrFormat(
"%-20s @ $%06X", symbol.name, symbol.address));
456 }
catch (
const std::exception& e) {
457 app_context.error_message =
"Exception: " + std::string(e.what());
462 auto back_button = Button(
"Back to Main Menu", [&] {
463 output_message.clear();
464 symbols_list.clear();
468 auto container = Container::Vertical({
474 auto renderer = Renderer(container, [&] {
475 std::vector<Element> elements = {
476 text(
"Extract Assembly Symbols") | center | bold,
478 text(
"Assembly File:"),
479 asm_file_input->Render(),
481 extract_button->Render() | center,
484 if (!output_message.empty()) {
485 elements.push_back(separator());
486 elements.push_back(text(output_message) | color(Color::Green));
488 if (!symbols_list.empty()) {
489 elements.push_back(separator());
490 elements.push_back(text(
"Symbols:") | bold);
492 std::vector<Element> symbol_elements;
493 for (
const auto& symbol : symbols_list) {
494 symbol_elements.push_back(text(symbol) | color(Color::Cyan));
496 elements.push_back(vbox(symbol_elements) | frame |
497 size(HEIGHT, LESS_THAN, 15));
501 elements.push_back(separator());
502 elements.push_back(back_button->Render() | center);
504 return vbox(elements) | center | border;
507 screen.Loop(renderer);
511 static std::string asm_file;
512 static std::string output_message;
513 static Color output_color = Color::White;
515 auto asm_file_input = Input(&asm_file,
"Assembly file (.asm)");
517 auto validate_button = Button(
"Validate Assembly", [&] {
518 if (asm_file.empty()) {
519 app_context.error_message =
"Please specify an assembly file";
520 SwitchComponents(screen, LayoutID::kError);
525 core::AsarWrapper wrapper;
526 auto init_status = wrapper.Initialize();
527 if (!init_status.ok()) {
528 app_context.error_message =
529 absl::StrCat(
"Failed to initialize Asar: ", init_status.message());
530 SwitchComponents(screen, LayoutID::kError);
534 auto validation_status = wrapper.ValidateAssembly(asm_file);
535 if (validation_status.ok()) {
536 output_message =
"✅ Assembly file is valid!";
537 output_color = Color::Green;
540 absl::StrCat(
"❌ Validation failed:\n", validation_status.message());
541 output_color = Color::Red;
544 }
catch (
const std::exception& e) {
545 app_context.error_message =
"Exception: " + std::string(e.what());
550 auto back_button = Button(
"Back to Main Menu", [&] {
551 output_message.clear();
552 SwitchComponents(screen, LayoutID::kMainMenu);
555 auto container = Container::Vertical({
561 auto renderer = Renderer(container, [&] {
562 std::vector<Element> elements = {
563 text(
"Validate Assembly File") | center | bold,
565 text(
"Assembly File:"),
566 asm_file_input->Render(),
568 validate_button->Render() | center,
571 if (!output_message.empty()) {
572 elements.push_back(separator());
573 elements.push_back(text(output_message) | color(output_color));
576 elements.push_back(separator());
577 elements.push_back(back_button->Render() | center);
579 return vbox(elements) | center | border;
582 screen.Loop(renderer);
643 auto help_text = vbox({
645 text(
"╔══════════════════════════════════════════════════════════╗") |
646 color(Color::Cyan1) | bold,
648 " - AI-Powered CLI ║")) |
649 color(Color::Cyan1) | bold,
650 text(
"║ The Legend of Zelda: A Link to the Past Editor ║") |
652 text(
"╚══════════════════════════════════════════════════════════╝") |
653 color(Color::Cyan1) | bold,
656 text(
"✨ Author: ") | color(Color::Yellow1) | bold,
657 text(
"scawful") | color(Color::Magenta),
658 text(
" │ ") | color(Color::GrayDark),
659 text(
"🤖 AI: ") | color(Color::Green1) | bold,
660 text(
"Ollama + Gemini + OpenAI + Anthropic Integration") |
661 color(Color::GreenLight),
668 text(
"🤖 AI AGENT COMMANDS") | bold | color(Color::Green1) | center,
669 text(
" Conversational AI for ROM inspection and modification") |
670 color(Color::GreenLight) | center,
672 hbox({text(
" "), text(
"💬 Test Chat Mode") | bold | color(Color::Cyan),
673 filler(), text(
"agent test-conversation") | color(Color::White),
674 text(
" [--rom=<file>] [--verbose]") | color(Color::GrayLight)}),
676 text(
"→ Interactive AI testing with embedded labels") |
677 color(Color::GrayLight)}),
679 hbox({text(
" "), text(
"📊 Chat with AI") | bold | color(Color::Cyan),
680 filler(), text(
"agent chat") | color(Color::White),
681 text(
" <prompt> [--host] [--port]") | color(Color::GrayLight)}),
682 hbox({text(
" "), text(
"→ Natural language ROM inspection (rooms, "
683 "sprites, entrances)") |
684 color(Color::GrayLight)}),
686 hbox({text(
" "), text(
"🎯 Simple Chat") | bold | color(Color::Cyan),
687 filler(), text(
"agent simple-chat") | color(Color::White),
688 text(
" <prompt> [--rom=<file>]") | color(Color::GrayLight)}),
690 text(
"→ Quick AI queries with automatic ROM loading") |
691 color(Color::GrayLight)}),
696 text(
"🎯 ASAR 65816 ASSEMBLER") | bold | color(Color::Yellow1) | center,
697 text(
" Assemble and patch with Asar integration") |
698 color(Color::YellowLight) | center,
700 hbox({text(
" "), text(
"⚡ Apply Patch") | bold | color(Color::Cyan),
701 filler(), text(
"patch apply-asar") | color(Color::White),
702 text(
" <patch.asm> [--rom=<file>]") | color(Color::GrayLight)}),
703 hbox({text(
" "), text(
"🔍 Extract Symbols") | bold | color(Color::Cyan),
704 filler(), text(
"patch extract-symbols") | color(Color::White),
705 text(
" <patch.asm>") | color(Color::GrayLight)}),
706 hbox({text(
" "), text(
"✓ Validate Assembly") | bold | color(Color::Cyan),
707 filler(), text(
"patch validate") | color(Color::White),
708 text(
" <patch.asm>") | color(Color::GrayLight)}),
713 text(
"📦 PATCH MANAGEMENT") | bold | color(Color::Blue) | center,
715 hbox({text(
" "), text(
"Apply BPS Patch") | color(Color::Cyan), filler(),
716 text(
"patch apply-bps") | color(Color::White),
717 text(
" <patch.bps> [--rom=<file>]") | color(Color::GrayLight)}),
718 hbox({text(
" "), text(
"Create BPS Patch") | color(Color::Cyan), filler(),
719 text(
"patch create") | color(Color::White),
720 text(
" <src> <modified>") | color(Color::GrayLight)}),
725 text(
"🗃️ ROM OPERATIONS") | bold | color(Color::Magenta) | center,
727 hbox({text(
" "), text(
"Show ROM Info") | color(Color::Cyan), filler(),
728 text(
"rom info") | color(Color::White),
729 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
730 hbox({text(
" "), text(
"Validate ROM") | color(Color::Cyan), filler(),
731 text(
"rom validate") | color(Color::White),
732 text(
" [--rom=<file>]") | color(Color::GrayLight)}),
733 hbox({text(
" "), text(
"Compare ROMs") | color(Color::Cyan), filler(),
734 text(
"rom diff") | color(Color::White),
735 text(
" <rom_a> <rom_b>") | color(Color::GrayLight)}),
736 hbox({text(
" "), text(
"Backup ROM") | color(Color::Cyan), filler(),
737 text(
"rom backup") | color(Color::White),
738 text(
" <rom_file> [name]") | color(Color::GrayLight)}),
739 hbox({text(
" "), text(
"Expand ROM") | color(Color::Cyan), filler(),
740 text(
"rom expand") | color(Color::White),
741 text(
" <rom_file> <size>") | color(Color::GrayLight)}),
746 text(
"🏰 EMBEDDED RESOURCE LABELS") | bold | color(Color::Red1) | center,
747 text(
" All Zelda3 names built-in and always available to AI") |
748 color(Color::RedLight) | center,
750 hbox({text(
" 📚 296+ Room Names") | color(Color::GreenLight),
751 text(
" │ ") | color(Color::GrayDark),
752 text(
"256 Sprite Names") | color(Color::GreenLight),
753 text(
" │ ") | color(Color::GrayDark),
754 text(
"133 Entrance Names") | color(Color::GreenLight)}),
755 hbox({text(
" 🎨 100 Item Names") | color(Color::GreenLight),
756 text(
" │ ") | color(Color::GrayDark),
757 text(
"160 Overworld Maps") | color(Color::GreenLight),
758 text(
" │ ") | color(Color::GrayDark),
759 text(
"48 Music Tracks") | color(Color::GreenLight)}),
760 hbox({text(
" 🔧 60 Tile Types") | color(Color::GreenLight),
761 text(
" │ ") | color(Color::GrayDark),
762 text(
"26 Overlord Names") | color(Color::GreenLight),
763 text(
" │ ") | color(Color::GrayDark),
764 text(
"32 GFX Sheets") | color(Color::GreenLight)}),
768 text(
"🌐 GLOBAL FLAGS") | bold | color(Color::White) | center,
770 hbox({text(
" --tui") | color(Color::Cyan), filler(),
771 text(
"Launch Text User Interface") | color(Color::GrayLight)}),
772 hbox({text(
" --rom=<file>") | color(Color::Cyan), filler(),
773 text(
"Specify ROM file") | color(Color::GrayLight)}),
774 hbox({text(
" --output=<file>") | color(Color::Cyan), filler(),
775 text(
"Specify output file") | color(Color::GrayLight)}),
776 hbox({text(
" --verbose") | color(Color::Cyan), filler(),
777 text(
"Enable verbose output") | color(Color::GrayLight)}),
778 hbox({text(
" --dry-run") | color(Color::Cyan), filler(),
779 text(
"Test without changes") | color(Color::GrayLight)}),
782 text(
"Press 'q' to quit, '/' for command palette, 'h' for help") |
783 center | color(Color::GrayLight),
789 auto container = Container::Vertical({
793 auto renderer = Renderer(container, [&] {
795 help_text | vscroll_indicator | frame | flex,
797 back_button->Render() | center,
802 screen.Loop(renderer);
806 static int selected = 0;
808 option.focused_entry = &selected;
811 auto content_renderer = ftxui::Renderer([&] {
815 text(
"Welcome to the z3ed Dashboard!") | center,
816 text(
"Select a tool from the menu to begin.") | center | dim,
820 auto main_container = Container::Horizontal({menu, content_renderer});
822 auto layout = Renderer(main_container, [&] {
823 std::string rom_info =
824 app_context.rom.is_loaded() ? app_context.rom.title() :
"No ROM";
825 return vbox({hbox({menu->Render() | size(WIDTH, EQUAL, 30) | border,
826 (content_renderer->Render() | center | flex) | border}),
827 hbox({text(rom_info) | bold, filler(),
828 text(
"q: Quit | ↑/↓: Navigate | Enter: Select")}) |
832 auto event_handler = CatchEvent(layout, [&](
const Event& event) {
833 if (event == Event::Character(
'q')) {
837 if (event == Event::Return) {
840 case MainMenuEntry::kLoadRom:
843 case MainMenuEntry::kAIAgentChat:
846 case MainMenuEntry::kTodoManager:
849 case MainMenuEntry::kRomTools:
852 case MainMenuEntry::kGraphicsTools:
855 case MainMenuEntry::kTestingTools:
858 case MainMenuEntry::kSettings:
861 case MainMenuEntry::kHelp:
864 case MainMenuEntry::kExit:
873 screen.Loop(event_handler);
878 static int selected = 0;
880 option.focused_entry = &selected;
885 std::string rom_information =
"ROM not loaded";
886 if (app_context.rom.is_loaded()) {
887 rom_information = app_context.rom.title();
892 text(
" ███████╗██████╗ ███████╗██████╗ ") | color(Color::Cyan1) | bold,
893 text(
" ╚══███╔╝╚════██╗██╔════╝██╔══██╗") | color(Color::Cyan1) | bold,
894 text(
" ███╔╝ █████╔╝█████╗ ██║ ██║") | color(Color::Cyan1) | bold,
895 text(
" ███╔╝ ╚═══██╗██╔══╝ ██║ ██║") | color(Color::Cyan1) | bold,
896 text(
" ███████╗██████╔╝███████╗██████╔╝") | color(Color::Cyan1) | bold,
897 text(
" ╚══════╝╚═════╝ ╚══════╝╚═════╝ ") | color(Color::Cyan1) | bold,
900 text(
" ▲ ") | color(Color::Yellow1) | bold,
901 text(
"Zelda 3 Editor") | color(Color::White) | bold,
904 text(
" ▲ ▲ ") | color(Color::Yellow1) | bold,
905 text(
"AI-Powered CLI") | color(Color::GrayLight),
907 text(
" ▲▲▲▲▲ ") | color(Color::Yellow1) | bold | center,
910 auto title = border(hbox({
912 color(Color::Green1),
914 text(rom_information) | bold | color(Color::Red1),
917 auto renderer = Renderer(menu, [&] {
924 menu->Render() | center,
929 auto main_component = CatchEvent(renderer, [&](
const Event& event) {
930 if (event == Event::Return) {
932 case MainMenuEntry::kLoadRom:
935 case MainMenuEntry::kAIAgentChat:
938 case MainMenuEntry::kTodoManager:
941 case MainMenuEntry::kRomTools:
944 case MainMenuEntry::kGraphicsTools:
947 case MainMenuEntry::kTestingTools:
950 case MainMenuEntry::kSettings:
953 case MainMenuEntry::kHelp:
956 case MainMenuEntry::kExit:
962 if (event == Event::Character(
'q')) {
969 screen.Loop(main_component);