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"
17#include "core/project.h"
18#include "util/json.h"
19#include "util/log.h"
20#include "zelda3/common.h"
22
23namespace yaze {
24namespace editor {
25
26namespace {
27
28constexpr int kScoreExact = 1000;
29constexpr int kScorePrefix = 800;
30constexpr int kScoreWordStartSubstring = 600;
31constexpr int kScoreSubstring = 400;
33
34// Per-character weights for subsequence matches. The word-start bonus is
35// larger than the consecutive bonus so "ow" prefers "Show: OverWorld"-style
36// initials over two adjacent letters inside one word.
37constexpr int kCharBase = 2;
38constexpr int kCharWordStartBonus = 8;
39constexpr int kCharConsecutiveBonus = 4;
40
41// The shortcuts provider id; its entries lose representative ties so the
42// palette-native name is shown while the live keybinding is inherited.
43constexpr const char* kShortcutsProviderId = "shortcuts";
44
45std::string ToLower(std::string_view text) {
46 std::string out(text);
47 std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
48 return static_cast<char>(std::tolower(c));
49 });
50 return out;
51}
52
53bool IsAlnum(char c) {
54 return std::isalnum(static_cast<unsigned char>(c)) != 0;
55}
56
57bool IsWordStartAt(const std::string& text, size_t index) {
58 return index == 0 || !IsAlnum(text[index - 1]);
59}
60
61std::vector<std::string> SplitWhitespace(const std::string& text) {
62 std::vector<std::string> tokens;
63 std::string current;
64 for (char c : text) {
65 if (std::isspace(static_cast<unsigned char>(c))) {
66 if (!current.empty()) {
67 tokens.push_back(current);
68 current.clear();
69 }
70 } else {
71 current.push_back(c);
72 }
73 }
74 if (!current.empty())
75 tokens.push_back(current);
76 return tokens;
77}
78
79// Best in-order alignment of `query` inside `text` (both lowercase).
80// Returns -1 when `query` is not a subsequence.
81int BestSubsequenceScore(const std::string& text, const std::string& query) {
82 const size_t n = text.size();
83 const size_t m = query.size();
84 if (m == 0 || m > n)
85 return -1;
86 constexpr int kNone = -1;
87 // prev[i]: best score with query[0..j-1] matched and query[j-1] at text[i].
88 std::vector<int> prev(n, kNone);
89 std::vector<int> cur(n, kNone);
90 for (size_t j = 0; j < m; ++j) {
91 int best_before = kNone; // max of prev[k] for k < i - 1
92 for (size_t i = 0; i < n; ++i) {
93 cur[i] = kNone;
94 if (i >= 2 && prev[i - 2] > best_before)
95 best_before = prev[i - 2];
96 if (text[i] != query[j])
97 continue;
98 int char_score = kCharBase;
99 if (IsWordStartAt(text, i))
100 char_score += kCharWordStartBonus;
101 if (j == 0) {
102 cur[i] = char_score;
103 continue;
104 }
105 int best = kNone;
106 if (best_before != kNone)
107 best = best_before + char_score;
108 if (i >= 1 && prev[i - 1] != kNone) {
109 best = std::max(best, prev[i - 1] + char_score + kCharConsecutiveBonus);
110 }
111 cur[i] = best;
112 }
113 std::swap(prev, cur);
114 }
115 int result = kNone;
116 for (int v : prev)
117 result = std::max(result, v);
118 return result;
119}
120
121int ScoreSingle(const std::string& text, const std::string& query) {
122 if (query.empty() || text.empty())
123 return 0;
124 if (text == query)
125 return kScoreExact;
126 size_t pos = text.find(query);
127 if (pos == 0)
128 return kScorePrefix;
129 if (pos != std::string::npos) {
130 while (pos != std::string::npos) {
131 if (IsWordStartAt(text, pos))
133 pos = text.find(query, pos + 1);
134 }
135 return kScoreSubstring;
136 }
137 const int subsequence = BestSubsequenceScore(text, query);
138 if (subsequence < 0)
139 return 0;
140 return std::min(kScoreSubsequenceMax, 1 + subsequence);
141}
142
143// True when `name` contains the word "Panel"/"Panels" (legacy alias names).
144bool HasPanelWord(std::string_view name) {
145 const std::string lower = ToLower(name);
146 for (size_t pos = lower.find("panel"); pos != std::string::npos;
147 pos = lower.find("panel", pos + 1)) {
148 const size_t end = pos + 5;
149 const bool starts = pos == 0 || !IsAlnum(lower[pos - 1]);
150 const bool ends = end == lower.size() || !IsAlnum(lower[end]) ||
151 (lower[end] == 's' &&
152 (end + 1 == lower.size() || !IsAlnum(lower[end + 1])));
153 if (starts && ends)
154 return true;
155 }
156 return false;
157}
158
159int FrecencyBoost(int usage_count, int64_t last_used_ms, int64_t now_ms) {
160 int boost = std::min(usage_count, 20) * 3; // <= 60
161 if (last_used_ms > 0) {
162 const int64_t age_ms = now_ms - last_used_ms;
163 if (age_ms < 60LL * 1000) {
164 boost += 50;
165 } else if (age_ms < 60LL * 60 * 1000) {
166 boost += 30;
167 } else if (age_ms < 24LL * 60 * 60 * 1000) {
168 boost += 15;
169 }
170 }
171 return boost; // <= 110, below the 200-point gap between match tiers
172}
173
174} // namespace
175
177 return std::chrono::duration_cast<std::chrono::milliseconds>(
178 std::chrono::system_clock::now().time_since_epoch())
179 .count();
180}
181
182void CommandPalette::AddCommand(const std::string& name,
183 const std::string& category,
184 const std::string& description,
185 const std::string& shortcut,
186 std::function<void()> callback) {
187 CommandEntry entry;
188 entry.name = name;
189 entry.category = category;
190 entry.description = description;
191 entry.shortcut = shortcut;
192 entry.callback = std::move(callback);
194 ++generation_;
195 if (auto it = usage_.find(name); it != usage_.end()) {
196 entry.usage_count = it->second.count;
197 entry.last_used_ms = it->second.last_used_ms;
198 }
199 commands_[name] = std::move(entry);
200}
201
203 ++generation_;
204 commands_.clear();
205 providers_.clear();
206 current_provider_id_.clear();
207}
208
210 std::unique_ptr<CommandProvider> provider) {
211 if (!provider)
212 return;
213 const std::string id = provider->ProviderId();
214 if (id.empty()) {
215 LOG_WARN("CommandPalette",
216 "Provider with empty id refused; skipping registration");
217 return;
218 }
219 // Replace any prior provider with the same id (warn so duplicate-id bugs
220 // don't hide silently).
221 for (auto it = providers_.begin(); it != providers_.end(); ++it) {
222 if ((*it)->ProviderId() == id) {
223 LOG_WARN("CommandPalette", "Replacing existing CommandProvider '%s'",
224 id.c_str());
226 providers_.erase(it);
227 break;
228 }
229 }
230 CommandProvider* raw = provider.get();
231 providers_.push_back(std::move(provider));
233 raw->Provide(this);
234 current_provider_id_.clear();
235}
236
237void CommandPalette::UnregisterProvider(const std::string& provider_id) {
238 RemoveProviderCommands(provider_id);
239 providers_.erase(
240 std::remove_if(providers_.begin(), providers_.end(),
241 [&](const std::unique_ptr<CommandProvider>& p) {
242 return p && p->ProviderId() == provider_id;
243 }),
244 providers_.end());
245}
246
247void CommandPalette::RefreshProvider(const std::string& provider_id) {
248 for (auto& provider : providers_) {
249 if (provider && provider->ProviderId() == provider_id) {
250 RemoveProviderCommands(provider_id);
251 current_provider_id_ = provider_id;
252 provider->Provide(this);
253 current_provider_id_.clear();
254 return;
255 }
256 }
257}
258
260 // Snapshot ids so we don't iterate while mutating.
261 std::vector<std::string> ids;
262 ids.reserve(providers_.size());
263 for (const auto& provider : providers_) {
264 if (provider)
265 ids.push_back(provider->ProviderId());
266 }
267 for (const auto& id : ids) {
268 RefreshProvider(id);
269 }
270}
271
272void CommandPalette::RemoveProviderCommands(const std::string& provider_id) {
273 if (provider_id.empty())
274 return;
275 ++generation_;
276 for (auto it = commands_.begin(); it != commands_.end();) {
277 if (it->second.provider_id == provider_id) {
278 it = commands_.erase(it);
279 } else {
280 ++it;
281 }
282 }
283}
284
285bool CommandPalette::RecordUsage(const std::string& name) {
286 const std::string canonical = CanonicalCommandName(name);
287 if (canonical.empty())
288 return false;
289 const int64_t now = NowMs();
290 auto& stats = usage_[canonical];
291 // Fold in any stats recorded under another member of the dedupe group so
292 // the count keeps growing from the group's best value.
293 for (const auto& [other_name, entry] : commands_) {
294 if (other_name != canonical &&
295 NormalizeCommandName(other_name) == NormalizeCommandName(canonical)) {
296 stats.count = std::max(stats.count, entry.usage_count);
297 }
298 }
299 stats.count++;
300 stats.last_used_ms = now;
301 ++generation_;
302 if (auto it = commands_.find(canonical); it != commands_.end()) {
303 it->second.usage_count = stats.count;
304 it->second.last_used_ms = stats.last_used_ms;
305 }
306 return true;
307}
308
309/*static*/ int CommandPalette::FuzzyScore(const std::string& text,
310 const std::string& query) {
311 return ScoreText(text, query);
312}
313
314/*static*/ int CommandPalette::ScoreText(std::string_view text,
315 std::string_view query) {
316 const std::string query_lower = ToLower(query);
317 const std::string text_lower = ToLower(text);
318 const int whole = ScoreSingle(text_lower, query_lower);
319 if (whole > 0)
320 return whole;
321
322 // Multi-word queries ("open room", "pin dungeon") match when every token
323 // matches somewhere, regardless of order. Stays in the subsequence tier.
324 const auto tokens = SplitWhitespace(query_lower);
325 if (tokens.size() < 2)
326 return 0;
327 int total = 0;
328 for (const auto& token : tokens) {
329 const int token_score = ScoreSingle(text_lower, token);
330 if (token_score <= 0)
331 return 0;
332 total += token_score;
333 }
334 const int average = total / static_cast<int>(tokens.size());
335 return std::clamp(average / 2, 1, kScoreSubsequenceMax);
336}
337
338/*static*/ int CommandPalette::ScoreEntry(const CommandEntry& entry,
339 std::string_view query,
340 int64_t now_ms) {
341 int score = ScoreText(entry.name, query);
342 score = std::max(score, ScoreText(entry.category, query) / 2);
343 score = std::max(score, ScoreText(entry.description, query) / 4);
344 if (score <= 0)
345 return 0;
346 return score + FrecencyBoost(entry.usage_count, entry.last_used_ms, now_ms);
347}
348
350 std::string_view name) {
351 std::string lower = ToLower(name);
352 // "(Alt)" marks alternate bindings of the same action.
353 for (size_t pos = lower.find("(alt)"); pos != std::string::npos;
354 pos = lower.find("(alt)")) {
355 lower.erase(pos, 5);
356 }
357 std::vector<std::string> words;
358 std::string current;
359 for (char c : lower) {
360 if (IsAlnum(c)) {
361 current.push_back(c);
362 } else if (!current.empty()) {
363 words.push_back(std::move(current));
364 current.clear();
365 }
366 }
367 if (!current.empty())
368 words.push_back(std::move(current));
369 std::string out;
370 for (auto& word : words) {
371 // Legacy "Panel" names are aliases of the "Window" names.
372 if (word == "panel")
373 word = "window";
374 else if (word == "panels")
375 word = "windows";
376 if (!out.empty())
377 out.push_back(' ');
378 out += word;
379 }
380 return out;
381}
382
383/*static*/ bool CommandPalette::IsInternalCommandId(std::string_view name) {
384 if (name.empty())
385 return true;
386 for (char c : name) {
387 if (std::isspace(static_cast<unsigned char>(c)))
388 return false;
389 }
390 return name.find('.') != std::string_view::npos ||
391 name.find('_') != std::string_view::npos ||
392 std::islower(static_cast<unsigned char>(name.front())) != 0;
393}
394
395std::vector<CommandEntry> CommandPalette::GetVisibleCommands() const {
396 std::vector<const CommandEntry*> sorted;
397 sorted.reserve(commands_.size());
398 for (const auto& [name, entry] : commands_) {
399 if (IsInternalCommandId(entry.name))
400 continue;
401 sorted.push_back(&entry);
402 }
403 std::sort(sorted.begin(), sorted.end(),
404 [](const CommandEntry* a, const CommandEntry* b) {
405 return a->name < b->name;
406 });
407
408 // Lower rank wins the representative slot: palette-native before
409 // ShortcutManager mirrors, then "Window" names before legacy "Panel"
410 // aliases (the Window names own the chords).
411 auto rank_of = [](const CommandEntry& entry) {
412 int rank = 0;
413 if (entry.provider_id == kShortcutsProviderId)
414 rank += 2;
415 if (HasPanelWord(entry.name))
416 rank += 1;
417 return rank;
418 };
419
420 struct Group {
421 const CommandEntry* rep = nullptr;
422 int rep_rank = 0;
423 std::string live_shortcut; // from the shortcuts provider
424 int live_rank = 0;
425 std::string other_shortcut; // first static hint
426 std::function<void()> fallback_callback;
427 int usage_count = 0;
428 int64_t last_used_ms = 0;
429 };
430 std::vector<std::string> order;
431 std::unordered_map<std::string, Group> groups;
432 for (const CommandEntry* entry : sorted) {
433 const std::string key = NormalizeCommandName(entry->name);
434 auto [it, inserted] = groups.try_emplace(key);
435 if (inserted)
436 order.push_back(key);
437 Group& group = it->second;
438 const bool is_shortcut = entry->provider_id == kShortcutsProviderId;
439 const int rank = rank_of(*entry);
440 if (!group.rep || rank < group.rep_rank) {
441 group.rep = entry;
442 group.rep_rank = rank;
443 }
444 if (!entry->shortcut.empty()) {
445 if (is_shortcut) {
446 if (group.live_shortcut.empty() || rank < group.live_rank) {
447 group.live_shortcut = entry->shortcut;
448 group.live_rank = rank;
449 }
450 } else if (group.other_shortcut.empty()) {
451 group.other_shortcut = entry->shortcut;
452 }
453 }
454 if (!group.fallback_callback && entry->callback)
455 group.fallback_callback = entry->callback;
456 group.usage_count = std::max(group.usage_count, entry->usage_count);
457 group.last_used_ms = std::max(group.last_used_ms, entry->last_used_ms);
458 }
459
460 std::vector<CommandEntry> result;
461 result.reserve(order.size());
462 for (const auto& key : order) {
463 const Group& group = groups[key];
464 CommandEntry entry = *group.rep;
465 if (!group.live_shortcut.empty()) {
466 entry.shortcut = group.live_shortcut;
467 } else if (entry.shortcut.empty()) {
468 entry.shortcut = group.other_shortcut;
469 }
470 if (!entry.callback)
471 entry.callback = group.fallback_callback;
472 entry.usage_count = group.usage_count;
473 entry.last_used_ms = group.last_used_ms;
474 result.push_back(std::move(entry));
475 }
476 return result;
477}
478
480 const std::string& name) const {
481 if (name.empty())
482 return {};
483 const std::string key = NormalizeCommandName(name);
484 for (const auto& entry : GetVisibleCommands()) {
485 if (NormalizeCommandName(entry.name) == key)
486 return entry.name;
487 }
488 return {};
489}
490
491std::vector<CommandMatch> CommandPalette::Search(const std::string& query,
492 int64_t now_ms) const {
493 std::vector<CommandMatch> matches;
494 const bool empty_query = SplitWhitespace(query).empty();
495 for (auto& entry : GetVisibleCommands()) {
496 int score = 0;
497 if (empty_query) {
498 score = 1 + FrecencyBoost(entry.usage_count, entry.last_used_ms, now_ms);
499 } else {
500 score = ScoreEntry(entry, query, now_ms);
501 }
502 if (score > 0)
503 matches.push_back({std::move(entry), score});
504 }
505 // Stable tiebreak: GetVisibleCommands() is name-sorted, stable_sort keeps
506 // that order among equal scores.
507 std::stable_sort(matches.begin(), matches.end(),
508 [](const CommandMatch& a, const CommandMatch& b) {
509 return a.score > b.score;
510 });
511 return matches;
512}
513
514std::vector<CommandMatch> CommandPalette::Search(
515 const std::string& query) const {
516 return Search(query, NowMs());
517}
518
519std::vector<CommandEntry> CommandPalette::SearchCommands(
520 const std::string& query) {
521 std::vector<CommandEntry> results;
522 for (auto& match : Search(query)) {
523 results.push_back(std::move(match.entry));
524 }
525 return results;
526}
527
528std::vector<CommandEntry> CommandPalette::GetRecentCommands(int limit) {
529 std::vector<CommandEntry> recent;
530 for (auto& entry : GetVisibleCommands()) {
531 if (entry.usage_count > 0)
532 recent.push_back(std::move(entry));
533 }
534 std::stable_sort(recent.begin(), recent.end(),
535 [](const CommandEntry& a, const CommandEntry& b) {
536 return a.last_used_ms > b.last_used_ms;
537 });
538 if (recent.size() > static_cast<size_t>(limit))
539 recent.resize(limit);
540 return recent;
541}
542
543std::vector<CommandEntry> CommandPalette::GetFrequentCommands(int limit) {
544 std::vector<CommandEntry> frequent;
545 for (auto& entry : GetVisibleCommands()) {
546 if (entry.usage_count > 0)
547 frequent.push_back(std::move(entry));
548 }
549 std::stable_sort(frequent.begin(), frequent.end(),
550 [](const CommandEntry& a, const CommandEntry& b) {
551 return a.usage_count > b.usage_count;
552 });
553 if (frequent.size() > static_cast<size_t>(limit))
554 frequent.resize(limit);
555 return frequent;
556}
557
558void CommandPalette::SaveHistory(const std::string& filepath) {
559 try {
560 yaze::Json j;
561 j["version"] = 1;
562 j["commands"] = yaze::Json::object();
563
564 for (const auto& [name, stats] : usage_) {
565 if (stats.count > 0) {
566 yaze::Json cmd;
567 cmd["usage_count"] = stats.count;
568 cmd["last_used_ms"] = stats.last_used_ms;
569 j["commands"][name] = cmd;
570 }
571 }
572
573 std::ofstream file(filepath);
574 if (file.is_open()) {
575 file << j.dump(2);
576 LOG_DEBUG("CommandPalette", "Saved command history to %s",
577 filepath.c_str());
578 }
579 } catch (const std::exception& e) {
580 LOG_ERROR("CommandPalette", "Failed to save command history: %s", e.what());
581 }
582}
583
584void CommandPalette::LoadHistory(const std::string& filepath) {
585 if (!std::filesystem::exists(filepath)) {
586 return;
587 }
588
589 try {
590 std::ifstream file(filepath);
591 if (!file.is_open()) {
592 return;
593 }
594
595 std::string content((std::istreambuf_iterator<char>(file)),
596 std::istreambuf_iterator<char>());
597 yaze::Json j = yaze::Json::parse(content);
598
599 if (!j.contains("commands") || !j["commands"].is_object()) {
600 return;
601 }
602
603 int loaded = 0;
604 for (auto& [name, cmd_json] : j["commands"].items()) {
605 UsageStats stats;
606 stats.count = cmd_json.value("usage_count", 0);
607 stats.last_used_ms = cmd_json.value("last_used_ms", int64_t{0});
608 if (stats.count <= 0)
609 continue;
610 usage_[name] = stats;
611 ++generation_;
612 if (auto it = commands_.find(name); it != commands_.end()) {
613 it->second.usage_count = stats.count;
614 it->second.last_used_ms = stats.last_used_ms;
615 }
616 loaded++;
617 }
618
619 LOG_INFO("CommandPalette", "Loaded %d command history entries from %s",
620 loaded, filepath.c_str());
621 } catch (const std::exception& e) {
622 LOG_ERROR("CommandPalette", "Failed to load command history: %s", e.what());
623 }
624}
625
626std::vector<CommandEntry> CommandPalette::GetAllCommands() const {
627 std::vector<CommandEntry> result;
628 result.reserve(commands_.size());
629 for (const auto& [name, entry] : commands_) {
630 result.push_back(entry);
631 }
632 return result;
633}
634
636 WorkspaceWindowManager* window_manager, size_t session_id) {
637 if (!window_manager)
638 return;
639
640 for (const auto& base_id : window_manager->GetWindowsInSession(session_id)) {
641 const auto* descriptor =
642 window_manager->GetWindowDescriptor(session_id, base_id);
643 if (!descriptor) {
644 continue;
645 }
646
647 // Create show command
648 std::string show_name =
649 absl::StrFormat("Show: %s", descriptor->display_name);
650 std::string show_desc =
651 absl::StrFormat("Open the %s window", descriptor->display_name);
652
653 AddCommand(show_name, CommandCategory::kPanel, show_desc,
654 descriptor->shortcut_hint,
655 [window_manager, base_id, session_id]() {
656 window_manager->OpenWindow(session_id, base_id);
657 });
658
659 // Create hide command
660 std::string hide_name =
661 absl::StrFormat("Hide: %s", descriptor->display_name);
662 std::string hide_desc =
663 absl::StrFormat("Close the %s window", descriptor->display_name);
664
665 AddCommand(hide_name, CommandCategory::kPanel, hide_desc, "",
666 [window_manager, base_id, session_id]() {
667 window_manager->CloseWindow(session_id, base_id);
668 });
669
670 // Create toggle command (legacy name kept for muscle memory / docs)
671 std::string toggle_name =
672 absl::StrFormat("Toggle: %s", descriptor->display_name);
673 std::string toggle_desc = absl::StrFormat("Toggle the %s window visibility",
674 descriptor->display_name);
675
676 auto toggle_fn = [window_manager, base_id, session_id]() {
677 window_manager->ToggleWindow(session_id, base_id);
678 };
679
680 AddCommand(toggle_name, CommandCategory::kPanel, toggle_desc, "",
681 toggle_fn);
682
683 // Window Finder selects a destination, rather than toggling its visibility.
684 // Keep the explicit Toggle command above for open/close actions.
685 std::string window_name =
686 absl::StrFormat("window: %s", descriptor->display_name);
688 window_name, CommandCategory::kPanel, show_desc,
689 descriptor->shortcut_hint, [window_manager, base_id, session_id]() {
690 WindowHost host(window_manager);
691 const bool opened = ImGui::GetCurrentContext() != nullptr
692 ? host.OpenAndFocusWindow(session_id, base_id)
693 : host.OpenWindow(session_id, base_id);
694 if (opened) {
695 window_manager->MarkWindowRecentlyUsed(base_id);
696 }
697 });
698
699 // Pin-to-global toggle. Mirrors the sidebar right-click pin + the panel
700 // tab pin UI, so users who live in the command palette never need to
701 // reach for the sidebar just to make a panel survive editor switches.
702 std::string pin_toggle_name =
703 absl::StrFormat("Toggle Pin: %s", descriptor->display_name);
704 std::string pin_toggle_desc =
705 absl::StrFormat("Keep the %s window visible across editor switches",
706 descriptor->display_name);
707
708 AddCommand(pin_toggle_name, CommandCategory::kPanel, pin_toggle_desc, "",
709 [window_manager, base_id, session_id]() {
710 const bool pinned =
711 window_manager->IsWindowPinned(session_id, base_id);
712 window_manager->SetWindowPinned(session_id, base_id, !pinned);
713 });
714 }
715}
716
717void CommandPalette::RegisterDrawerCommands(
718 std::function<void(int drawer_type)> toggle_callback,
719 std::function<void()> cycle_next, std::function<void()> cycle_prev) {
720 if (!toggle_callback)
721 return;
722
723 for (const auto& entry : GetDrawerCatalog()) {
724 if (!entry.name)
725 continue;
726
727 const int type_as_int = static_cast<int>(entry.type);
728 std::string name = absl::StrFormat("drawer: %s", entry.name);
729 std::string desc =
730 absl::StrFormat("Toggle the %s right drawer", entry.name);
731
732 AddCommand(
733 name, CommandCategory::kDrawer, desc, /*shortcut=*/"",
734 [toggle_callback, type_as_int]() { toggle_callback(type_as_int); });
735 }
736
737 if (cycle_next) {
738 AddCommand("drawer: Next", CommandCategory::kDrawer,
739 "Cycle to the next right drawer", "", std::move(cycle_next));
740 }
741 if (cycle_prev) {
742 AddCommand("drawer: Previous", CommandCategory::kDrawer,
743 "Cycle to the previous right drawer", "", std::move(cycle_prev));
744 }
745}
746
747void CommandPalette::RegisterEditorCommands(
748 std::function<void(const std::string&)> switch_callback) {
749 // Get all editor categories
750 auto categories = EditorRegistry::GetAllEditorCategories();
751
752 for (const auto& category : categories) {
753 std::string name = absl::StrFormat("Switch to: %s Editor", category);
754 std::string desc =
755 absl::StrFormat("Switch to the %s editor category", category);
756
757 AddCommand(name, CommandCategory::kEditor, desc, "",
758 [switch_callback, category]() { switch_callback(category); });
759 }
760}
761
762void CommandPalette::RegisterLayoutCommands(
763 std::function<void(const std::string&)> apply_callback) {
764 struct ProfileInfo {
765 const char* id;
766 const char* name;
767 const char* description;
768 };
769
770 static const ProfileInfo profiles[] = {
771 {"code", "Code", "Focused editing workspace with minimal panel noise"},
772 {"debug", "Debug",
773 "Debugger-first workspace for tracing and memory tools"},
774 {"mapping", "Mapping",
775 "Map-centric workspace for overworld/dungeon flows"},
776 {"chat", "Chat + Agent",
777 "Agent collaboration workspace with chat-centric layout"},
778 };
779
780 for (const auto& profile : profiles) {
781 std::string name = absl::StrFormat("Apply Profile: %s", profile.name);
782 auto apply_fn = [apply_callback, profile_id = std::string(profile.id)]() {
783 apply_callback("profile:" + profile_id);
784 };
785 AddCommand(name, CommandCategory::kLayout, profile.description, "",
786 apply_fn);
787 AddCommand(absl::StrFormat("layout: profile %s", profile.name),
788 CommandCategory::kLayout, profile.description, "", apply_fn);
789 }
790
791 AddCommand("Capture Layout Snapshot", CommandCategory::kLayout,
792 "Capture current layout as temporary session snapshot", "",
793 [apply_callback]() { apply_callback("session:capture"); });
794 AddCommand("layout: capture snapshot", CommandCategory::kLayout,
795 "Capture current layout as temporary session snapshot", "",
796 [apply_callback]() { apply_callback("session:capture"); });
797 AddCommand("Restore Layout Snapshot", CommandCategory::kLayout,
798 "Restore temporary session snapshot", "",
799 [apply_callback]() { apply_callback("session:restore"); });
800 AddCommand("layout: restore snapshot", CommandCategory::kLayout,
801 "Restore temporary session snapshot", "",
802 [apply_callback]() { apply_callback("session:restore"); });
803 AddCommand("Clear Layout Snapshot", CommandCategory::kLayout,
804 "Clear temporary session snapshot", "",
805 [apply_callback]() { apply_callback("session:clear"); });
806 AddCommand("layout: clear snapshot", CommandCategory::kLayout,
807 "Clear temporary session snapshot", "",
808 [apply_callback]() { apply_callback("session:clear"); });
809
810 // Legacy named workspace presets
811 struct PresetInfo {
812 const char* name;
813 const char* description;
814 };
815
816 static const PresetInfo presets[] = {
817 {"Minimal", "Minimal workspace with essential panels only"},
818 {"Developer", "Debug-focused layout with emulator and memory tools"},
819 {"Designer", "Visual-focused layout for graphics and palette editing"},
820 {"Modder", "Full-featured layout with all panels available"},
821 {"Overworld Expert", "Optimized layout for overworld editing"},
822 {"Dungeon Expert", "Optimized layout for dungeon editing"},
823 {"Testing", "QA-focused layout with testing tools"},
824 {"Audio", "Music and sound editing focused layout"},
825 {"Logic Debugger", "Debug and development focused layout"},
826 {"Overworld Artist", "Visual and overworld focused layout"},
827 {"Dungeon Master", "Comprehensive dungeon editing layout"},
828 {"Audio Engineer", "Music and sound editing layout"},
829 };
830
831 for (const auto& preset : presets) {
832 std::string name = absl::StrFormat("Apply Layout: %s", preset.name);
833 auto apply_fn = [apply_callback, preset_name = std::string(preset.name)]() {
834 apply_callback(preset_name);
835 };
836
837 AddCommand(name, CommandCategory::kLayout, preset.description, "",
838 apply_fn);
839 AddCommand(absl::StrFormat("layout: %s", preset.name),
840 CommandCategory::kLayout, preset.description, "", apply_fn);
841 }
842
843 // Reset to default layout command
844 AddCommand("Reset Layout: Default", CommandCategory::kLayout,
845 "Reset to the default layout for current editor", "",
846 [apply_callback]() { apply_callback("Default"); });
847 AddCommand("layout: Default", CommandCategory::kLayout,
848 "Reset to the default layout for current editor", "",
849 [apply_callback]() { apply_callback("Default"); });
850}
851
852void CommandPalette::RegisterRecentFilesCommands(
853 std::function<void(const std::string&)> open_callback) {
854 const auto& recent_files =
856
857 for (const auto& filepath : recent_files) {
858 // Skip files that no longer exist
859 if (!std::filesystem::exists(filepath)) {
860 continue;
861 }
862
863 // Extract just the filename for display
864 std::filesystem::path path(filepath);
865 std::string filename = path.filename().string();
866
867 std::string name = absl::StrFormat("Open Recent: %s", filename);
868 std::string desc = absl::StrFormat("Open file %s", filepath);
869
870 AddCommand(name, CommandCategory::kFile, desc, "",
871 [open_callback, filepath]() { open_callback(filepath); });
872 }
873}
874
875void CommandPalette::RegisterOverworldMapCommands(size_t session_id) {
876 for (int map_id = 0; map_id < zelda3::kNumOverworldMaps; ++map_id) {
877 const std::string label = zelda3::GetOverworldMapLabel(map_id);
878 const std::string map_name =
879 label.empty() ? absl::StrFormat("Map %02X", map_id) : label;
880 const std::string name =
881 absl::StrFormat("Overworld: Open Map [%02X] %s", map_id, map_name);
882 const std::string desc =
883 absl::StrFormat("Jump to overworld map %02X", map_id);
884 AddCommand(
885 name, CommandCategory::kNavigation, desc, "", [map_id, session_id]() {
886 if (auto* bus = ContentRegistry::Context::event_bus()) {
887 bus->Publish(JumpToMapRequestEvent::Create(map_id, session_id));
888 }
889 });
890 }
891}
892
893void CommandPalette::RegisterDungeonRoomCommands(size_t session_id) {
894 constexpr int kTotalRooms = 0x128;
895 for (int room_id = 0; room_id < kTotalRooms; ++room_id) {
896 const std::string label = zelda3::GetRoomLabel(room_id);
897 const std::string room_name =
898 label.empty() ? absl::StrFormat("Room %03X", room_id) : label;
899
900 const std::string name =
901 absl::StrFormat("Dungeon: Open Room [%03X] %s", room_id, room_name);
902 const std::string desc =
903 absl::StrFormat("Jump to dungeon room %03X", room_id);
904
905 AddCommand(
906 name, CommandCategory::kNavigation, desc, "", [room_id, session_id]() {
907 if (auto* bus = ContentRegistry::Context::event_bus()) {
908 bus->Publish(JumpToRoomRequestEvent::Create(room_id, session_id));
909 }
910 });
911 }
912}
913
914void CommandPalette::RegisterWorkflowCommands(
915 WorkspaceWindowManager* window_manager, size_t session_id) {
916 if (window_manager) {
917 const auto categories = window_manager->GetAllCategories(session_id);
918 for (const auto& category : categories) {
919 for (const auto& descriptor :
920 window_manager->GetWindowsInCategory(session_id, category)) {
921 if (descriptor.workflow_group.empty()) {
922 continue;
923 }
924 if (descriptor.enabled_condition && !descriptor.enabled_condition()) {
925 continue;
926 }
927 const std::string label = descriptor.workflow_label.empty()
928 ? descriptor.display_name
929 : descriptor.workflow_label;
930 const std::string group = descriptor.workflow_group.empty()
931 ? std::string("General")
932 : descriptor.workflow_group;
933 const std::string description =
934 descriptor.workflow_description.empty()
935 ? absl::StrFormat("Open %s", descriptor.display_name)
936 : descriptor.workflow_description;
937 AddCommand(
938 absl::StrFormat("%s: %s", group, label), CommandCategory::kWorkflow,
939 description, descriptor.shortcut_hint,
940 [window_manager, session_id, panel_id = descriptor.card_id]() {
941 window_manager->OpenWindow(session_id, panel_id);
942 });
943 }
944 }
945 }
946
947 for (const auto& action : ContentRegistry::WorkflowActions::GetAll()) {
948 if (action.enabled && !action.enabled()) {
949 continue;
950 }
951 const std::string group =
952 action.group.empty() ? std::string("General") : action.group;
953 AddCommand(absl::StrFormat("%s: %s", group, action.label),
954 CommandCategory::kWorkflow, action.description, action.shortcut,
955 action.callback);
956 }
957}
958
959void CommandPalette::RegisterWelcomeCommands(
960 const RecentProjectsModel* model,
961 const std::vector<std::string>& template_names,
962 std::function<void(const std::string&)> remove_callback,
963 std::function<void(const std::string&)> toggle_pin_callback,
964 std::function<void()> undo_remove_callback,
965 std::function<void()> clear_recents_callback,
966 std::function<void(const std::string&)> create_from_template_callback,
967 std::function<void()> dismiss_welcome_callback,
968 std::function<void()> show_welcome_callback) {
969 // Per-entry remove/pin commands. Keeping the palette fully driven by the
970 // same model that powers the welcome screen means this list stays in sync
971 // with pins/renames automatically on the next RefreshCommands().
972 if (model) {
973 for (const auto& entry : model->entries()) {
974 if (entry.unavailable)
975 continue; // Platform-gated; skip silently.
976 const std::string label = entry.display_name_override.empty()
977 ? entry.name
978 : entry.display_name_override;
979 const std::string path = entry.filepath;
980
981 if (remove_callback) {
982 const std::string name =
983 absl::StrFormat("Welcome: Remove Recent \"%s\"", label);
984 const std::string desc =
985 absl::StrFormat("Remove %s from the welcome screen's recents list.",
986 entry.filepath);
987 AddCommand(name, CommandCategory::kFile, desc, "",
988 [remove_callback, path]() { remove_callback(path); });
989 }
990 if (toggle_pin_callback) {
991 const std::string name = absl::StrFormat(
992 "Welcome: %s Recent \"%s\"", entry.pinned ? "Unpin" : "Pin", label);
993 const std::string desc =
994 absl::StrFormat("%s %s on the welcome screen.",
995 entry.pinned ? "Unpin" : "Pin", entry.filepath);
996 AddCommand(
997 name, CommandCategory::kFile, desc, "",
998 [toggle_pin_callback, path]() { toggle_pin_callback(path); });
999 }
1000 }
1001 }
1002
1003 if (clear_recents_callback) {
1004 AddCommand("Welcome: Clear Recent Files", CommandCategory::kFile,
1005 "Forget every entry in the welcome screen's recent list.", "",
1006 clear_recents_callback);
1007 }
1008
1009 if (undo_remove_callback) {
1010 AddCommand("Welcome: Undo Last Recent Removal", CommandCategory::kFile,
1011 "Restore the last recent-project entry removed via Forget.", "",
1012 undo_remove_callback);
1013 }
1014
1015 if (create_from_template_callback) {
1016 for (const auto& template_name : template_names) {
1017 if (template_name.empty())
1018 continue;
1019 const std::string name = absl::StrFormat(
1020 "Welcome: Create Project from Template: %s", template_name);
1021 const std::string desc = absl::StrFormat(
1022 "Start a new project using the \"%s\" template.", template_name);
1023 AddCommand(name, CommandCategory::kFile, desc, "",
1024 [create_from_template_callback, template_name]() {
1025 create_from_template_callback(template_name);
1026 });
1027 }
1028 }
1029
1030 if (show_welcome_callback) {
1031 AddCommand("Welcome: Show Welcome Screen", CommandCategory::kView,
1032 "Bring back the welcome screen if it's been dismissed.", "",
1033 show_welcome_callback);
1034 }
1035 if (dismiss_welcome_callback) {
1036 AddCommand("Welcome: Dismiss Welcome Screen", CommandCategory::kView,
1037 "Hide the welcome screen for the rest of this session.", "",
1038 dismiss_welcome_callback);
1039 }
1040}
1041
1042} // namespace editor
1043} // 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 SaveHistory(const std::string &filepath)
Save command usage history to disk.
std::string CanonicalCommandName(const std::string &name) const
void Clear()
Clear all commands (and forget every registered provider). Usage history is kept so refreshing provid...
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 (raw, not deduplicated)
static std::string NormalizeCommandName(std::string_view name)
std::unordered_map< std::string, CommandEntry > commands_
void RegisterProvider(std::unique_ptr< CommandProvider > provider)
static int ScoreEntry(const CommandEntry &entry, std::string_view query, int64_t now_ms)
static int ScoreText(std::string_view text, std::string_view query)
std::vector< CommandEntry > GetRecentCommands(int limit=10)
std::vector< CommandEntry > GetVisibleCommands() const
bool RecordUsage(const std::string &name)
void RefreshProviders()
Remove-and-re-run every registered provider.
static bool IsInternalCommandId(std::string_view name)
std::vector< std::unique_ptr< CommandProvider > > providers_
std::vector< CommandMatch > Search(const std::string &query, int64_t now_ms) const
std::unordered_map< std::string, UsageStats > usage_
Usage keyed by command name; survives Clear() and provider refreshes.
std::vector< CommandEntry > GetFrequentCommands(int limit=10)
static int FuzzyScore(const std::string &text, const std::string &query)
Legacy name for ScoreText (kept for the Window Finder).
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()
Definition project.h:598
const std::vector< std::string > & GetRecentFiles() const
Definition project.h:635
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
bool IsWordStartAt(const std::string &text, size_t index)
int BestSubsequenceScore(const std::string &text, const std::string &query)
std::vector< std::string > SplitWhitespace(const std::string &text)
int ScoreSingle(const std::string &text, const std::string &query)
int FrecencyBoost(int usage_count, int64_t last_used_ms, int64_t now_ms)
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.
constexpr int kNumOverworldMaps
Definition common.h:85
std::string GetOverworldMapLabel(int id)
Convenience function to get an overworld map label.
static constexpr const char * kPanel
std::function< void()> callback
A search hit: the (deduplicated) entry plus the score it matched with.