yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_palette.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <chrono>
6#include <filesystem>
7#include <fstream>
8
9#include "absl/strings/str_format.h"
14#include "core/project.h"
15#include "util/json.h"
16#include "util/log.h"
18
19namespace yaze {
20namespace editor {
21
22void CommandPalette::AddCommand(const std::string& name,
23 const std::string& category,
24 const std::string& description,
25 const std::string& shortcut,
26 std::function<void()> callback) {
27 CommandEntry entry;
28 entry.name = name;
29 entry.category = category;
30 entry.description = description;
31 entry.shortcut = shortcut;
32 entry.callback = callback;
33 commands_[name] = entry;
34}
35
36void CommandPalette::RecordUsage(const std::string& name) {
37 auto it = commands_.find(name);
38 if (it != commands_.end()) {
39 it->second.usage_count++;
40 it->second.last_used_ms =
41 std::chrono::duration_cast<std::chrono::milliseconds>(
42 std::chrono::system_clock::now().time_since_epoch())
43 .count();
44 }
45}
46
47/*static*/ int CommandPalette::FuzzyScore(const std::string& text,
48 const std::string& query) {
49 if (query.empty())
50 return 0;
51
52 int score = 0;
53 size_t text_idx = 0;
54 size_t query_idx = 0;
55
56 std::string text_lower = text;
57 std::string query_lower = query;
58 std::transform(text_lower.begin(), text_lower.end(), text_lower.begin(),
59 ::tolower);
60 std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(),
61 ::tolower);
62
63 // Exact match bonus
64 if (text_lower == query_lower)
65 return 1000;
66
67 // Starts with bonus
68 if (text_lower.find(query_lower) == 0)
69 return 500;
70
71 // Contains bonus
72 if (text_lower.find(query_lower) != std::string::npos)
73 return 250;
74
75 // Fuzzy match - characters in order
76 while (text_idx < text_lower.length() && query_idx < query_lower.length()) {
77 if (text_lower[text_idx] == query_lower[query_idx]) {
78 score += 10;
79 query_idx++;
80 }
81 text_idx++;
82 }
83
84 // Penalty if not all characters matched
85 if (query_idx != query_lower.length())
86 return 0;
87
88 return score;
89}
90
91std::vector<CommandEntry> CommandPalette::SearchCommands(
92 const std::string& query) {
93 std::vector<std::pair<int, CommandEntry>> scored;
94
95 for (const auto& [name, entry] : commands_) {
96 int score = FuzzyScore(entry.name, query);
97
98 // Also check category and description
99 score += FuzzyScore(entry.category, query) / 2;
100 score += FuzzyScore(entry.description, query) / 4;
101
102 // Frecency bonus (frequency + recency)
103 score += entry.usage_count * 2;
104
105 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
106 std::chrono::system_clock::now().time_since_epoch())
107 .count();
108 int64_t age_ms = now_ms - entry.last_used_ms;
109 if (age_ms < 60000) { // Used in last minute
110 score += 50;
111 } else if (age_ms < 3600000) { // Last hour
112 score += 25;
113 }
114
115 if (score > 0) {
116 scored.push_back({score, entry});
117 }
118 }
119
120 // Sort by score descending
121 std::sort(scored.begin(), scored.end(),
122 [](const auto& a, const auto& b) { return a.first > b.first; });
123
124 std::vector<CommandEntry> results;
125 for (const auto& [score, entry] : scored) {
126 results.push_back(entry);
127 }
128
129 return results;
130}
131
132std::vector<CommandEntry> CommandPalette::GetRecentCommands(int limit) {
133 std::vector<CommandEntry> recent;
134
135 for (const auto& [name, entry] : commands_) {
136 if (entry.usage_count > 0) {
137 recent.push_back(entry);
138 }
139 }
140
141 std::sort(recent.begin(), recent.end(),
142 [](const CommandEntry& a, const CommandEntry& b) {
143 return a.last_used_ms > b.last_used_ms;
144 });
145
146 if (recent.size() > static_cast<size_t>(limit)) {
147 recent.resize(limit);
148 }
149
150 return recent;
151}
152
153std::vector<CommandEntry> CommandPalette::GetFrequentCommands(int limit) {
154 std::vector<CommandEntry> frequent;
155
156 for (const auto& [name, entry] : commands_) {
157 if (entry.usage_count > 0) {
158 frequent.push_back(entry);
159 }
160 }
161
162 std::sort(frequent.begin(), frequent.end(),
163 [](const CommandEntry& a, const CommandEntry& b) {
164 return a.usage_count > b.usage_count;
165 });
166
167 if (frequent.size() > static_cast<size_t>(limit)) {
168 frequent.resize(limit);
169 }
170
171 return frequent;
172}
173
174void CommandPalette::SaveHistory(const std::string& filepath) {
175 try {
176 yaze::Json j;
177 j["version"] = 1;
178 j["commands"] = yaze::Json::object();
179
180 for (const auto& [name, entry] : commands_) {
181 if (entry.usage_count > 0) {
182 yaze::Json cmd;
183 cmd["usage_count"] = entry.usage_count;
184 cmd["last_used_ms"] = entry.last_used_ms;
185 j["commands"][name] = cmd;
186 }
187 }
188
189 std::ofstream file(filepath);
190 if (file.is_open()) {
191 file << j.dump(2);
192 LOG_INFO("CommandPalette", "Saved command history to %s",
193 filepath.c_str());
194 }
195 } catch (const std::exception& e) {
196 LOG_ERROR("CommandPalette", "Failed to save command history: %s", e.what());
197 }
198}
199
200void CommandPalette::LoadHistory(const std::string& filepath) {
201 if (!std::filesystem::exists(filepath)) {
202 return;
203 }
204
205 try {
206 std::ifstream file(filepath);
207 if (!file.is_open()) {
208 return;
209 }
210
211 std::string content((std::istreambuf_iterator<char>(file)),
212 std::istreambuf_iterator<char>());
213 yaze::Json j = yaze::Json::parse(content);
214
215 if (!j.contains("commands") || !j["commands"].is_object()) {
216 return;
217 }
218
219 int loaded = 0;
220 for (auto& [name, cmd_json] : j["commands"].items()) {
221 auto it = commands_.find(name);
222 if (it != commands_.end()) {
223 it->second.usage_count = cmd_json.value("usage_count", 0);
224 it->second.last_used_ms = cmd_json.value("last_used_ms", int64_t{0});
225 loaded++;
226 }
227 }
228
229 LOG_INFO("CommandPalette", "Loaded %d command history entries from %s",
230 loaded, filepath.c_str());
231 } catch (const std::exception& e) {
232 LOG_ERROR("CommandPalette", "Failed to load command history: %s", e.what());
233 }
234}
235
236std::vector<CommandEntry> CommandPalette::GetAllCommands() const {
237 std::vector<CommandEntry> result;
238 result.reserve(commands_.size());
239 for (const auto& [name, entry] : commands_) {
240 result.push_back(entry);
241 }
242 return result;
243}
244
246 size_t session_id) {
247 if (!panel_manager) return;
248
249 // Get all registered panel descriptors
250 const auto& descriptors = panel_manager->GetAllPanelDescriptors();
251
252 for (const auto& [prefixed_id, descriptor] : descriptors) {
253 // Use the base card_id from the descriptor, not the prefixed map key
254 const std::string& base_id = descriptor.card_id;
255
256 // Create show command
257 std::string show_name =
258 absl::StrFormat("Show: %s", descriptor.display_name);
259 std::string show_desc =
260 absl::StrFormat("Show the %s panel", descriptor.display_name);
261
262 AddCommand(show_name, CommandCategory::kPanel, show_desc,
263 descriptor.shortcut_hint,
264 [panel_manager, base_id, session_id]() {
265 panel_manager->ShowPanel(session_id, base_id);
266 });
267
268 // Create hide command
269 std::string hide_name =
270 absl::StrFormat("Hide: %s", descriptor.display_name);
271 std::string hide_desc =
272 absl::StrFormat("Hide the %s panel", descriptor.display_name);
273
274 AddCommand(hide_name, CommandCategory::kPanel, hide_desc, "",
275 [panel_manager, base_id, session_id]() {
276 panel_manager->HidePanel(session_id, base_id);
277 });
278
279 // Create toggle command
280 std::string toggle_name =
281 absl::StrFormat("Toggle: %s", descriptor.display_name);
282 std::string toggle_desc =
283 absl::StrFormat("Toggle the %s panel visibility", descriptor.display_name);
284
285 AddCommand(toggle_name, CommandCategory::kPanel, toggle_desc, "",
286 [panel_manager, base_id, session_id]() {
287 panel_manager->TogglePanel(session_id, base_id);
288 });
289 }
290}
291
293 std::function<void(const std::string&)> switch_callback) {
294 // Get all editor categories
295 auto categories = EditorRegistry::GetAllEditorCategories();
296
297 for (const auto& category : categories) {
298 std::string name = absl::StrFormat("Switch to: %s Editor", category);
299 std::string desc =
300 absl::StrFormat("Switch to the %s editor category", category);
301
302 AddCommand(name, CommandCategory::kEditor, desc, "",
303 [switch_callback, category]() { switch_callback(category); });
304 }
305}
306
308 std::function<void(const std::string&)> apply_callback) {
309 struct ProfileInfo {
310 const char* id;
311 const char* name;
312 const char* description;
313 };
314
315 static const ProfileInfo profiles[] = {
316 {"code", "Code",
317 "Focused editing workspace with minimal panel noise"},
318 {"debug", "Debug",
319 "Debugger-first workspace for tracing and memory tools"},
320 {"mapping", "Mapping",
321 "Map-centric workspace for overworld/dungeon flows"},
322 {"chat", "Chat + Agent",
323 "Agent collaboration workspace with chat-centric layout"},
324 };
325
326 for (const auto& profile : profiles) {
327 std::string name = absl::StrFormat("Apply Profile: %s", profile.name);
328 AddCommand(name, CommandCategory::kLayout, profile.description, "",
329 [apply_callback, profile_id = std::string(profile.id)]() {
330 apply_callback("profile:" + profile_id);
331 });
332 }
333
334 AddCommand("Capture Layout Snapshot", CommandCategory::kLayout,
335 "Capture current layout as temporary session snapshot", "",
336 [apply_callback]() { apply_callback("session:capture"); });
337 AddCommand("Restore Layout Snapshot", CommandCategory::kLayout,
338 "Restore temporary session snapshot", "",
339 [apply_callback]() { apply_callback("session:restore"); });
340 AddCommand("Clear Layout Snapshot", CommandCategory::kLayout,
341 "Clear temporary session snapshot", "",
342 [apply_callback]() { apply_callback("session:clear"); });
343
344 // Legacy named workspace presets
345 struct PresetInfo {
346 const char* name;
347 const char* description;
348 };
349
350 static const PresetInfo presets[] = {
351 {"Minimal", "Minimal workspace with essential panels only"},
352 {"Developer", "Debug-focused layout with emulator and memory tools"},
353 {"Designer", "Visual-focused layout for graphics and palette editing"},
354 {"Modder", "Full-featured layout with all panels available"},
355 {"Overworld Expert", "Optimized layout for overworld editing"},
356 {"Dungeon Expert", "Optimized layout for dungeon editing"},
357 {"Testing", "QA-focused layout with testing tools"},
358 {"Audio", "Music and sound editing focused layout"},
359 };
360
361 for (const auto& preset : presets) {
362 std::string name = absl::StrFormat("Apply Layout: %s", preset.name);
363
364 AddCommand(name, CommandCategory::kLayout, preset.description, "",
365 [apply_callback, preset_name = std::string(preset.name)]() {
366 apply_callback(preset_name);
367 });
368 }
369
370 // Reset to default layout command
371 AddCommand("Reset Layout: Default", CommandCategory::kLayout,
372 "Reset to the default layout for current editor", "",
373 [apply_callback]() { apply_callback("Default"); });
374}
375
377 std::function<void(const std::string&)> open_callback) {
378 const auto& recent_files =
380
381 for (const auto& filepath : recent_files) {
382 // Skip files that no longer exist
383 if (!std::filesystem::exists(filepath)) {
384 continue;
385 }
386
387 // Extract just the filename for display
388 std::filesystem::path path(filepath);
389 std::string filename = path.filename().string();
390
391 std::string name = absl::StrFormat("Open Recent: %s", filename);
392 std::string desc = absl::StrFormat("Open file %s", filepath);
393
394 AddCommand(name, CommandCategory::kFile, desc, "",
395 [open_callback, filepath]() { open_callback(filepath); });
396 }
397}
398
400 constexpr int kTotalRooms = 0x128;
401 for (int room_id = 0; room_id < kTotalRooms; ++room_id) {
402 const std::string label = zelda3::GetRoomLabel(room_id);
403 const std::string room_name =
404 label.empty() ? absl::StrFormat("Room %03X", room_id) : label;
405
406 const std::string name =
407 absl::StrFormat("Dungeon: Open Room [%03X] %s", room_id, room_name);
408 const std::string desc =
409 absl::StrFormat("Jump to dungeon room %03X", room_id);
410
412 [room_id, session_id]() {
413 if (auto* bus = ContentRegistry::Context::event_bus()) {
414 bus->Publish(
415 JumpToRoomRequestEvent::Create(room_id, session_id));
416 }
417 });
418 }
419}
420
421} // namespace editor
422} // namespace yaze
bool is_object() const
Definition json.h:57
static Json parse(const std::string &)
Definition json.h:36
static Json object()
Definition json.h:34
items_view items()
Definition json.h:88
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
bool contains(const std::string &) const
Definition json.h:53
std::vector< CommandEntry > SearchCommands(const std::string &query)
void RegisterDungeonRoomCommands(size_t session_id)
Register dungeon room navigation commands.
void RegisterPanelCommands(PanelManager *panel_manager, size_t session_id)
Register all panel toggle commands from PanelManager.
void SaveHistory(const std::string &filepath)
Save command usage history to disk.
void RegisterLayoutCommands(std::function< void(const std::string &)> apply_callback)
Register layout preset commands.
void AddCommand(const std::string &name, const std::string &category, const std::string &description, const std::string &shortcut, std::function< void()> callback)
void LoadHistory(const std::string &filepath)
Load command usage history from disk.
std::vector< CommandEntry > GetAllCommands() const
Get all registered commands.
void RecordUsage(const std::string &name)
std::unordered_map< std::string, CommandEntry > commands_
void RegisterEditorCommands(std::function< void(const std::string &)> switch_callback)
Register all editor switch commands.
std::vector< CommandEntry > GetRecentCommands(int limit=10)
void RegisterRecentFilesCommands(std::function< void(const std::string &)> open_callback)
Register commands to open recent files.
std::vector< CommandEntry > GetFrequentCommands(int limit=10)
static int FuzzyScore(const std::string &text, const std::string &query)
static std::vector< std::string > GetAllEditorCategories()
Get all editor categories in display order for sidebar.
Central registry for all editor cards with session awareness and dependency injection.
bool TogglePanel(size_t session_id, const std::string &base_card_id)
const std::unordered_map< std::string, PanelDescriptor > & GetAllPanelDescriptors() const
Get all panel descriptors (for layout designer, panel browser, etc.)
bool HidePanel(size_t session_id, const std::string &base_card_id)
static RecentFilesManager & GetInstance()
Definition project.h:374
const std::vector< std::string > & GetRecentFiles() const
Definition project.h:409
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_INFO(category, format,...)
Definition log.h:105
::yaze::EventBus * event_bus()
Get the current EventBus instance.
std::string GetRoomLabel(int id)
Convenience function to get a room label.
static constexpr const char * kLayout
static constexpr const char * kFile
static constexpr const char * kEditor
static constexpr const char * kPanel
static constexpr const char * kNavigation
std::function< void()> callback
static JumpToRoomRequestEvent Create(int room, size_t session=0)