9#include "absl/strings/str_format.h"
26 const std::string& category,
27 const std::string& description,
28 const std::string& shortcut,
29 std::function<
void()> callback) {
47 std::unique_ptr<CommandProvider> provider) {
50 const std::string
id = provider->ProviderId();
53 "Provider with empty id refused; skipping registration");
59 if ((*it)->ProviderId() == id) {
60 LOG_WARN(
"CommandPalette",
"Replacing existing CommandProvider '%s'",
78 [&](
const std::unique_ptr<CommandProvider>& p) {
79 return p && p->ProviderId() == provider_id;
86 if (provider && provider->ProviderId() == provider_id) {
89 provider->Provide(
this);
98 std::vector<std::string> ids;
102 ids.push_back(provider->ProviderId());
104 for (
const auto&
id : ids) {
110 if (provider_id.empty())
113 if (it->second.provider_id == provider_id) {
124 it->second.usage_count++;
125 it->second.last_used_ms =
126 std::chrono::duration_cast<std::chrono::milliseconds>(
127 std::chrono::system_clock::now().time_since_epoch())
133 const std::string& query) {
139 size_t query_idx = 0;
141 std::string text_lower = text;
142 std::string query_lower = query;
143 std::transform(text_lower.begin(), text_lower.end(), text_lower.begin(),
145 std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(),
149 if (text_lower == query_lower)
153 if (text_lower.find(query_lower) == 0)
157 if (text_lower.find(query_lower) != std::string::npos)
161 while (text_idx < text_lower.length() && query_idx < query_lower.length()) {
162 if (text_lower[text_idx] == query_lower[query_idx]) {
170 if (query_idx != query_lower.length())
177 const std::string& query) {
178 std::vector<std::pair<int, CommandEntry>> scored;
180 for (
const auto& [name, entry] :
commands_) {
184 score +=
FuzzyScore(entry.category, query) / 2;
185 score +=
FuzzyScore(entry.description, query) / 4;
188 score += entry.usage_count * 2;
190 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
191 std::chrono::system_clock::now().time_since_epoch())
193 int64_t age_ms = now_ms - entry.last_used_ms;
194 if (age_ms < 60000) {
196 }
else if (age_ms < 3600000) {
201 scored.push_back({score, entry});
206 std::sort(scored.begin(), scored.end(),
207 [](
const auto& a,
const auto& b) { return a.first > b.first; });
209 std::vector<CommandEntry> results;
210 for (
const auto& [score, entry] : scored) {
211 results.push_back(entry);
218 std::vector<CommandEntry> recent;
220 for (
const auto& [name, entry] :
commands_) {
221 if (entry.usage_count > 0) {
222 recent.push_back(entry);
226 std::sort(recent.begin(), recent.end(),
228 return a.last_used_ms > b.last_used_ms;
231 if (recent.size() >
static_cast<size_t>(limit)) {
232 recent.resize(limit);
239 std::vector<CommandEntry> frequent;
241 for (
const auto& [name, entry] :
commands_) {
242 if (entry.usage_count > 0) {
243 frequent.push_back(entry);
247 std::sort(frequent.begin(), frequent.end(),
249 return a.usage_count > b.usage_count;
252 if (frequent.size() >
static_cast<size_t>(limit)) {
253 frequent.resize(limit);
265 for (
const auto& [name, entry] :
commands_) {
266 if (entry.usage_count > 0) {
268 cmd[
"usage_count"] = entry.usage_count;
269 cmd[
"last_used_ms"] = entry.last_used_ms;
270 j[
"commands"][name] = cmd;
274 std::ofstream file(filepath);
275 if (file.is_open()) {
277 LOG_INFO(
"CommandPalette",
"Saved command history to %s",
280 }
catch (
const std::exception& e) {
281 LOG_ERROR(
"CommandPalette",
"Failed to save command history: %s", e.what());
286 if (!std::filesystem::exists(filepath)) {
291 std::ifstream file(filepath);
292 if (!file.is_open()) {
296 std::string content((std::istreambuf_iterator<char>(file)),
297 std::istreambuf_iterator<char>());
305 for (
auto& [name, cmd_json] : j[
"commands"].
items()) {
308 it->second.usage_count = cmd_json.value(
"usage_count", 0);
309 it->second.last_used_ms = cmd_json.value(
"last_used_ms", int64_t{0});
314 LOG_INFO(
"CommandPalette",
"Loaded %d command history entries from %s",
315 loaded, filepath.c_str());
316 }
catch (
const std::exception& e) {
317 LOG_ERROR(
"CommandPalette",
"Failed to load command history: %s", e.what());
322 std::vector<CommandEntry> result;
324 for (
const auto& [name, entry] :
commands_) {
325 result.push_back(entry);
336 const auto* descriptor =
343 std::string show_name =
344 absl::StrFormat(
"Show: %s", descriptor->display_name);
345 std::string show_desc =
346 absl::StrFormat(
"Open the %s window", descriptor->display_name);
349 descriptor->shortcut_hint,
350 [window_manager, base_id, session_id]() {
351 window_manager->OpenWindow(session_id, base_id);
355 std::string hide_name =
356 absl::StrFormat(
"Hide: %s", descriptor->display_name);
357 std::string hide_desc =
358 absl::StrFormat(
"Close the %s window", descriptor->display_name);
361 [window_manager, base_id, session_id]() {
366 std::string toggle_name =
367 absl::StrFormat(
"Toggle: %s", descriptor->display_name);
368 std::string toggle_desc = absl::StrFormat(
"Toggle the %s window visibility",
369 descriptor->display_name);
371 auto toggle_fn = [window_manager, base_id, session_id]() {
380 std::string window_name =
381 absl::StrFormat(
"window: %s", descriptor->display_name);
384 descriptor->shortcut_hint, [window_manager, base_id, session_id]() {
385 WindowHost host(window_manager);
386 const bool opened = ImGui::GetCurrentContext() != nullptr
387 ? host.OpenAndFocusWindow(session_id, base_id)
388 : host.OpenWindow(session_id, base_id);
390 window_manager->MarkWindowRecentlyUsed(base_id);
397 std::string pin_toggle_name =
398 absl::StrFormat(
"Toggle Pin: %s", descriptor->display_name);
399 std::string pin_toggle_desc =
400 absl::StrFormat(
"Keep the %s window visible across editor switches",
401 descriptor->display_name);
404 [window_manager, base_id, session_id]() {
412void CommandPalette::RegisterDrawerCommands(
413 std::function<
void(
int drawer_type)> toggle_callback,
414 std::function<
void()> cycle_next, std::function<
void()> cycle_prev) {
415 if (!toggle_callback)
422 const int type_as_int =
static_cast<int>(entry.type);
423 std::string name = absl::StrFormat(
"drawer: %s", entry.name);
425 absl::StrFormat(
"Toggle the %s right drawer", entry.name);
428 name, CommandCategory::kDrawer, desc,
"",
429 [toggle_callback, type_as_int]() { toggle_callback(type_as_int); });
433 AddCommand(
"drawer: Next", CommandCategory::kDrawer,
434 "Cycle to the next right drawer",
"", std::move(cycle_next));
437 AddCommand(
"drawer: Previous", CommandCategory::kDrawer,
438 "Cycle to the previous right drawer",
"", std::move(cycle_prev));
442void CommandPalette::RegisterEditorCommands(
443 std::function<
void(
const std::string&)> switch_callback) {
445 auto categories = EditorRegistry::GetAllEditorCategories();
447 for (
const auto& category : categories) {
448 std::string name = absl::StrFormat(
"Switch to: %s Editor", category);
450 absl::StrFormat(
"Switch to the %s editor category", category);
452 AddCommand(name, CommandCategory::kEditor, desc,
"",
453 [switch_callback, category]() { switch_callback(category); });
457void CommandPalette::RegisterLayoutCommands(
458 std::function<
void(
const std::string&)> apply_callback) {
462 const char* description;
465 static const ProfileInfo profiles[] = {
466 {
"code",
"Code",
"Focused editing workspace with minimal panel noise"},
468 "Debugger-first workspace for tracing and memory tools"},
469 {
"mapping",
"Mapping",
470 "Map-centric workspace for overworld/dungeon flows"},
471 {
"chat",
"Chat + Agent",
472 "Agent collaboration workspace with chat-centric layout"},
475 for (
const auto& profile : profiles) {
476 std::string name = absl::StrFormat(
"Apply Profile: %s", profile.name);
477 auto apply_fn = [apply_callback, profile_id = std::string(profile.id)]() {
478 apply_callback(
"profile:" + profile_id);
480 AddCommand(name, CommandCategory::kLayout, profile.description,
"",
482 AddCommand(absl::StrFormat(
"layout: profile %s", profile.name),
483 CommandCategory::kLayout, profile.description,
"", apply_fn);
486 AddCommand(
"Capture Layout Snapshot", CommandCategory::kLayout,
487 "Capture current layout as temporary session snapshot",
"",
488 [apply_callback]() { apply_callback(
"session:capture"); });
489 AddCommand(
"layout: capture snapshot", CommandCategory::kLayout,
490 "Capture current layout as temporary session snapshot",
"",
491 [apply_callback]() { apply_callback(
"session:capture"); });
492 AddCommand(
"Restore Layout Snapshot", CommandCategory::kLayout,
493 "Restore temporary session snapshot",
"",
494 [apply_callback]() { apply_callback(
"session:restore"); });
495 AddCommand(
"layout: restore snapshot", CommandCategory::kLayout,
496 "Restore temporary session snapshot",
"",
497 [apply_callback]() { apply_callback(
"session:restore"); });
498 AddCommand(
"Clear Layout Snapshot", CommandCategory::kLayout,
499 "Clear temporary session snapshot",
"",
500 [apply_callback]() { apply_callback(
"session:clear"); });
501 AddCommand(
"layout: clear snapshot", CommandCategory::kLayout,
502 "Clear temporary session snapshot",
"",
503 [apply_callback]() { apply_callback(
"session:clear"); });
508 const char* description;
511 static const PresetInfo presets[] = {
512 {
"Minimal",
"Minimal workspace with essential panels only"},
513 {
"Developer",
"Debug-focused layout with emulator and memory tools"},
514 {
"Designer",
"Visual-focused layout for graphics and palette editing"},
515 {
"Modder",
"Full-featured layout with all panels available"},
516 {
"Overworld Expert",
"Optimized layout for overworld editing"},
517 {
"Dungeon Expert",
"Optimized layout for dungeon editing"},
518 {
"Testing",
"QA-focused layout with testing tools"},
519 {
"Audio",
"Music and sound editing focused layout"},
520 {
"Logic Debugger",
"Debug and development focused layout"},
521 {
"Overworld Artist",
"Visual and overworld focused layout"},
522 {
"Dungeon Master",
"Comprehensive dungeon editing layout"},
523 {
"Audio Engineer",
"Music and sound editing layout"},
526 for (
const auto& preset : presets) {
527 std::string name = absl::StrFormat(
"Apply Layout: %s", preset.name);
528 auto apply_fn = [apply_callback, preset_name = std::string(preset.name)]() {
529 apply_callback(preset_name);
532 AddCommand(name, CommandCategory::kLayout, preset.description,
"",
534 AddCommand(absl::StrFormat(
"layout: %s", preset.name),
535 CommandCategory::kLayout, preset.description,
"", apply_fn);
539 AddCommand(
"Reset Layout: Default", CommandCategory::kLayout,
540 "Reset to the default layout for current editor",
"",
541 [apply_callback]() { apply_callback(
"Default"); });
542 AddCommand(
"layout: Default", CommandCategory::kLayout,
543 "Reset to the default layout for current editor",
"",
544 [apply_callback]() { apply_callback(
"Default"); });
547void CommandPalette::RegisterRecentFilesCommands(
548 std::function<
void(
const std::string&)> open_callback) {
549 const auto& recent_files =
552 for (
const auto& filepath : recent_files) {
554 if (!std::filesystem::exists(filepath)) {
559 std::filesystem::path path(filepath);
560 std::string filename = path.filename().string();
562 std::string name = absl::StrFormat(
"Open Recent: %s", filename);
563 std::string desc = absl::StrFormat(
"Open file %s", filepath);
565 AddCommand(name, CommandCategory::kFile, desc,
"",
566 [open_callback, filepath]() { open_callback(filepath); });
570void CommandPalette::RegisterDungeonRoomCommands(
size_t session_id) {
571 constexpr int kTotalRooms = 0x128;
572 for (
int room_id = 0; room_id < kTotalRooms; ++room_id) {
574 const std::string room_name =
575 label.empty() ? absl::StrFormat(
"Room %03X", room_id) : label;
577 const std::string name =
578 absl::StrFormat(
"Dungeon: Open Room [%03X] %s", room_id, room_name);
579 const std::string desc =
580 absl::StrFormat(
"Jump to dungeon room %03X", room_id);
583 name, CommandCategory::kNavigation, desc,
"", [room_id, session_id]() {
584 if (
auto* bus = ContentRegistry::Context::event_bus()) {
585 bus->Publish(JumpToRoomRequestEvent::Create(room_id, session_id));
591void CommandPalette::RegisterWorkflowCommands(
593 if (window_manager) {
595 for (
const auto& category : categories) {
596 for (
const auto& descriptor :
598 if (descriptor.workflow_group.empty()) {
601 if (descriptor.enabled_condition && !descriptor.enabled_condition()) {
604 const std::string label = descriptor.workflow_label.empty()
605 ? descriptor.display_name
606 : descriptor.workflow_label;
607 const std::string group = descriptor.workflow_group.empty()
608 ? std::string(
"General")
609 : descriptor.workflow_group;
610 const std::string description =
611 descriptor.workflow_description.empty()
612 ? absl::StrFormat(
"Open %s", descriptor.display_name)
613 : descriptor.workflow_description;
615 absl::StrFormat(
"%s: %s", group, label), CommandCategory::kWorkflow,
616 description, descriptor.shortcut_hint,
617 [window_manager, session_id, panel_id = descriptor.card_id]() {
618 window_manager->OpenWindow(session_id, panel_id);
624 for (
const auto& action : ContentRegistry::WorkflowActions::GetAll()) {
625 if (action.enabled && !action.enabled()) {
628 const std::string group =
629 action.group.empty() ? std::string(
"General") : action.group;
630 AddCommand(absl::StrFormat(
"%s: %s", group, action.label),
631 CommandCategory::kWorkflow, action.description, action.shortcut,
636void CommandPalette::RegisterWelcomeCommands(
638 const std::vector<std::string>& template_names,
639 std::function<
void(
const std::string&)> remove_callback,
640 std::function<
void(
const std::string&)> toggle_pin_callback,
641 std::function<
void()> undo_remove_callback,
642 std::function<
void()> clear_recents_callback,
643 std::function<
void(
const std::string&)> create_from_template_callback,
644 std::function<
void()> dismiss_welcome_callback,
645 std::function<
void()> show_welcome_callback) {
650 for (
const auto& entry : model->
entries()) {
651 if (entry.unavailable)
653 const std::string label = entry.display_name_override.empty()
655 : entry.display_name_override;
656 const std::string path = entry.filepath;
658 if (remove_callback) {
659 const std::string name =
660 absl::StrFormat(
"Welcome: Remove Recent \"%s\"", label);
661 const std::string desc =
662 absl::StrFormat(
"Remove %s from the welcome screen's recents list.",
664 AddCommand(name, CommandCategory::kFile, desc,
"",
665 [remove_callback, path]() { remove_callback(path); });
667 if (toggle_pin_callback) {
668 const std::string name = absl::StrFormat(
669 "Welcome: %s Recent \"%s\"", entry.pinned ?
"Unpin" :
"Pin", label);
670 const std::string desc =
671 absl::StrFormat(
"%s %s on the welcome screen.",
672 entry.pinned ?
"Unpin" :
"Pin", entry.filepath);
674 name, CommandCategory::kFile, desc,
"",
675 [toggle_pin_callback, path]() { toggle_pin_callback(path); });
680 if (clear_recents_callback) {
681 AddCommand(
"Welcome: Clear Recent Files", CommandCategory::kFile,
682 "Forget every entry in the welcome screen's recent list.",
"",
683 clear_recents_callback);
686 if (undo_remove_callback) {
687 AddCommand(
"Welcome: Undo Last Recent Removal", CommandCategory::kFile,
688 "Restore the last recent-project entry removed via Forget.",
"",
689 undo_remove_callback);
692 if (create_from_template_callback) {
693 for (
const auto& template_name : template_names) {
694 if (template_name.empty())
696 const std::string name = absl::StrFormat(
697 "Welcome: Create Project from Template: %s", template_name);
698 const std::string desc = absl::StrFormat(
699 "Start a new project using the \"%s\" template.", template_name);
700 AddCommand(name, CommandCategory::kFile, desc,
"",
701 [create_from_template_callback, template_name]() {
702 create_from_template_callback(template_name);
707 if (show_welcome_callback) {
708 AddCommand(
"Welcome: Show Welcome Screen", CommandCategory::kView,
709 "Bring back the welcome screen if it's been dismissed.",
"",
710 show_welcome_callback);
712 if (dismiss_welcome_callback) {
713 AddCommand(
"Welcome: Dismiss Welcome Screen", CommandCategory::kView,
714 "Hide the welcome screen for the rest of this session.",
"",
715 dismiss_welcome_callback);
static Json parse(const std::string &)
std::string dump(int=-1, char=' ', bool=false, int=0) const
bool contains(const std::string &) const
std::vector< CommandEntry > SearchCommands(const std::string &query)
void SaveHistory(const std::string &filepath)
Save command usage history to disk.
void Clear()
Clear all commands (and forget every registered provider).
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.
void UnregisterProvider(const std::string &provider_id)
void RegisterPanelCommands(WorkspaceWindowManager *window_manager, size_t session_id)
Register all window toggle commands from WorkspaceWindowManager.
void RemoveProviderCommands(const std::string &provider_id)
std::vector< CommandEntry > GetAllCommands() const
Get all registered commands.
void RecordUsage(const std::string &name)
std::unordered_map< std::string, CommandEntry > commands_
void RegisterProvider(std::unique_ptr< CommandProvider > provider)
std::vector< CommandEntry > GetRecentCommands(int limit=10)
void RefreshProviders()
Remove-and-re-run every registered provider.
std::string current_provider_id_
std::vector< std::unique_ptr< CommandProvider > > providers_
std::vector< CommandEntry > GetFrequentCommands(int limit=10)
static int FuzzyScore(const std::string &text, const std::string &query)
void RefreshProvider(const std::string &provider_id)
Plug-in command source for the palette.
virtual void Provide(CommandPalette *palette)=0
Populate palette with this source's commands.
const std::vector< RecentProject > & entries() const
Central registry for all editor cards with session awareness and dependency injection.
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
std::vector< std::string > GetWindowsInSession(size_t session_id) const
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
bool CloseWindow(size_t session_id, const std::string &base_window_id)
std::vector< std::string > GetAllCategories(size_t session_id) const
bool IsWindowPinned(size_t session_id, const std::string &base_window_id) const
bool ToggleWindow(size_t session_id, const std::string &base_window_id)
static RecentFilesManager & GetInstance()
const std::vector< std::string > & GetRecentFiles() const
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
absl::Span< const DrawerCatalogEntry > GetDrawerCatalog()
Switchable drawers in header-cycle order (excludes Tool Output).
std::string GetRoomLabel(int id)
Convenience function to get a room label.
static constexpr const char * kPanel
std::function< void()> callback