9#include <unordered_set>
12#include "absl/strings/ascii.h"
13#include "absl/strings/match.h"
14#include "absl/strings/str_format.h"
25#include "imgui/imgui.h"
26#include "imgui/misc/cpp/imgui_stdlib.h"
61 std::string trimmed = token;
62 if (absl::StartsWithIgnoreCase(trimmed,
"0x")) {
63 trimmed = trimmed.substr(2);
65 if (trimmed.empty()) {
69 unsigned long value = std::strtoul(trimmed.c_str(), &end, 16);
70 if (end ==
nullptr || *end !=
'\0') {
73 if (value > 0xFFFFu) {
76 *out =
static_cast<uint16_t
>(value);
80bool ParseHexList(
const std::string& input, std::vector<uint16_t>* out,
93 std::string normalized = input;
94 for (
char& c : normalized) {
95 if (c ==
',' || c ==
';') {
100 std::stringstream ss(normalized);
102 std::unordered_set<uint16_t> seen;
103 while (ss >> token) {
104 auto dash = token.find(
'-');
105 if (dash != std::string::npos) {
106 std::string left = token.substr(0, dash);
107 std::string right = token.substr(dash + 1);
112 *error = absl::StrFormat(
"Invalid range: %s", token);
118 *error = absl::StrFormat(
"Range end before start: %s", token);
122 for (uint16_t value = start; value <= end; ++value) {
123 if (seen.insert(value).second) {
124 out->push_back(value);
126 if (value == 0xFFFF) {
134 *error = absl::StrFormat(
"Invalid hex value: %s", token);
138 if (seen.insert(value).second) {
139 out->push_back(value);
146std::string FormatHexList(
const std::vector<uint16_t>& values) {
148 result.reserve(values.size() * 6);
149 for (
size_t i = 0; i < values.size(); ++i) {
150 const uint16_t value = values[i];
151 std::string token = value <= 0xFF ? absl::StrFormat(
"0x%02X", value)
152 : absl::StrFormat(
"0x%04X", value);
153 if (!result.empty()) {
156 result.append(token);
162 std::vector<uint16_t> values;
163 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
164 values.push_back(tile);
170 return {0xB7, 0xB8, 0xB9, 0xBA};
174 return {0xD0, 0xD1, 0xD2, 0xD3};
185bool IsLocalEndpoint(
const std::string& base_url) {
186 if (base_url.empty()) {
189 std::string lower = absl::AsciiStrToLower(base_url);
190 return absl::StrContains(lower,
"localhost") ||
191 absl::StrContains(lower,
"127.0.0.1") ||
192 absl::StrContains(lower,
"::1") ||
193 absl::StrContains(lower,
"0.0.0.0") ||
194 absl::StrContains(lower,
"192.168.") || absl::StartsWith(lower,
"10.");
197bool IsTailscaleEndpoint(
const std::string& base_url) {
198 if (base_url.empty()) {
201 std::string lower = absl::AsciiStrToLower(base_url);
202 return absl::StrContains(lower,
".ts.net") ||
203 absl::StrContains(lower,
"100.64.");
207 std::vector<std::string> tags;
208 if (IsLocalEndpoint(host.
base_url)) {
209 tags.push_back(
"local");
211 if (IsTailscaleEndpoint(host.
base_url)) {
212 tags.push_back(
"tailscale");
214 if (absl::StartsWith(absl::AsciiStrToLower(host.
base_url),
"https://")) {
215 tags.push_back(
"https");
216 }
else if (absl::StartsWith(absl::AsciiStrToLower(host.
base_url),
219 !IsTailscaleEndpoint(host.
base_url)) {
220 tags.push_back(
"http");
223 tags.push_back(
"vision");
226 tags.push_back(
"tools");
229 tags.push_back(
"stream");
234 std::string result =
"[";
235 for (
size_t i = 0; i < tags.size(); ++i) {
237 if (i + 1 < tags.size()) {
245bool AddUniquePath(std::vector<std::string>* paths,
const std::string& path) {
246 if (!paths || path.empty()) {
249 auto it = std::find(paths->begin(), paths->end(), path);
250 if (it != paths->end()) {
253 paths->push_back(path);
261 ImGui::TextDisabled(
"Settings not available");
268 ImGuiTreeNodeFlags_DefaultOpen)) {
276 if (ImGui::CollapsingHeader(
ICON_MD_FOLDER " Project Configuration")) {
297 if (ImGui::CollapsingHeader(
ICON_MD_TUNE " Editor Behavior")) {
336 ImGui::TextDisabled(
"Feature Flags configuration");
344 if (ImGui::TreeNode(
ICON_MD_MAP " Overworld Flags")) {
367 ImGui::TextDisabled(
"No active project.");
381 const char* roles[] = {
"base",
"dev",
"patched",
"release"};
383 if (ImGui::Combo(
"Role", &role_index, roles, IM_ARRAYSIZE(roles))) {
388 const char* policies[] = {
"allow",
"warn",
"block"};
390 if (ImGui::Combo(
"Write Policy", &policy_index, policies,
391 IM_ARRAYSIZE(policies))) {
398 if (ImGui::InputText(
"Expected Hash", &expected_hash)) {
403 static std::string cached_rom_hash;
404 static std::string cached_rom_path;
410 ImGui::Text(
"Current ROM Hash: %s", cached_rom_hash.empty()
412 : cached_rom_hash.c_str());
413 if (ImGui::Button(
"Use Current ROM Hash")) {
418 ImGui::TextDisabled(
"Current ROM Hash: (no ROM loaded)");
427 if (ImGui::InputText(
"Output Folder", &output_folder)) {
434 if (ImGui::InputText(
"Git Repository", &git_repo)) {
445 if (ImGui::InputText(
"Build Target (ROM)", &build_target)) {
452 if (ImGui::InputText(
"Symbols File", &symbols_file)) {
461 "Optional: load a hack manifest JSON (generated by an ASM project) to "
462 "annotate room tags, show feature flags, and surface which ROM regions "
463 "are owned by ASM vs safe to edit in yaze.");
466 if (ImGui::InputText(
"Hack Manifest File", &manifest_file)) {
474 ImGui::TextDisabled(manifest_loaded ?
"(loaded)" :
"(not loaded)");
475 if (ImGui::Button(
"Reload Manifest")) {
479 if (manifest_loaded) {
482 ImGui::Text(
"Manifest Version: %d",
487 if (!pipeline.dev_rom.empty()) {
488 ImGui::Text(
"Dev ROM: %s", pipeline.dev_rom.c_str());
490 if (!pipeline.patched_rom.empty()) {
491 ImGui::Text(
"Patched ROM: %s", pipeline.patched_rom.c_str());
493 if (!pipeline.build_script.empty()) {
494 ImGui::Text(
"Build Script: %s", pipeline.build_script.c_str());
498 if (msg_layout.first_expanded_id != 0 || msg_layout.last_expanded_id != 0) {
499 ImGui::Text(
"Expanded Messages: 0x%03X-0x%03X (%d)",
500 msg_layout.first_expanded_id, msg_layout.last_expanded_id,
501 msg_layout.expanded_count);
504 if (ImGui::TreeNode(
ICON_MD_FLAG " Hack Feature Flags")) {
506 ImGui::BulletText(
"%s = %d (%s)", flag.name.c_str(), flag.value,
507 flag.enabled ?
"enabled" :
"disabled");
508 if (!flag.source.empty()) {
510 ImGui::TextDisabled(
"%s", flag.source.c_str());
516 if (ImGui::TreeNode(
ICON_MD_LABEL " Room Tags (Dispatch)")) {
518 ImGui::BulletText(
"0x%02X: %s", tag.tag_id, tag.name.c_str());
519 if (!tag.enabled && !tag.feature_flag.empty()) {
521 ImGui::TextDisabled(
"(disabled by %s)", tag.feature_flag.c_str());
523 if (!tag.purpose.empty() && ImGui::IsItemHovered()) {
524 ImGui::SetTooltip(
"%s", tag.purpose.c_str());
536 if (ImGui::InputText(
"Backup Folder", &backup_folder)) {
542 if (ImGui::Checkbox(
"Backup Before Save", &backup_on_save)) {
548 if (ImGui::InputInt(
"Retention Count", &retention)) {
550 std::max(0, retention);
555 if (ImGui::Checkbox(
"Keep Daily Snapshots", &keep_daily)) {
561 if (ImGui::InputInt(
"Keep Daily Days", &keep_days)) {
563 std::max(1, keep_days);
571 "Configure collision/object IDs used by minecart overlays and audits. "
572 "Hex values, ranges allowed (e.g. B0-BE).");
574 static std::string overlay_project_path;
575 static HexListEditorState track_tiles_state;
576 static HexListEditorState stop_tiles_state;
577 static HexListEditorState switch_tiles_state;
578 static HexListEditorState track_object_state;
579 static HexListEditorState minecart_sprite_state;
583 track_tiles_state.text =
585 stop_tiles_state.text =
587 switch_tiles_state.text =
589 track_object_state.text =
591 minecart_sprite_state.text =
593 track_tiles_state.error.clear();
594 stop_tiles_state.error.clear();
595 switch_tiles_state.error.clear();
596 track_object_state.error.clear();
597 minecart_sprite_state.error.clear();
600 auto draw_hex_list = [&](
const char* label,
const char* hint,
601 HexListEditorState& state,
602 const std::vector<uint16_t>& defaults,
603 std::vector<uint16_t>* target) {
609 ImGui::PushItemWidth(-180.0f);
610 if (ImGui::InputTextWithHint(label, hint, &state.text)) {
613 ImGui::PopItemWidth();
614 if (ImGui::IsItemDeactivatedAfterEdit()) {
619 if (ImGui::SmallButton(absl::StrFormat(
"Apply##%s", label).c_str())) {
623 if (ImGui::SmallButton(absl::StrFormat(
"Defaults##%s", label).c_str())) {
624 state.text = FormatHexList(defaults);
627 if (ImGui::IsItemHovered()) {
628 ImGui::SetTooltip(
"Reset to defaults");
631 if (ImGui::SmallButton(absl::StrFormat(
"Clear##%s", label).c_str())) {
635 if (ImGui::IsItemHovered()) {
636 ImGui::SetTooltip(
"Clear list (empty uses defaults)");
639 const bool uses_defaults = target->empty();
640 const std::vector<uint16_t>& effective_values =
641 uses_defaults ? defaults : *target;
644 if (ImGui::IsItemHovered()) {
645 ImGui::BeginTooltip();
646 ImGui::Text(
"Effective: %s", FormatHexList(effective_values).c_str());
648 ImGui::TextDisabled(
"Using defaults (list is empty)");
654 std::vector<uint16_t> parsed;
656 if (ParseHexList(state.text, &parsed, &error)) {
660 state.text = FormatHexList(parsed);
666 if (!state.error.empty()) {
667 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f),
"%s",
668 state.error.c_str());
672 draw_hex_list(
"Track Tiles",
"0xB0-0xBE", track_tiles_state,
674 draw_hex_list(
"Stop Tiles",
"0xB7, 0xB8, 0xB9, 0xBA", stop_tiles_state,
677 draw_hex_list(
"Switch Tiles",
"0xD0-0xD3", switch_tiles_state,
678 DefaultSwitchTiles(),
680 draw_hex_list(
"Track Object IDs",
"0x31", track_object_state,
681 DefaultTrackObjectIds(),
683 draw_hex_list(
"Minecart Sprite IDs",
"0xA3", minecart_sprite_state,
684 DefaultMinecartSpriteIds(),
695 static int selected_root_index = -1;
696 static std::string new_root_path;
702 ImGui::TextDisabled(
"No project roots configured.");
705 if (ImGui::BeginChild(
"ProjectRootsList", ImVec2(0, 140),
true)) {
706 for (
size_t i = 0; i < roots.size(); ++i) {
707 const bool is_default = roots[i] == prefs.default_project_root;
711 label +=
" (default)";
713 if (ImGui::Selectable(label.c_str(),
714 selected_root_index ==
static_cast<int>(i))) {
715 selected_root_index =
static_cast<int>(i);
721 const bool has_selection =
722 selected_root_index >= 0 &&
723 selected_root_index < static_cast<int>(roots.size());
725 if (ImGui::Button(
"Set Default")) {
726 prefs.default_project_root = roots[selected_root_index];
731 const std::string removed = roots[selected_root_index];
732 roots.erase(roots.begin() + selected_root_index);
733 if (prefs.default_project_root == removed) {
734 prefs.default_project_root = roots.empty() ?
"" : roots.front();
736 selected_root_index = roots.empty()
738 : std::min(selected_root_index,
739 static_cast<int>(roots.size() - 1));
748 ImGui::InputTextWithHint(
"##project_root_add",
"Add folder path...",
751 const std::string trimmed =
752 std::string(absl::StripAsciiWhitespace(new_root_path));
753 if (!trimmed.empty()) {
754 if (AddUniquePath(&roots, trimmed) &&
755 prefs.default_project_root.empty()) {
756 prefs.default_project_root = trimmed;
764 if (!folder.empty()) {
765 if (AddUniquePath(&roots, folder) && prefs.default_project_root.empty()) {
766 prefs.default_project_root = folder;
779 if (AddUniquePath(&roots, docs_dir->string()) &&
780 prefs.default_project_root.empty()) {
781 prefs.default_project_root = docs_dir->string();
790 if (icloud_dir.ok()) {
791 if (AddUniquePath(&roots, icloud_dir->string())) {
792 prefs.default_project_root = icloud_dir->string();
798 "iCloud projects live in Documents/Yaze/iCloud on this Mac.");
804 bool use_icloud_sync = prefs.use_icloud_sync;
805 if (ImGui::Checkbox(
"Use iCloud sync (Documents)", &use_icloud_sync)) {
806 prefs.use_icloud_sync = use_icloud_sync;
807 if (use_icloud_sync) {
810 if (icloud_dir.ok()) {
811 AddUniquePath(&roots, icloud_dir->string());
812 prefs.default_project_root = icloud_dir->string();
818 bool use_files_app = prefs.use_files_app;
819 if (ImGui::Checkbox(
"Prefer Files app on iOS", &use_files_app)) {
820 prefs.use_files_app = use_files_app;
832 const auto& current = theme_manager.GetCurrentThemeName();
833 const auto& current_theme = theme_manager.GetCurrentTheme();
835 ImGui::Text(
"Current Theme:");
840 ImDrawList* draw_list = ImGui::GetWindowDrawList();
841 ImVec2 cursor = ImGui::GetCursorScreenPos();
842 const float swatch_size = 12.0f;
843 const float spacing = 2.0f;
845 auto draw_swatch = [&](
const gui::Color& color,
float offset_x) {
846 ImVec2 p_min(cursor.x + offset_x, cursor.y);
847 ImVec2 p_max(p_min.x + swatch_size, p_min.y + swatch_size);
850 draw_list->AddRectFilled(p_min, p_max, col);
853 ImGui::ColorConvertFloat4ToU32(ImVec4(0.5f, 0.5f, 0.5f, 0.6f)));
856 draw_swatch(current_theme.primary, 0.0f);
857 draw_swatch(current_theme.surface, swatch_size + spacing);
858 draw_swatch(current_theme.accent, 2.0f * (swatch_size + spacing));
862 ImVec2(3.0f * swatch_size + 2.0f * spacing + 4.0f, swatch_size));
866 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f),
"%s", current.c_str());
871 ImGui::Text(
"Available Themes:");
873 bool any_theme_hovered =
false;
874 if (ImGui::BeginChild(
"ThemeList", ImVec2(0, 200),
true)) {
875 for (
const auto& theme_name : theme_manager.GetAvailableThemes()) {
876 ImGui::PushID(theme_name.c_str());
877 bool is_current = (theme_name == current);
880 const gui::Theme* theme_data = theme_manager.GetTheme(theme_name);
882 ImDrawList* draw_list = ImGui::GetWindowDrawList();
883 ImVec2 cursor = ImGui::GetCursorScreenPos();
884 const float swatch_size = 10.0f;
885 const float swatch_spacing = 2.0f;
886 const float total_swatch_width =
887 3.0f * swatch_size + 2.0f * swatch_spacing + 6.0f;
889 auto draw_small_swatch = [&](
const gui::Color& color,
float offset_x) {
890 ImVec2 p_min(cursor.x + offset_x, cursor.y + 2.0f);
891 ImVec2 p_max(p_min.x + swatch_size, p_min.y + swatch_size);
894 draw_list->AddRectFilled(p_min, p_max, col);
897 ImGui::ColorConvertFloat4ToU32(ImVec4(0.4f, 0.4f, 0.4f, 0.5f)));
900 draw_small_swatch(theme_data->
primary, 0.0f);
901 draw_small_swatch(theme_data->
surface, swatch_size + swatch_spacing);
902 draw_small_swatch(theme_data->
accent,
903 2.0f * (swatch_size + swatch_spacing));
906 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + total_swatch_width);
910 std::string label = is_current
912 : std::string(
" ") + theme_name;
914 if (ImGui::Selectable(label.c_str(), is_current)) {
918 if (theme_manager.IsPreviewActive()) {
919 theme_manager.EndPreview();
921 theme_manager.LoadTheme(theme_name);
925 if (ImGui::IsItemHovered()) {
926 any_theme_hovered =
true;
927 theme_manager.StartPreview(theme_name);
936 if (!any_theme_hovered && theme_manager.IsPreviewActive()) {
937 theme_manager.EndPreview();
942 theme_manager.RefreshAvailableThemes();
944 if (ImGui::IsItemHovered()) {
945 ImGui::SetTooltip(
"Re-scan theme directories for new or changed themes");
949 ImGui::SeparatorText(
"Display Density");
952 auto preset = theme_manager.GetCurrentTheme().density_preset;
953 int density =
static_cast<int>(preset);
954 bool changed =
false;
955 changed |= ImGui::RadioButton(
"Compact (0.75x)", &density, 0);
957 changed |= ImGui::RadioButton(
"Normal (1.0x)", &density, 1);
959 changed |= ImGui::RadioButton(
"Comfortable (1.25x)", &density, 2);
963 auto theme = theme_manager.GetCurrentTheme();
964 theme.ApplyDensityPreset(new_preset);
965 theme_manager.ApplyTheme(theme);
970 ImGui::SeparatorText(
"Editor/Workspace Motion");
974 if (ImGui::Checkbox(
"Reduced Motion", &reduced_motion)) {
975 prefs.reduced_motion = reduced_motion;
977 prefs.reduced_motion,
981 if (ImGui::IsItemHovered()) {
983 "Disable panel/editor transition animations for a calmer editing "
987 int switch_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
988 const char* switch_profile_labels[] = {
"Snappy",
"Standard",
"Relaxed"};
989 if (ImGui::Combo(
"Switch Motion Profile", &switch_profile,
990 switch_profile_labels,
991 IM_ARRAYSIZE(switch_profile_labels))) {
992 prefs.switch_motion_profile = switch_profile;
994 prefs.reduced_motion,
998 if (ImGui::IsItemHovered()) {
1000 "Controls editor/workspace switch timing and easing for panel fades "
1001 "and sidebar slides.");
1010 ImGui::Text(
"Global Font Scale");
1012 if (ImGui::SliderFloat(
"##global_font_scale", &scale, 0.5f, 2.0f,
"%.2f")) {
1014 ImGui::GetIO().FontGlobalScale = scale;
1024 if (ImGui::Checkbox(
"Show Status Bar", &show_status_bar)) {
1032 if (ImGui::IsItemHovered()) {
1034 "Display ROM, session, cursor, and zoom info at bottom of window");
1045 if (ImGui::Checkbox(
"Enable Auto-Save",
1053 if (ImGui::SliderInt(
"Interval (sec)", &interval, 60, 600)) {
1058 if (ImGui::Checkbox(
"Backup Before Save",
1078 const char* editors[] = {
"None",
"Overworld",
"Dungeon",
"Graphics"};
1080 editors, IM_ARRAYSIZE(editors))) {
1087 if (ImGui::Checkbox(
"Use HMagic sprite names (expanded)",
1115 if (ImGui::SliderInt(
"Cache Size (MB)",
1120 if (ImGui::SliderInt(
"Undo History",
1127 ImGui::Text(
"Current FPS: %.1f", ImGui::GetIO().Framerate);
1128 ImGui::Text(
"Frame Time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate);
1137 static int selected_host_index = -1;
1139 auto draw_key_row = [&](
const char* label, std::string* key,
1140 const char* env_var,
const char* id) {
1142 ImGui::Text(
"%s", label);
1143 const ImVec2 button_size = ImGui::CalcTextSize(
ICON_MD_SYNC " Env");
1144 float env_button_width =
1145 button_size.x + ImGui::GetStyle().FramePadding.x * 2.0f;
1146 float input_width = ImGui::GetContentRegionAvail().x - env_button_width -
1147 ImGui::GetStyle().ItemSpacing.x;
1148 bool stack = input_width < 160.0f;
1149 ImGui::SetNextItemWidth(stack ? -1.0f : input_width);
1150 if (ImGui::InputTextWithHint(
"##key",
"API key...", key,
1151 ImGuiInputTextFlags_Password)) {
1158 const char* env_key = std::getenv(env_var);
1170 draw_key_row(
"OpenAI", &prefs.openai_api_key,
"OPENAI_API_KEY",
"openai_key");
1171 draw_key_row(
"Anthropic", &prefs.anthropic_api_key,
"ANTHROPIC_API_KEY",
1173 draw_key_row(
"Google (Gemini)", &prefs.gemini_api_key,
"GEMINI_API_KEY",
1178 ImGui::Text(
"%s Provider Defaults (legacy)",
ICON_MD_CLOUD);
1181 const char* providers[] = {
"Ollama (Local)",
"Gemini (Cloud)",
1183 if (ImGui::Combo(
"##Provider", &prefs.ai_provider, providers,
1184 IM_ARRAYSIZE(providers))) {
1192 const char* active_preview =
"None";
1193 const char* remote_preview =
"None";
1194 for (
const auto& host : hosts) {
1195 if (!prefs.active_ai_host_id.empty() &&
1196 host.id == prefs.active_ai_host_id) {
1197 active_preview = host.label.c_str();
1199 if (!prefs.remote_build_host_id.empty() &&
1200 host.id == prefs.remote_build_host_id) {
1201 remote_preview = host.label.c_str();
1205 if (ImGui::BeginCombo(
"Active Host", active_preview)) {
1206 for (
size_t i = 0; i < hosts.size(); ++i) {
1207 const bool is_selected = (!prefs.active_ai_host_id.empty() &&
1208 hosts[i].id == prefs.active_ai_host_id);
1209 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1210 prefs.active_ai_host_id = hosts[i].id;
1211 if (prefs.remote_build_host_id.empty()) {
1212 prefs.remote_build_host_id = hosts[i].id;
1217 ImGui::SetItemDefaultFocus();
1223 if (ImGui::BeginCombo(
"Remote Build Host", remote_preview)) {
1224 for (
size_t i = 0; i < hosts.size(); ++i) {
1225 const bool is_selected = (!prefs.remote_build_host_id.empty() &&
1226 hosts[i].id == prefs.remote_build_host_id);
1227 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1228 prefs.remote_build_host_id = hosts[i].id;
1232 ImGui::SetItemDefaultFocus();
1242 if (selected_host_index >=
static_cast<int>(hosts.size())) {
1243 selected_host_index = hosts.empty() ? -1 : 0;
1245 if (selected_host_index < 0 && !hosts.empty()) {
1246 for (
size_t i = 0; i < hosts.size(); ++i) {
1247 if (!prefs.active_ai_host_id.empty() &&
1248 hosts[i].id == prefs.active_ai_host_id) {
1249 selected_host_index =
static_cast<int>(i);
1253 if (selected_host_index < 0) {
1254 selected_host_index = 0;
1258 ImGui::BeginChild(
"##ai_host_list", ImVec2(0, 150),
true);
1259 for (
size_t i = 0; i < hosts.size(); ++i) {
1260 const bool is_selected =
static_cast<int>(i) == selected_host_index;
1261 std::string label = hosts[i].label;
1262 if (hosts[i].
id == prefs.active_ai_host_id) {
1263 label +=
" (active)";
1265 if (hosts[i].
id == prefs.remote_build_host_id) {
1266 label +=
" (build)";
1268 if (ImGui::Selectable(label.c_str(), is_selected)) {
1269 selected_host_index =
static_cast<int>(i);
1271 std::string tags = BuildHostTagString(hosts[i]);
1272 if (!tags.empty()) {
1274 ImGui::TextDisabled(
"%s", tags.c_str());
1280 if (host.id.empty()) {
1281 host.id = absl::StrFormat(
"host-%zu", hosts.size() + 1);
1283 hosts.push_back(host);
1284 selected_host_index =
static_cast<int>(hosts.size() - 1);
1285 if (prefs.active_ai_host_id.empty()) {
1286 prefs.active_ai_host_id = host.id;
1288 if (prefs.remote_build_host_id.empty()) {
1289 prefs.remote_build_host_id = host.id;
1296 host.
label =
"New Host";
1297 host.
base_url =
"http://localhost:1234";
1302 if (ImGui::Button(
ICON_MD_DELETE " Remove") && selected_host_index >= 0 &&
1303 selected_host_index <
static_cast<int>(hosts.size())) {
1304 const std::string removed_id = hosts[selected_host_index].id;
1305 hosts.erase(hosts.begin() + selected_host_index);
1306 if (prefs.active_ai_host_id == removed_id) {
1307 prefs.active_ai_host_id = hosts.empty() ?
"" : hosts.front().id;
1309 if (prefs.remote_build_host_id == removed_id) {
1310 prefs.remote_build_host_id = prefs.active_ai_host_id;
1312 selected_host_index =
1315 : std::min(selected_host_index,
static_cast<int>(hosts.size() - 1));
1320 if (ImGui::Button(
"Add LM Studio")) {
1322 host.
label =
"LM Studio (local)";
1323 host.
base_url =
"http://localhost:1234";
1330 if (ImGui::Button(
"Add Ollama")) {
1332 host.
label =
"Ollama (local)";
1333 host.
base_url =
"http://localhost:11434";
1340 static std::string tailscale_host;
1341 ImGui::InputTextWithHint(
"##tailscale_host",
"host.ts.net:1234",
1344 if (ImGui::Button(
"Add Tailscale Host")) {
1345 std::string trimmed =
1346 std::string(absl::StripAsciiWhitespace(tailscale_host));
1347 if (!trimmed.empty()) {
1349 host.
label =
"Tailscale Host";
1350 if (absl::StrContains(trimmed,
"://")) {
1353 host.
base_url =
"http://" + trimmed;
1360 tailscale_host.clear();
1364 if (selected_host_index >= 0 &&
1365 selected_host_index <
static_cast<int>(hosts.size())) {
1366 auto& host = hosts[
static_cast<size_t>(selected_host_index)];
1368 ImGui::Text(
"Host Details");
1370 if (ImGui::InputText(
"Label", &host.label)) {
1373 if (ImGui::InputText(
"Base URL", &host.base_url)) {
1377 const char* api_types[] = {
"openai",
"ollama",
"gemini",
1378 "anthropic",
"lmstudio",
"grpc"};
1380 for (
int i = 0; i < IM_ARRAYSIZE(api_types); ++i) {
1381 if (host.api_type == api_types[i]) {
1386 if (ImGui::Combo(
"API Type", &api_index, api_types,
1387 IM_ARRAYSIZE(api_types))) {
1388 host.api_type = api_types[api_index];
1392 if (ImGui::InputText(
"API Key", &host.api_key,
1393 ImGuiInputTextFlags_Password)) {
1396 if (ImGui::InputText(
"Keychain ID", &host.credential_id)) {
1400 if (ImGui::SmallButton(
"Use Host ID")) {
1401 host.credential_id = host.id;
1404 if (!host.credential_id.empty() && host.api_key.empty()) {
1405 ImGui::TextDisabled(
"Keychain lookup enabled (leave API key empty).");
1408 if (ImGui::Checkbox(
"Supports Vision", &host.supports_vision)) {
1412 if (ImGui::Checkbox(
"Supports Tools", &host.supports_tools)) {
1416 if (ImGui::Checkbox(
"Supports Streaming", &host.supports_streaming)) {
1419 if (ImGui::Checkbox(
"Allow Insecure HTTP", &host.allow_insecure)) {
1428 auto& model_paths = prefs.ai_model_paths;
1429 static int selected_model_path = -1;
1430 static std::string new_model_path;
1432 if (model_paths.empty()) {
1433 ImGui::TextDisabled(
"No model paths configured.");
1436 if (ImGui::BeginChild(
"ModelPathsList", ImVec2(0, 120),
true)) {
1437 for (
size_t i = 0; i < model_paths.size(); ++i) {
1440 if (ImGui::Selectable(label.c_str(),
1441 selected_model_path ==
static_cast<int>(i))) {
1442 selected_model_path =
static_cast<int>(i);
1448 const bool has_model_selection =
1449 selected_model_path >= 0 &&
1450 selected_model_path < static_cast<int>(model_paths.size());
1451 if (has_model_selection) {
1453 model_paths.erase(model_paths.begin() + selected_model_path);
1454 selected_model_path =
1457 : std::min(selected_model_path,
1458 static_cast<int>(model_paths.size() - 1));
1464 ImGui::InputTextWithHint(
"##model_path_add",
"Add folder path...",
1467 const std::string trimmed =
1468 std::string(absl::StripAsciiWhitespace(new_model_path));
1469 if (!trimmed.empty() && AddUniquePath(&model_paths, trimmed)) {
1471 new_model_path.clear();
1477 if (!folder.empty() && AddUniquePath(&model_paths, folder)) {
1487 if (!home_dir.empty() && home_dir !=
".") {
1488 if (AddUniquePath(&model_paths, (home_dir /
"models").
string())) {
1494 if (ImGui::Button(
"Add ~/.lmstudio/models")) {
1495 if (!home_dir.empty() && home_dir !=
".") {
1496 if (AddUniquePath(&model_paths,
1497 (home_dir /
".lmstudio" /
"models").
string())) {
1503 if (ImGui::Button(
"Add ~/.ollama/models")) {
1504 if (!home_dir.empty() && home_dir !=
".") {
1505 if (AddUniquePath(&model_paths,
1506 (home_dir /
".ollama" /
"models").
string())) {
1520 ImGui::TextDisabled(
"Higher = more creative");
1531 if (ImGui::Checkbox(
"Proactive Suggestions",
1536 if (ImGui::Checkbox(
"Auto-Learn Preferences",
1541 if (ImGui::Checkbox(
"Enable Vision",
1550 const char* log_levels[] = {
"Debug",
"Info",
"Warning",
"Error",
"Fatal"};
1552 IM_ARRAYSIZE(log_levels))) {
1560 ImGuiTreeNodeFlags_DefaultOpen)) {
1561 ImGui::InputTextWithHint(
"##shortcut_filter",
"Filter shortcuts...",
1563 if (ImGui::IsItemHovered()) {
1564 ImGui::SetTooltip(
"Filter by action name or key combo");
1568 if (ImGui::TreeNode(
"Global Shortcuts")) {
1572 if (ImGui::TreeNode(
"Editor Shortcuts")) {
1576 if (ImGui::TreeNode(
"Panel Shortcuts")) {
1580 ImGui::TextDisabled(
1581 "Tip: Use Cmd/Opt labels on macOS or Ctrl/Alt on Windows/Linux. "
1582 "Function keys and symbols (/, -) are supported.");
1591 std::string haystack = absl::AsciiStrToLower(text);
1593 return absl::StrContains(haystack, needle);
1598 ImGui::TextDisabled(
"Not available");
1604 if (shortcuts.empty()) {
1605 ImGui::TextDisabled(
"No global shortcuts registered.");
1609 static std::unordered_map<std::string, std::string> editing;
1611 bool has_match =
false;
1612 for (
const auto& sc : shortcuts) {
1613 std::string label = sc.name;
1619 auto it = editing.find(sc.name);
1620 if (it == editing.end()) {
1625 current = u->second;
1627 editing[sc.name] = current;
1630 ImGui::PushID(sc.name.c_str());
1631 ImGui::Text(
"%s", sc.name.c_str());
1633 ImGui::SetNextItemWidth(180);
1634 std::string& value = editing[sc.name];
1635 if (ImGui::InputText(
"##global", &value,
1636 ImGuiInputTextFlags_EnterReturnsTrue |
1637 ImGuiInputTextFlags_AutoSelectAll)) {
1639 if (!parsed.empty() || value.empty()) {
1642 if (value.empty()) {
1653 ImGui::TextDisabled(
"No shortcuts match the current filter.");
1659 ImGui::TextDisabled(
"Not available");
1665 std::map<std::string, std::vector<Shortcut>> grouped;
1666 static std::unordered_map<std::string, std::string> editing;
1668 for (
const auto& sc : shortcuts) {
1669 auto pos = sc.name.find(
".");
1671 pos != std::string::npos ? sc.name.substr(0, pos) :
"general";
1672 grouped[group].push_back(sc);
1674 bool has_match =
false;
1675 for (
const auto& [group, list] : grouped) {
1676 std::vector<Shortcut> filtered;
1677 filtered.reserve(list.size());
1678 for (
const auto& sc : list) {
1681 filtered.push_back(sc);
1684 if (filtered.empty()) {
1688 if (ImGui::TreeNode(group.c_str())) {
1689 for (
const auto& sc : filtered) {
1690 ImGui::PushID(sc.name.c_str());
1691 ImGui::Text(
"%s", sc.name.c_str());
1693 ImGui::SetNextItemWidth(180);
1694 std::string& value = editing[sc.name];
1695 if (value.empty()) {
1703 if (ImGui::InputText(
"##editor", &value,
1704 ImGuiInputTextFlags_EnterReturnsTrue |
1705 ImGuiInputTextFlags_AutoSelectAll)) {
1707 if (!parsed.empty() || value.empty()) {
1709 if (value.empty()) {
1723 ImGui::TextDisabled(
"No shortcuts match the current filter.");
1729 ImGui::TextDisabled(
"Registry not available");
1736 bool has_match =
false;
1737 for (
const auto& category : categories) {
1739 std::vector<
decltype(cards)::value_type> filtered_cards;
1740 filtered_cards.reserve(cards.size());
1741 for (
const auto& card : cards) {
1744 filtered_cards.push_back(card);
1747 if (filtered_cards.empty()) {
1751 if (ImGui::TreeNode(category.c_str())) {
1753 for (
const auto& card : filtered_cards) {
1754 ImGui::PushID(card.card_id.c_str());
1756 ImGui::Text(
"%s %s", card.icon.c_str(), card.display_name.c_str());
1758 std::string current_shortcut;
1761 current_shortcut = it->second;
1762 }
else if (!card.shortcut_hint.empty()) {
1763 current_shortcut = card.shortcut_hint;
1765 current_shortcut =
"None";
1769 std::string display_shortcut = current_shortcut;
1771 if (!parsed.empty()) {
1776 ImGui::SetNextItemWidth(120);
1777 ImGui::SetKeyboardFocusHere();
1780 ImGuiInputTextFlags_EnterReturnsTrue)) {
1797 if (ImGui::Button(display_shortcut.c_str(), ImVec2(120, 0))) {
1803 if (ImGui::IsItemHovered()) {
1804 ImGui::SetTooltip(
"Click to edit shortcut");
1815 ImGui::TextDisabled(
"No shortcuts match the current filter.");
1824 if (patches_dir_status.ok()) {
1839 ImGui::TextDisabled(
"No patches loaded");
1840 ImGui::TextDisabled(
"Place .asm patches in assets/patches/");
1842 if (ImGui::Button(
"Browse for Patches Folder...")) {
1851 ImGui::Text(
"Loaded: %d patches (%d enabled)", total_count, enabled_count);
1857 ImGuiTabBarFlags_FittingPolicyScroll)) {
1859 if (ImGui::BeginTabItem(folder.c_str())) {
1862 ImGui::EndTabItem();
1875 ImGui::TextDisabled(
"Select a patch to view details");
1886 LOG_ERROR(
"Settings",
"Failed to apply patches: %s", status.message());
1888 LOG_INFO(
"Settings",
"Applied %d patches successfully", enabled_count);
1891 LOG_WARN(
"Settings",
"No ROM loaded");
1894 if (ImGui::IsItemHovered()) {
1895 ImGui::SetTooltip(
"Apply all enabled patches to the loaded ROM");
1902 LOG_ERROR(
"Settings",
"Failed to save patches: %s", status.message());
1915 if (patches.empty()) {
1916 ImGui::TextDisabled(
"No patches in this folder");
1921 float available_height = std::min(200.0f, patches.size() * 25.0f + 10.0f);
1922 if (ImGui::BeginChild(
"##PatchList", ImVec2(0, available_height),
true)) {
1923 for (
auto* patch : patches) {
1924 ImGui::PushID(patch->filename().c_str());
1926 bool enabled = patch->enabled();
1927 if (ImGui::Checkbox(
"##Enabled", &enabled)) {
1928 patch->set_enabled(enabled);
1935 if (ImGui::Selectable(patch->name().c_str(), is_selected)) {
1968 if (!params.empty()) {
1973 for (
auto& param : params) {
1985 switch (param->
type) {
1989 int value = param->
value;
1990 const char* format = param->
use_decimal ?
"%d" :
"$%X";
1993 ImGui::SetNextItemWidth(100);
1994 if (ImGui::InputInt(
"##Value", &value, 1, 16)) {
2008 if (ImGui::Checkbox(param->
display_name.c_str(), &checked)) {
2016 for (
size_t i = 0; i < param->
choices.size(); ++i) {
2017 bool selected = (param->
value ==
static_cast<int>(i));
2018 if (ImGui::RadioButton(param->
choices[i].c_str(), selected)) {
2019 param->
value =
static_cast<int>(i);
2027 for (
size_t i = 0; i < param->
choices.size(); ++i) {
2028 if (param->
choices[i].empty() || param->
choices[i] ==
"_EMPTY") {
2031 bool bit_set = (param->
value & (1 << i)) != 0;
2032 if (ImGui::Checkbox(param->
choices[i].c_str(), &bit_set)) {
2034 param->
value |= (1 << i);
2036 param->
value &= ~(1 << i);
2046 ImGui::SetNextItemWidth(150);
2047 if (ImGui::InputInt(
"Item ID", ¶m->
value)) {
2048 param->
value = std::clamp(param->
value, 0, 255);
const std::string & version() const
std::vector< PatchParameter > & mutable_parameters()
const std::string & author() const
const std::string & description() const
const std::string & name() const
const std::vector< FeatureFlag > & feature_flags() const
const MessageLayout & message_layout() const
const std::vector< RoomTagEntry > & room_tags() const
Get all room tags.
const std::string & hack_name() const
bool loaded() const
Check if the manifest has been loaded.
int manifest_version() const
const BuildPipeline & build_pipeline() const
absl::Status ApplyEnabledPatches(Rom *rom)
Apply all enabled patches to a ROM.
absl::Status SaveAllPatches()
Save all patches to their files.
const std::vector< std::string > & folders() const
Get list of patch folder names.
int GetEnabledPatchCount() const
Get count of enabled patches.
std::vector< AsmPatch * > GetPatchesInFolder(const std::string &folder)
Get all patches in a specific folder.
const std::vector< std::unique_ptr< AsmPatch > > & patches() const
Get all loaded patches.
absl::Status LoadPatches(const std::string &patches_dir)
Load all patches from a directory structure.
virtual void SetDependencies(const EditorDependencies &deps)
std::vector< std::string > GetAllCategories(size_t session_id) const
std::vector< PanelDescriptor > GetPanelsInCategory(size_t session_id, const std::string &category) const
void DrawPerformanceSettings()
std::string editing_card_id_
void SetStatusBar(StatusBar *bar)
void DrawProjectSettings()
void DrawPatchList(const std::string &folder)
void DrawFilesystemSettings()
void DrawAppearanceSettings()
PanelManager * panel_manager_
char shortcut_edit_buffer_[64]
ShortcutManager * shortcut_manager_
void SetDependencies(const EditorDependencies &deps) override
void DrawEditorShortcuts()
void DrawGlobalShortcuts()
bool MatchesShortcutFilter(const std::string &text) const
void DrawParameterWidget(core::PatchParameter *param)
void DrawEditorBehavior()
core::AsmPatch * selected_patch_
void DrawGeneralSettings()
void SetPanelManager(PanelManager *registry)
core::PatchManager patch_manager_
project::YazeProject * project_
void SetShortcutManager(ShortcutManager *manager)
void SetUserSettings(UserSettings *settings)
std::string selected_folder_
UserSettings * user_settings_
void DrawKeyboardShortcuts()
bool is_editing_shortcut_
std::string shortcut_filter_
void SetProject(project::YazeProject *project)
void DrawPanelShortcuts()
void DrawAIAgentSettings()
std::vector< Shortcut > GetShortcutsByScope(Shortcut::Scope scope) const
bool UpdateShortcutKeys(const std::string &name, const std::vector< ImGuiKey > &keys)
void SetEnabled(bool enabled)
Enable or disable the status bar.
static MotionProfile ClampMotionProfile(int raw_profile)
void SetMotionPreferences(bool reduced_motion, MotionProfile profile)
static ThemeManager & Get()
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_FOLDER_SPECIAL
#define ICON_MD_VIDEOGAME_ASSET
#define ICON_MD_EXTENSION
#define ICON_MD_PSYCHOLOGY
#define ICON_MD_HORIZONTAL_RULE
#define ICON_MD_SMART_TOY
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
std::vector< uint16_t > DefaultTrackTiles()
bool ParseHexToken(const std::string &token, uint16_t *out)
bool AddUniquePath(std::vector< std::string > *paths, const std::string &path)
std::vector< uint16_t > DefaultSwitchTiles()
std::vector< uint16_t > DefaultStopTiles()
std::string BuildHostTagString(const UserSettings::Preferences::AiHost &host)
std::vector< uint16_t > DefaultMinecartSpriteIds()
std::vector< uint16_t > DefaultTrackObjectIds()
std::vector< ImGuiKey > ParseShortcut(const std::string &shortcut)
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
ImVec4 ConvertColorToImVec4(const Color &color)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
DensityPreset
Typography and spacing density presets.
std::string ComputeRomHash(const uint8_t *data, size_t size)
void SetPreferHmagicSpriteNames(bool prefer)
Represents a configurable parameter within an ASM patch.
std::vector< std::string > choices
Unified dependency container for all editor types.
project::YazeProject * project
ShortcutManager * shortcut_manager
UserSettings * user_settings
PanelManager * panel_manager
std::vector< std::string > project_root_paths
std::unordered_map< std::string, std::string > panel_shortcuts
std::unordered_map< std::string, std::string > editor_shortcuts
std::vector< AiHost > ai_hosts
std::unordered_map< std::string, std::string > global_shortcuts
bool prefer_hmagic_sprite_names
Comprehensive theme structure for YAZE.
std::vector< uint16_t > track_object_ids
std::vector< uint16_t > minecart_sprite_ids
std::vector< uint16_t > track_stop_tiles
std::vector< uint16_t > track_tiles
std::vector< uint16_t > track_switch_tiles
int backup_keep_daily_days
int backup_retention_count
std::string rom_backup_folder
std::string git_repository
core::HackManifest hack_manifest
void ReloadHackManifest()
std::string hack_manifest_file
WorkspaceSettings workspace_settings
std::string output_folder
DungeonOverlaySettings dungeon_overlay
std::string symbols_filename