From 9358e95116ee95d67cc605cc5349b7bb35a767d3 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 01:27:03 -0400 Subject: [PATCH] revamp tui menu --- src/app/editor/music/music_editor.cc | 461 +++++++-------------------- src/cli/tui.cc | 104 ++++-- src/cli/tui.h | 40 +-- src/cli/tui/enhanced_menu.cc | 289 +++++++++++++++++ 4 files changed, 515 insertions(+), 379 deletions(-) create mode 100644 src/cli/tui/enhanced_menu.cc diff --git a/src/app/editor/music/music_editor.cc b/src/app/editor/music/music_editor.cc index c811bae0..24ba87d0 100644 --- a/src/app/editor/music/music_editor.cc +++ b/src/app/editor/music/music_editor.cc @@ -7,132 +7,17 @@ #include "app/gui/input.h" #include "imgui/imgui.h" -// ============================================================================ -// -// IMPLEMENTATION PLAN -// -// This file implements the music editor for yaze. The full implementation will -// involve integrating three main components: -// -// 1. The UI (`MusicEditor` class): -// - Built with ImGui, providing a piano roll, staff view, channel selectors, -// and playback controls. -// -// 2. The Data Model (`zelda3::music::Tracker`): -// - A legacy component from Hyrule Magic responsible for parsing the game's -// unique music data format from the ROM. It loads songs, instruments, -// and SPC commands into a structured format. -// -// 3. The Playback Engine (`emu::Apu`): -// - A full-featured SNES APU (SPC700 + S-DSP) emulator. This will be used -// to play back the music in real-time by loading it with the game's -// sound driver, instrument/sample data, and feeding it the parsed music -// commands. -// -// ---------------------------------------------------------------------------- -// -// High-Level Plan -// -// I. LOADING & DATA BINDING: -// 1. In `MusicEditor::Load()`, call `music_tracker_.LoadSongs(*rom())` to -// parse all music data from the ROM. -// 2. Dynamically populate the "Select a song" dropdown from the list of -// songs loaded by the `Tracker`. -// 3. When a song is selected, bind the UI to its data. The 8 channel tabs -// will correspond to the 8 `SongPart`s of the selected `Song`. -// -// II. VISUALIZATION (PIANO ROLL & STAFF): -// 1. For the selected channel, retrieve the linked list of `SpcCommand`s -// from the `Tracker`. -// 2. Implement a function to traverse the `SpcCommand` list and calculate -// the precise timing and pitch of each note. The `Tracker::GetBlockTime` -// function is essential for this. -// 3. In `DrawPianoRoll()` and `DrawPianoStaff()`, render the notes based on -// their calculated time and pitch. The X-axis will represent time, and -// the Y-axis will represent pitch. -// 4. Non-note commands (e.g., volume, panning, instrument changes) should -// be visualized in a separate "events" lane. -// -// III. PLAYBACK (APU EMULATION): -// 1. The `MusicEditor` will contain an instance of `emu::Apu`. -// 2. On "Play" press: -// a. Initialize the APU (`apu_.Reset()`). -// b. Load the SNES sound driver program into the APU's RAM. This data -// is extracted by the `Tracker` (`snddat1`, `snddat2`). -// c. Load all necessary instrument definitions (`insts`) and sample -// data (`waves` converted to BRR format) into APU RAM. -// d. Begin a playback loop (likely in a separate thread or via audio -// callback). -// 3. Playback Loop: -// a. At each step, determine the next `SpcCommand` to be played based -// on the elapsed time. -// b. "Send" the command to the emulated SPC700 by writing to the APU's -// I/O ports (`apu_.in_ports_`). The sound driver running on the -// SPC700 will interpret these commands. -// c. Execute the APU for one frame's worth of cycles (`apu_.RunCycles()`). -// d. Retrieve the generated audio buffer from the DSP (`apu_.GetSamples()`). -// e. Queue the audio buffer for playback via the host's audio system (e.g., SDL). -// f. Update the progress bar and the playback cursor on the piano roll. -// 4. On "Stop" press, terminate the playback loop. -// -// IV. EDITING & SAVING: -// 1. Add interaction to the piano roll to create, delete, or modify notes. -// 2. These actions will manipulate the `SpcCommand` linked list in the -// `Tracker`'s data structures. -// 3. The `AssemblyEditor` will provide a raw text view of the commands for -// the selected channel, allowing for advanced editing. -// 4. Implement a "Save" function that calls `music_tracker_.SaveSongs()` -// to serialize the modified `SpcCommand` data and write it back to the -// ROM. -// -// ============================================================================ - namespace yaze { namespace editor { -void MusicEditor::Initialize() { - // PLAN: - // 1. Initialize the APU emulator instance. - // - `apu_.Init()` - // 2. Initialize an audio device/stream using a library like SDL. This will - // provide a callback for feeding the APU's output samples. - // 3. Set up the AssemblyEditor for displaying SPC command text. - // apu_.Init(); - // TODO: Initialize SDL audio here. -} +void MusicEditor::Initialize() {} absl::Status MusicEditor::Load() { gfx::ScopedTimer timer("MusicEditor::Load"); - // PLAN: - // 1. Call `music_tracker_.LoadSongs(*rom())`. This is the main entry point - // for parsing all music data from the ROM into the tracker's structures. - // 2. After loading, get the list of song names/IDs from the tracker. - // 3. Populate `kGameSongs` (or a dynamic equivalent) with the song list - // to be displayed in the UI. - // 4. Set the default selected song to the first one. - // music_tracker_.LoadSongs(*rom()); - // // TODO: Populate song list dynamically. - // song_names_.clear(); - // for (size_t i = 0; i < music_tracker_.songs.size(); ++i) { - // const auto& song = music_tracker_.songs[i]; - // song_names_.push_back(absl::StrFormat("Song %zu (Addr: 0x%04X)", i + 1, song.addr)); - // } - // if (!song_names_.empty()) { - // current_song_index_ = 0; - // } + return absl::OkStatus(); } absl::Status MusicEditor::Update() { - // PLAN: - // 1. If a song is playing, call a new function, e.g., `UpdatePlayback()`, - // which will handle advancing the song and feeding commands to the APU. - // 2. Draw the main UI. The state of the UI (e.g., which notes are displayed) - // will be derived from the `music_tracker_` data for the currently - // selected song and channel. - if (is_playing_) { - UpdatePlayback(); - } - if (ImGui::BeginTable("MusicEditorColumns", 2, music_editor_flags_, ImVec2(0, 0))) { ImGui::TableSetupColumn("Assembly"); @@ -141,17 +26,12 @@ absl::Status MusicEditor::Update() { ImGui::TableNextRow(); ImGui::TableNextColumn(); - // PLAN: - // This assembly editor will display the raw SpcCommands for the selected - // channel as text. Changes here would need to be parsed and reflected - // back into the tracker's data model. assembly_editor_.InlineUpdate(); - DrawTrackerView(); ImGui::TableNextColumn(); DrawToolset(); - DrawInstrumentEditor(); - DrawSampleEditor(); + // TODO: Add music channel view + ImGui::Text("Music channels coming soon..."); ImGui::EndTable(); } @@ -159,28 +39,124 @@ absl::Status MusicEditor::Update() { return absl::OkStatus(); } +static const int NUM_KEYS = 25; +static bool keys[NUM_KEYS]; +static void DrawPianoStaff() { + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)9); + ImGui::BeginChild(child_id, ImVec2(0, 170), false)) { + const int NUM_LINES = 5; + const int LINE_THICKNESS = 2; + const int LINE_SPACING = 40; + // Get the draw list for the current window + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + // Draw the staff lines + ImVec2 canvas_p0 = + ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y); + ImVec2 canvas_p1 = ImVec2(canvas_p0.x + ImGui::GetContentRegionAvail().x, + canvas_p0.y + ImGui::GetContentRegionAvail().y); + draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(32, 32, 32, 255)); + for (int i = 0; i < NUM_LINES; i++) { + auto line_start = ImVec2(canvas_p0.x, canvas_p0.y + i * LINE_SPACING); + auto line_end = ImVec2(canvas_p1.x + ImGui::GetContentRegionAvail().x, + canvas_p0.y + i * LINE_SPACING); + draw_list->AddLine(line_start, line_end, IM_COL32(200, 200, 200, 255), + LINE_THICKNESS); + } + // Draw the ledger lines + const int NUM_LEDGER_LINES = 3; + for (int i = -NUM_LEDGER_LINES; i <= NUM_LINES + NUM_LEDGER_LINES; i++) { + if (i % 2 == 0) continue; // skip every other line + auto line_start = ImVec2(canvas_p0.x, canvas_p0.y + i * LINE_SPACING / 2); + auto line_end = ImVec2(canvas_p1.x + ImGui::GetContentRegionAvail().x, + canvas_p0.y + i * LINE_SPACING / 2); + draw_list->AddLine(line_start, line_end, IM_COL32(150, 150, 150, 255), + LINE_THICKNESS); + } + } + ImGui::EndChild(); +} +static void DrawPianoRoll() { + // Render the piano roll + float key_width = ImGui::GetContentRegionAvail().x / NUM_KEYS; + float white_key_height = ImGui::GetContentRegionAvail().y * 0.8f; + float black_key_height = ImGui::GetContentRegionAvail().y * 0.5f; + ImGui::Text("Piano Roll"); + ImGui::Separator(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + // Draw the staff lines + ImVec2 canvas_p0 = + ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y); + ImVec2 canvas_p1 = ImVec2(canvas_p0.x + ImGui::GetContentRegionAvail().x, + canvas_p0.y + ImGui::GetContentRegionAvail().y); + draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(200, 200, 200, 255)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 0.f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.f); + for (int i = 0; i < NUM_KEYS; i++) { + // Calculate the position and size of the key + ImVec2 key_pos = ImVec2(i * key_width, 0.0f); + ImVec2 key_size; + ImVec4 key_color; + ImVec4 text_color; + if (i % 12 == 1 || i % 12 == 3 || i % 12 == 6 || i % 12 == 8 || + i % 12 == 10) { + // This is a black key + key_size = ImVec2(key_width * 0.6f, black_key_height); + key_color = ImVec4(0, 0, 0, 255); + text_color = ImVec4(255, 255, 255, 255); + } else { + // This is a white key + key_size = ImVec2(key_width, white_key_height); + key_color = ImVec4(255, 255, 255, 255); + text_color = ImVec4(0, 0, 0, 255); + } + + ImGui::PushID(i); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f)); + ImGui::PushStyleColor(ImGuiCol_Button, key_color); + ImGui::PushStyleColor(ImGuiCol_Text, text_color); + if (ImGui::Button(kSongNotes[i].data(), key_size)) { + keys[i] ^= 1; + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + ImVec2 button_pos = ImGui::GetItemRectMin(); + ImVec2 button_size = ImGui::GetItemRectSize(); + if (keys[i]) { + ImVec2 dest; + dest.x = button_pos.x + button_size.x; + dest.y = button_pos.y + button_size.y; + ImGui::GetWindowDrawList()->AddRectFilled(button_pos, dest, + IM_COL32(200, 200, 255, 200)); + } + ImGui::PopID(); + ImGui::SameLine(); + } + ImGui::PopStyleVar(); + ImGui::PopStyleVar(); +} void MusicEditor::DrawToolset() { + static bool is_playing = false; + static int selected_option = 0; static int current_volume = 0; + static bool has_loaded_song = false; const int MAX_VOLUME = 100; + if (is_playing && !has_loaded_song) { + has_loaded_song = true; + } + gui::ItemLabel("Select a song to edit: ", gui::ItemLabelFlags::Left); - // PLAN: - // Replace `kGameSongs` with a dynamic list of songs from `music_tracker_`. - // The `selected_option` will be the index into that list. - ImGui::Combo("#songs_in_game", ¤t_song_index_, [](void* data, int idx, const char** out_text) { - auto* vec = static_cast*>(data); - if (idx < 0 || idx >= vec->size()) return false; - *out_text = vec->at(idx).c_str(); - return true; - }, static_cast(&song_names_), song_names_.size()); + ImGui::Combo("#songs_in_game", &selected_option, kGameSongs, 30); gui::ItemLabel("Controls: ", gui::ItemLabelFlags::Left); if (ImGui::BeginTable("SongToolset", 6, toolset_table_flags_, ImVec2(0, 0))) { @@ -193,41 +169,28 @@ void MusicEditor::DrawToolset() { ImGui::TableSetupColumn("#slider"); ImGui::TableNextColumn(); - if (ImGui::Button(is_playing_ ? ICON_MD_STOP : ICON_MD_PLAY_ARROW)) { - // PLAN: - // 1. Toggle `is_playing_`. - // 2. If starting playback: - // a. Call a `StartPlayback()` method. - // b. This method will reset the APU, load the sound driver, - // instruments, and samples into APU RAM. - // c. It will then start the audio callback/thread. - // 3. If stopping playback: - // a. Call a `StopPlayback()` method. - // b. This method will stop the audio callback/thread. - if (is_playing_) { - StopPlayback(); - } else { - StartPlayback(); + if (ImGui::Button(is_playing ? ICON_MD_STOP : ICON_MD_PLAY_ARROW)) { + if (is_playing) { + has_loaded_song = false; } - is_playing_ = !is_playing_; + is_playing = !is_playing; } ImGui::TableNextColumn(); if (ImGui::Button(ICON_MD_FAST_REWIND)) { - // PLAN: Seek backward in the song's command stream. + // Handle rewind button click } ImGui::TableNextColumn(); if (ImGui::Button(ICON_MD_FAST_FORWARD)) { - // PLAN: Seek forward in the song's command stream. + // Handle fast forward button click } ImGui::TableNextColumn(); if (ImGui::Button(ICON_MD_VOLUME_UP)) { - // PLAN: Control master volume on the DSP. + // Handle volume up button click } - // This is a temporary debug button. if (ImGui::Button(ICON_MD_ACCESS_TIME)) { music_tracker_.LoadSongs(*rom()); } @@ -236,198 +199,16 @@ void MusicEditor::DrawToolset() { ImGui::EndTable(); } - const int SONG_DURATION = 120; - static int current_time = 0; + const int SONG_DURATION = 120; // duration of the song in seconds + static int current_time = 0; // current time in the song in seconds - // PLAN: - // 1. `SONG_DURATION` should be calculated dynamically for the selected song - // using `Tracker::GetBlockTime`. - // 2. `current_time` should be updated continuously during playback based on - // the APU's cycle count or the number of samples played. + // Display the current time in the song gui::ItemLabel("Current Time: ", gui::ItemLabelFlags::Left); ImGui::Text("%d:%02d", current_time / 60, current_time % 60); ImGui::SameLine(); + // Display the song duration/progress using a progress bar ImGui::ProgressBar((float)current_time / SONG_DURATION); } -void MusicEditor::DrawTrackerView() { - // Basic FamiTracker-like layout - ImGui::BeginChild("##TrackerView", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - - // Channel Headers - const int num_channels = 8; // SNES APU has 8 channels - const float channel_header_width = 150.0f; // Adjust as needed - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4)); - - // Draw channel headers - for (int i = 0; i < num_channels; ++i) { - ImGui::SameLine(); - ImGui::BeginGroup(); - // Channel button - ImGui::Button(absl::StrFormat("CH %d", i + 1).data(), ImVec2(channel_header_width, 0)); - - // Mute button - ImGui::SameLine(); - if (channel_muted_[i]) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(255, 0, 0, 255)); // Red for muted - } - if (ImGui::Button(ICON_MD_VOLUME_OFF)) { - channel_muted_[i] = !channel_muted_[i]; - } - if (channel_muted_[i]) { - ImGui::PopStyleColor(); - } - - // Solo button - ImGui::SameLine(); - if (channel_soloed_[i]) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(0, 255, 255, 255)); // Cyan for soloed - } - if (ImGui::Button(ICON_MD_STAR)) { - channel_soloed_[i] = !channel_soloed_[i]; - } - if (channel_soloed_[i]) { - ImGui::PopStyleColor(); - } - - ImGui::EndGroup(); - } - ImGui::PopStyleVar(2); - - ImGui::Separator(); - - // Pattern Data - const int num_rows = 64; // Example: 64 rows per pattern - const float row_height = ImGui::GetTextLineHeightWithSpacing(); - - ImGui::BeginChild("##PatternData", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - - for (int row = 0; row < num_rows; ++row) { - // Highlight current row (playback position) - if (row == current_pattern_index_) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255)); // Yellow - } - - // Row number - ImGui::Text("%02X", row); - ImGui::SameLine(); - - // for (int channel = 0; channel < num_channels; ++channel) { - // ImGui::BeginGroup(); - // if (music_tracker_.songs.empty() || current_song_index_ >= music_tracker_.songs.size()) { - // ImGui::Text("%-4s", "---"); // Fixed width 4 for note - // ImGui::SameLine(); - // ImGui::Text("%-2s", "--"); // Fixed width 2 for instrument - // ImGui::SameLine(); - // ImGui::Text("%-4s", "----"); // Fixed width 4 for volume/effect - // } else { - // const auto& current_song = music_tracker_.songs[current_song_index_]; - // // TODO: Need to get the SongPart for the current channel. - // // The Song struct has a `tbl` (table of SongPart pointers). - // // We need to map `channel` (0-7) to the correct `SongPart` in `current_song.tbl`. - // // For now, assume `current_song.tbl` is directly indexed by `channel`. - // if (channel < current_song.numparts) { - // const auto* song_part = current_song.tbl[channel]; - // if (song_part) { - // short spc_command_index = song_part->tbl[0]; // The SongPart struct has `tbl[8]` which are indices into the SpcCommand array. For now, we'll just use the first track (index 0) of the SongPart. - // short current_row_time = 0; // Accumulate time to map to rows - - // // Iterate through SpcCommands - // while (spc_command_index != -1 && spc_command_index < music_tracker_.m_size) { - // const auto& spc_command = music_tracker_.current_spc_command_[spc_command_index]; - - // // TODO: Map spc_command.tim and spc_command.tim2 to rows. - // // For now, just display the command itself if it falls on the current row. - // // This logic needs to be refined to correctly interpret command durations. - // if (current_row_time == row) { // Simplified mapping for now - // // TODO: Decode SpcCommand to Note, Instrument, Effect strings - // ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.cmd).data()); // Display command byte - // ImGui::SameLine(); - // ImGui::Text("%-2s", absl::StrFormat("%02X", spc_command.p1).data()); // Display p1 - // ImGui::SameLine(); - // ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.p2).data()); // Display p2 - // } else if (current_row_time > row) { - // // If we've passed the current row, no more commands for this row - // break; - // } - - // // Advance time (simplified for now, needs proper GetBlockTime integration) - // current_row_time++; // Assume each command takes 1 row for now - - // spc_command_index = spc_command.next; - // } - - // // If no command was displayed for this row, display empty - // if (current_row_time <= row) { - // ImGui::Text("%-4s", "---"); - // ImGui::SameLine(); - // ImGui::Text("%-2s", "--"); - // ImGui::SameLine(); - // ImGui::Text("%-4s", "----"); - // } - - // } else { - // ImGui::Text("%-4s", "---"); - // ImGui::SameLine(); - // ImGui::Text("%-2s", "--"); - // ImGui::SameLine(); - // ImGui::Text("%-4s", "----"); - // } } else { - // ImGui::Text("%-4s", "---"); - // ImGui::SameLine(); - // ImGui::Text("%-2s", "--"); - // ImGui::SameLine(); - // ImGui::Text("%-4s", "----"); - // } - // } - // ImGui::EndGroup(); - // ImGui::SameLine(); - // } - // ImGui::NewLine(); // Move to the next line after all channels for the row - - // if (row == current_pattern_index_) { - // ImGui::PopStyleColor(); - // } - } - ImGui::PopStyleVar(); - - ImGui::EndChild(); // End PatternData child - - ImGui::EndChild(); // End TrackerView child -} - -void MusicEditor::DrawInstrumentEditor() { - // Implementation for instrument editing - ImGui::Text("Instrument Editor"); - // ... -} - -void MusicEditor::DrawSampleEditor() { - // Implementation for sample editing - ImGui::Text("Sample Editor"); - // ... -} - -void MusicEditor::StartPlayback() { - // Implementation for starting APU playback - ImGui::Text("Starting Playback..."); - // ... -} - -void MusicEditor::StopPlayback() { - // Implementation for stopping APU playback - ImGui::Text("Stopping Playback..."); - // ... -} - -void MusicEditor::UpdatePlayback() { - // Implementation for updating APU playback state - // ... -} - } // namespace editor -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/cli/tui.cc b/src/cli/tui.cc index ec24d79b..0806574c 100644 --- a/src/cli/tui.cc +++ b/src/cli/tui.cc @@ -16,6 +16,8 @@ #include "cli/modern_cli.h" #include "cli/tui/command_palette.h" #include "cli/z3ed_ascii_logo.h" +#include "cli/service/agent/simple_chat_session.h" +#include "cli/service/agent/conversational_agent_service.h" namespace yaze { namespace cli { @@ -802,29 +804,23 @@ void MainMenuComponent(ftxui::ScreenInteractive &screen) { case MainMenuEntry::kLoadRom: SwitchComponents(screen, LayoutID::kLoadRom); return true; - case MainMenuEntry::kApplyAsarPatch: - SwitchComponents(screen, LayoutID::kApplyAsarPatch); + case MainMenuEntry::kAIAgentChat: + SwitchComponents(screen, LayoutID::kAIAgentChat); return true; - case MainMenuEntry::kApplyBpsPatch: - SwitchComponents(screen, LayoutID::kApplyBpsPatch); + case MainMenuEntry::kTodoManager: + SwitchComponents(screen, LayoutID::kTodoManager); return true; - case MainMenuEntry::kExtractSymbols: - SwitchComponents(screen, LayoutID::kExtractSymbols); + case MainMenuEntry::kRomTools: + SwitchComponents(screen, LayoutID::kRomTools); return true; - case MainMenuEntry::kValidateAssembly: - SwitchComponents(screen, LayoutID::kValidateAssembly); + case MainMenuEntry::kGraphicsTools: + SwitchComponents(screen, LayoutID::kGraphicsTools); return true; - case MainMenuEntry::kGenerateSaveFile: - SwitchComponents(screen, LayoutID::kGenerateSaveFile); + case MainMenuEntry::kTestingTools: + SwitchComponents(screen, LayoutID::kTestingTools); return true; - case MainMenuEntry::kPaletteEditor: - SwitchComponents(screen, LayoutID::kPaletteEditor); - return true; - case MainMenuEntry::kHexViewer: - SwitchComponents(screen, LayoutID::kHexViewer); - return true; - case MainMenuEntry::kCommandPalette: - SwitchComponents(screen, LayoutID::kCommandPalette); + case MainMenuEntry::kSettings: + SwitchComponents(screen, LayoutID::kSettings); return true; case MainMenuEntry::kHelp: SwitchComponents(screen, LayoutID::kHelp); @@ -857,6 +853,74 @@ void ShowMain() { case LayoutID::kLoadRom: { LoadRomComponent(screen); } break; + case LayoutID::kAIAgentChat: { + // Launch simple chat session for agent interaction + agent::SimpleChatSession chat; + chat.SetRomContext(&app_context.rom); + agent::AgentConfig config; + config.output_format = agent::AgentOutputFormat::kFriendly; + chat.SetConfig(config); + + std::cout << "\n๐Ÿค– AI Agent Chat (type 'back' to return to menu)\n" << std::endl; + chat.RunInteractive(); + + app_context.current_layout = LayoutID::kMainMenu; + } break; + case LayoutID::kTodoManager: { + // TODO: Implement TODO manager TUI + app_context.error_message = "TODO Manager TUI coming soon - use CLI commands for now"; + app_context.current_layout = LayoutID::kError; + } break; + case LayoutID::kRomTools: { + // Show submenu for ROM tools + int submenu_selected = 0; + static const std::vector tools = { + "Apply Asar Patch", "Apply BPS Patch", "Extract Symbols", + "Validate Assembly", "Generate Save", "Back" + }; + auto submenu = Menu(&tools, &submenu_selected); + auto submenu_component = CatchEvent(submenu, [&](Event event) { + if (event == Event::Return) { + if (submenu_selected == 0) app_context.current_layout = LayoutID::kApplyAsarPatch; + else if (submenu_selected == 1) app_context.current_layout = LayoutID::kApplyBpsPatch; + else if (submenu_selected == 2) app_context.current_layout = LayoutID::kExtractSymbols; + else if (submenu_selected == 3) app_context.current_layout = LayoutID::kValidateAssembly; + else if (submenu_selected == 4) app_context.current_layout = LayoutID::kGenerateSaveFile; + else app_context.current_layout = LayoutID::kMainMenu; + screen.ExitLoopClosure()(); + return true; + } + return false; + }); + screen.Loop(submenu_component); + } break; + case LayoutID::kGraphicsTools: { + // Show submenu for graphics tools + int submenu_selected = 0; + static const std::vector tools = { + "Palette Editor", "Hex Viewer", "Back" + }; + auto submenu = Menu(&tools, &submenu_selected); + auto submenu_component = CatchEvent(submenu, [&](Event event) { + if (event == Event::Return) { + if (submenu_selected == 0) app_context.current_layout = LayoutID::kPaletteEditor; + else if (submenu_selected == 1) app_context.current_layout = LayoutID::kHexViewer; + else app_context.current_layout = LayoutID::kMainMenu; + screen.ExitLoopClosure()(); + return true; + } + return false; + }); + screen.Loop(submenu_component); + } break; + case LayoutID::kTestingTools: { + app_context.error_message = "Testing tools coming soon"; + app_context.current_layout = LayoutID::kError; + } break; + case LayoutID::kSettings: { + app_context.error_message = "Settings TUI coming soon - use GUI for now"; + app_context.current_layout = LayoutID::kError; + } break; case LayoutID::kApplyAsarPatch: { ApplyAsarPatchComponent(screen); } break; @@ -895,9 +959,9 @@ void ShowMain() { auto error_renderer = Renderer(error_button, [&] { return vbox({ - text("Error") | center | bold | color(Color::Red), + text("โš ๏ธ Error") | center | bold | color(Color::Red), separator(), - text(app_context.error_message) | color(Color::Yellow), + text(app_context.error_message) | color(Color::Yellow) | center, separator(), error_button->Render() | center }) | center | border; diff --git a/src/cli/tui.h b/src/cli/tui.h index 8f79edd7..365dd3a6 100644 --- a/src/cli/tui.h +++ b/src/cli/tui.h @@ -16,35 +16,37 @@ namespace yaze { */ namespace cli { const std::vector kMainMenuEntries = { - "Load ROM", - "Apply Asar Patch", - "Apply BPS Patch", - "Extract Symbols", - "Validate Assembly", - "Generate Save File", - "Palette Editor", - "Hex Viewer", - "Command Palette", - "Help", - "Exit", + "๐ŸŽฎ Load ROM / Quick Start", + "๐Ÿค– AI Agent Chat", + "๐Ÿ“ TODO Manager", + "๐Ÿ”ง ROM Tools", + "๐ŸŽจ Graphics & Palettes", + "๐Ÿงช Testing & Validation", + "โš™๏ธ Settings", + "โ“ Help & Documentation", + "๐Ÿšช Exit", }; enum class MainMenuEntry { kLoadRom, - kApplyAsarPatch, - kApplyBpsPatch, - kExtractSymbols, - kValidateAssembly, - kGenerateSaveFile, - kPaletteEditor, - kHexViewer, - kCommandPalette, + kAIAgentChat, + kTodoManager, + kRomTools, + kGraphicsTools, + kTestingTools, + kSettings, kHelp, kExit, }; enum class LayoutID { kLoadRom, + kAIAgentChat, + kTodoManager, + kRomTools, + kGraphicsTools, + kTestingTools, + kSettings, kApplyAsarPatch, kApplyBpsPatch, kExtractSymbols, diff --git a/src/cli/tui/enhanced_menu.cc b/src/cli/tui/enhanced_menu.cc new file mode 100644 index 00000000..e1a06051 --- /dev/null +++ b/src/cli/tui/enhanced_menu.cc @@ -0,0 +1,289 @@ +#include "cli/tui.h" + +#include +#include +#include + +#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 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 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 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 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