revamp tui menu

This commit is contained in:
scawful
2025-10-05 01:27:03 -04:00
parent fb7f443fe3
commit 9358e95116
4 changed files with 515 additions and 379 deletions

View File

@@ -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", &current_song_index_, [](void* data, int idx, const char** out_text) {
auto* vec = static_cast<std::vector<std::string>*>(data);
if (idx < 0 || idx >= vec->size()) return false;
*out_text = vec->at(idx).c_str();
return true;
}, static_cast<void*>(&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
} // namespace yaze

View File

@@ -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<std::string> 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<std::string> 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;

View File

@@ -16,35 +16,37 @@ namespace yaze {
*/
namespace cli {
const std::vector<std::string> 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,

View File

@@ -0,0 +1,289 @@
#include "cli/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