Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
47 kB · 1256 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257#include "Commands.h"
#include <QGuiApplication>#include <QJSValue>#include <QKeySequence>#include <QMetaObject>#include <QQmlEngine>
#include <ranges>
#include "AppModel.h"#include "PanePool.h"#include "TerminalQuickItem.h"#include "Workspace.h"#include "XenoSettings.h"
// ---------------------------------------------------------------------------// CommandDef// ---------------------------------------------------------------------------
CommandDef::CommandDef(QString commandId, QString title, QString category, QStringList menus, QObject *parent) : QObject(parent), m_commandId(std::move(commandId)), m_title(std::move(title)), m_category(std::move(category)), m_menus(std::move(menus)) {}
void CommandDef::setEnabled(bool enabled) { if (m_enabled == enabled) { return; } m_enabled = enabled; emit enabledChanged();}
QString CommandDef::shortcutHint() const { if (m_sequence.isEmpty()) { return {}; } // On macOS "Ctrl" IS the Cmd key (Qt's modifier mapping); render the // human display form accordingly.#ifdef Q_OS_MACOS QString hint = m_sequence; return hint.replace(QStringLiteral("Ctrl"), QStringLiteral("Cmd"));#else return m_sequence;#endif}
CommandDef *CommandDef::setMenuLabel(QString label) { m_menuLabel = std::move(label); return this;}void CommandDef::applySequence(QString sequence) { if (m_sequence == sequence) { return; } m_sequence = std::move(sequence); emit sequenceChanged();}
CommandDef *CommandDef::setSequence(QString sequence) { m_defaultSequence = sequence; m_sequence = std::move(sequence); return this;}CommandDef *CommandDef::setIntercept(bool intercept) { m_intercept = intercept; return this;}CommandDef *CommandDef::setSeparators(QStringList separators) { m_separators = std::move(separators); return this;}CommandDef *CommandDef::setPaletteVisible(bool visible) { m_paletteVisible = visible; return this;}CommandDef *CommandDef::setTarget(QString target) { m_target = std::move(target); return this;}
// ---------------------------------------------------------------------------// Commands singleton// ---------------------------------------------------------------------------
Commands *Commands::s_instance = nullptr;
Commands::Commands(QObject *parent) : QObject(parent) { s_instance = this; ensureModelConnected();
// --- Clipboard ------------------------------------------------------------ // The QML registry derived copy's global `enabled` from the focused pane's // hasSelection, but selection changes do not notify the model, so the // binding could go stale. Keep both enabled=true globally; the ctx-gated // commandEnabledFor decides (menu refresh calls commandEnabledFor before // showing). // clang-format off auto* copy = new CommandDef( QStringLiteral("copy"), tr("Copy"), QStringLiteral("clipboard"), {QStringLiteral("edit"), QStringLiteral("pane")}, this); copy->setSequence(QStringLiteral("Ctrl+C"))->setIntercept(false) ->setTarget(QStringLiteral("terminalPage")); // clang-format on m_commands.append(copy);
auto *paste = new CommandDef( QStringLiteral("paste"), tr("Paste"), QStringLiteral("clipboard"), {QStringLiteral("edit"), QStringLiteral("pane")}, this); paste->setSequence(QStringLiteral("Ctrl+V")) ->setIntercept(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(paste);
// --- Pane splitting // --------------------------------------------------------- auto *splitHorizontal = new CommandDef(QStringLiteral("splitHorizontal"), tr("Split Horizontal"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); splitHorizontal->setSequence(QStringLiteral("Ctrl+Shift+H")) ->setSeparators({QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(splitHorizontal);
auto *splitVertical = new CommandDef(QStringLiteral("splitVertical"), tr("Split Vertical"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); splitVertical->setSequence(QStringLiteral("Ctrl+Shift+V")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(splitVertical);
auto *splitWithTerminal = new CommandDef(QStringLiteral("splitWithTerminal"), tr("Split with Terminal"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); splitWithTerminal->setSequence(QStringLiteral("Ctrl+Shift+T")) ->setSeparators({QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(splitWithTerminal);
auto *splitWithEditor = new CommandDef(QStringLiteral("splitWithEditor"), tr("Split with Editor"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); splitWithEditor->setSequence(QStringLiteral("Ctrl+Shift+E")) ->setSeparators({QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(splitWithEditor);
// --- Pane lifecycle // --------------------------------------------------------- auto *closePane = new CommandDef( QStringLiteral("closePane"), tr("Close Pane"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); closePane->setSequence(QStringLiteral("Ctrl+Alt+W")) ->setSeparators({QStringLiteral("view"), QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(closePane);
auto *keepOnlyPane = new CommandDef(QStringLiteral("keepOnlyPane"), tr("Keep Only This Pane"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); keepOnlyPane->setSequence(QStringLiteral("Ctrl+Alt+O")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(keepOnlyPane);
auto *focusNextPane = new CommandDef(QStringLiteral("focusNextPane"), tr("Focus Next Pane"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); focusNextPane->setSequence(QStringLiteral("Ctrl+]")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(focusNextPane);
// --- Pane zoom ------------------------------------------------------------ // One per-tab zoom state on XenoTab; maximize is a zoom that also // re-asserts focus (they share the model state, two spellings of the // intent). globalEnabled's "pane"-category gate (hasFocus) is exactly // "a zoom is possible or active": a zoom needs a focused pane, and an // active zoom implies one (the zoomed pane keeps focus). auto *togglePaneZoom = new CommandDef(QStringLiteral("togglePaneZoom"), tr("Toggle Pane Zoom"), QStringLiteral("pane"), {QStringLiteral("view"), QStringLiteral("pane")}, this); togglePaneZoom->setSequence(QStringLiteral("Ctrl+Z")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(togglePaneZoom);
auto *maximizePane = new CommandDef(QStringLiteral("maximizePane"), tr("Maximize Pane"), QStringLiteral("pane"), {QStringLiteral("pane")}, this); maximizePane->setSequence(QStringLiteral("Ctrl+Shift+Z")) ->setSeparators({QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(maximizePane);
// --- Pane focus movement (shortcuts + palette only) // -------------------------- auto *focusPaneLeft = new CommandDef(QStringLiteral("focusPaneLeft"), tr("Focus Pane Left"), QStringLiteral("pane"), {}, this); focusPaneLeft->setSequence(QStringLiteral("Ctrl+Left")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(focusPaneLeft);
auto *focusPaneRight = new CommandDef(QStringLiteral("focusPaneRight"), tr("Focus Pane Right"), QStringLiteral("pane"), {}, this); focusPaneRight->setSequence(QStringLiteral("Ctrl+Right")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(focusPaneRight);
auto *focusPaneUp = new CommandDef(QStringLiteral("focusPaneUp"), tr("Focus Pane Up"), QStringLiteral("pane"), {}, this); focusPaneUp->setSequence(QStringLiteral("Ctrl+Up")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(focusPaneUp);
auto *focusPaneDown = new CommandDef(QStringLiteral("focusPaneDown"), tr("Focus Pane Down"), QStringLiteral("pane"), {}, this); focusPaneDown->setSequence(QStringLiteral("Ctrl+Down")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(focusPaneDown);
// --- Pane swapping (shortcuts + palette only) // -------------------------------- auto *swapPaneLeft = new CommandDef(QStringLiteral("swapPaneLeft"), tr("Swap Pane Left"), QStringLiteral("pane"), {}, this); swapPaneLeft->setSequence(QStringLiteral("Ctrl+Shift+Left")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(swapPaneLeft);
auto *swapPaneRight = new CommandDef(QStringLiteral("swapPaneRight"), tr("Swap Pane Right"), QStringLiteral("pane"), {}, this); swapPaneRight->setSequence(QStringLiteral("Ctrl+Shift+Right")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(swapPaneRight);
auto *swapPaneUp = new CommandDef(QStringLiteral("swapPaneUp"), tr("Swap Pane Up"), QStringLiteral("pane"), {}, this); swapPaneUp->setSequence(QStringLiteral("Ctrl+Shift+Up")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(swapPaneUp);
auto *swapPaneDown = new CommandDef(QStringLiteral("swapPaneDown"), tr("Swap Pane Down"), QStringLiteral("pane"), {}, this); swapPaneDown->setSequence(QStringLiteral("Ctrl+Shift+Down")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(swapPaneDown);
// --- Terminal reset // ----------------------------------------------------------- auto *resetTerminal = new CommandDef(QStringLiteral("resetTerminal"), tr("Reset Terminal"), QStringLiteral("pane"), {QStringLiteral("edit"), QStringLiteral("pane")}, this); resetTerminal->setMenuLabel(tr("Reset")) ->setSeparators({QStringLiteral("edit"), QStringLiteral("pane")}) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(resetTerminal);
// --- Scrollback search ------------------------------------------------ // openSearch/closeSearch are pane-targeted UI decisions (the search bar // lives on the pane); next/previous route to the focused pane's item. auto *openSearch = new CommandDef(QStringLiteral("openSearch"), tr("Find in Terminal"), QStringLiteral("pane"), {}, this); openSearch->setSequence(QStringLiteral("Ctrl+F")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(openSearch);
auto *closeSearch = new CommandDef(QStringLiteral("closeSearch"), tr("Close Search"), QStringLiteral("pane"), {}, this); // Escape is handled by the search bar itself (Keys.onEscapePressed) and by // the copy-mode key path on the focused item. It must NOT enter the global // intercept whitelist, because that accepts the ShortcutOverride for every // Escape and stops the key from ever reaching the program in the pane. closeSearch->setSequence(QStringLiteral("Esc")) ->setIntercept(false) ->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(closeSearch);
auto *searchNext = new CommandDef(QStringLiteral("searchNext"), tr("Next Match"), QStringLiteral("pane"), {}, this); searchNext->setSequence(QStringLiteral("Ctrl+G")) ->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(searchNext);
auto *searchPrevious = new CommandDef(QStringLiteral("searchPrevious"), tr("Previous Match"), QStringLiteral("pane"), {}, this); searchPrevious->setSequence(QStringLiteral("Ctrl+Shift+G")) ->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(searchPrevious);
// --- Copy mode ----------------------------------------------------------- // enterCopyMode/exitCopyMode are pane-targeted UI decisions routed like // the search commands; move/select/yank route straight to the focused // pane's item (the mode's own key handling drives them normally). auto *enterCopyMode = new CommandDef(QStringLiteral("enterCopyMode"), tr("Copy Mode"), QStringLiteral("pane"), {}, this); enterCopyMode->setTarget(QStringLiteral("terminalPage")); m_commands.append(enterCopyMode);
auto *exitCopyMode = new CommandDef(QStringLiteral("exitCopyMode"), tr("Leave Copy Mode"), QStringLiteral("pane"), {}, this); exitCopyMode->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(exitCopyMode);
// --- Tabs ----------------------------------------------------------------- auto *newTab = new CommandDef( QStringLiteral("newTab"), tr("New Tab"), QStringLiteral("tab"), {QStringLiteral("file"), QStringLiteral("workspace")}, this); newTab->setSequence(QStringLiteral("Ctrl+N")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(newTab);
auto *closeTab = new CommandDef(QStringLiteral("closeTab"), tr("Close Tab"), QStringLiteral("tab"), {QStringLiteral("file")}, this); closeTab->setSequence(QStringLiteral("Ctrl+W")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(closeTab);
auto *nextTab = new CommandDef(QStringLiteral("nextTab"), tr("Next Tab"), QStringLiteral("tab"), {}, this); nextTab->setSequence(QStringLiteral("Ctrl+PgDown")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(nextTab);
auto *previousTab = new CommandDef(QStringLiteral("previousTab"), tr("Previous Tab"), QStringLiteral("tab"), {}, this); previousTab->setSequence(QStringLiteral("Ctrl+PgUp")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(previousTab);
// Tab reorder: move the ACTIVE tab one slot left/right inside its // workspace. No-op at the strip's ends; the enabled predicate below // mirrors the model's guards exactly. auto *moveTabLeft = new CommandDef(QStringLiteral("moveTabLeft"), tr("Move Tab Left"), QStringLiteral("tab"), {}, this); moveTabLeft->setSequence(QStringLiteral("Ctrl+Shift+PgUp")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(moveTabLeft);
auto *moveTabRight = new CommandDef(QStringLiteral("moveTabRight"), tr("Move Tab Right"), QStringLiteral("tab"), {}, this); moveTabRight->setSequence(QStringLiteral("Ctrl+Shift+PgDown")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(moveTabRight);
auto *renameTab = new CommandDef(QStringLiteral("renameTab"), tr("Rename Tab…"), QStringLiteral("tab"), {QStringLiteral("file")}, this); renameTab->setTarget(QStringLiteral("terminalPage")); m_commands.append(renameTab);
// --- Workspace navigation (palette + shortcuts) // ------------------------------- auto *previousWorkspace = new CommandDef( QStringLiteral("previousWorkspace"), tr("Previous Workspace"), QStringLiteral("workspace"), {}, this); previousWorkspace->setSequence(QStringLiteral("Alt+Up")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(previousWorkspace);
auto *nextWorkspace = new CommandDef(QStringLiteral("nextWorkspace"), tr("Next Workspace"), QStringLiteral("workspace"), {}, this); nextWorkspace->setSequence(QStringLiteral("Alt+Down")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(nextWorkspace);
// Jump-to-workspace N (1..9): paletteVisible=false; enabled when that // many workspaces exist. for (const int n : std::views::iota(1, 10)) { auto *jump = new CommandDef(QStringLiteral("selectWorkspace%1").arg(n), tr("Jump to Workspace %1").arg(n), QStringLiteral("workspace"), {}, this); jump->setSequence(QStringLiteral("Ctrl+%1").arg(n)) ->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(jump); }
// --- Fuzzy jump (Cmd+P) and the command palette (Cmd+Shift+P) // --------------- auto *jumpToPane = new CommandDef(QStringLiteral("jumpToPane"), tr("Jump to Pane"), QStringLiteral("workspace"), {}, this); jumpToPane->setSequence(QStringLiteral("Ctrl+P")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(jumpToPane);
auto *commandPalette = new CommandDef(QStringLiteral("commandPalette"), tr("Command Palette"), QStringLiteral("app"), {}, this); commandPalette->setSequence(QStringLiteral("Ctrl+Shift+P")) ->setPaletteVisible(false) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(commandPalette);
// --- View ----------------------------------------------------------------- auto *resetLayout = new CommandDef(QStringLiteral("resetLayout"), tr("Reset Layout"), QStringLiteral("view"), {QStringLiteral("file")}, this); resetLayout->setTarget(QStringLiteral("terminalPage")); m_commands.append(resetLayout);
auto *equalizeSplits = new CommandDef(QStringLiteral("equalizeSplits"), tr("Equalize Splits"), QStringLiteral("view"), {QStringLiteral("view")}, this); equalizeSplits->setTarget(QStringLiteral("terminalPage")); m_commands.append(equalizeSplits);
auto *toggleSidebar = new CommandDef(QStringLiteral("toggleSidebar"), tr("Toggle Sidebar"), QStringLiteral("view"), {}, this); toggleSidebar->setSequence(QStringLiteral("Ctrl+B")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(toggleSidebar);
auto *missionControl = new CommandDef(QStringLiteral("missionControl"), tr("Mission Control"), QStringLiteral("view"), {}, this); missionControl->setSequence(QStringLiteral("Ctrl+M")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(missionControl);
// --- Application ----------------------------------------------------------- auto *preferences = new CommandDef(QStringLiteral("preferences"), tr("Preferences…"), QStringLiteral("app"), {QStringLiteral("app")}, this); preferences->setSequence(QStringLiteral("Ctrl+,")) ->setTarget(QStringLiteral("terminalPage")); m_commands.append(preferences);
auto *quit = new CommandDef(QStringLiteral("quit"), tr("Quit Xenomorphic"), QStringLiteral("app"), {QStringLiteral("app")}, this); quit->setSequence(QStringLiteral("Ctrl+Q")) ->setSeparators({QStringLiteral("app")}) ->setTarget(QStringLiteral("app")); m_commands.append(quit);
// --- New workspace (opens the AddWorkspaceMenu dropdown) ------------------- auto *newWorkspace = new CommandDef(QStringLiteral("newWorkspace"), tr("New Workspace…"), QStringLiteral("add"), {QStringLiteral("file")}, this); newWorkspace->setSeparators({QStringLiteral("file")}) ->setPaletteVisible(false) ->setTarget(QStringLiteral("workspaces")); m_commands.append(newWorkspace);
// --- Add-workspace sources (AddWorkspaceMenu + workspace-row menu) // -------------------------------------------------------------------------- auto *addSameDirectory = new CommandDef(QStringLiteral("addSameDirectory"), tr("Same Directory"), QStringLiteral("add"), {QStringLiteral("add")}, this); addSameDirectory->setTarget(QStringLiteral("workspaces")); m_commands.append(addSameDirectory);
auto *addJjWorkspace = new CommandDef(QStringLiteral("addJjWorkspace"), tr("New jj Workspace"), QStringLiteral("add"), {QStringLiteral("add"), QStringLiteral("workspace")}, this); addJjWorkspace->setSeparators({QStringLiteral("workspace")}) ->setTarget(QStringLiteral("workspaces")); m_commands.append(addJjWorkspace);
auto *addGitWorktree = new CommandDef(QStringLiteral("addGitWorktree"), tr("New Git Worktree"), QStringLiteral("add"), {QStringLiteral("add"), QStringLiteral("workspace")}, this); addGitWorktree->setTarget(QStringLiteral("workspaces")); m_commands.append(addGitWorktree);
auto *addLocation = new CommandDef(QStringLiteral("addLocation"), tr("New Location…"), QStringLiteral("add"), {QStringLiteral("add")}, this); addLocation->setTarget(QStringLiteral("workspaces")); m_commands.append(addLocation);
// --- Close workspace // --------------------------------------------------------- Deletes the // active workspace; deleting the last workspace rebuilds a default one // (AppModel::deleteWorkspace never leaves zero workspaces). auto *closeWorkspace = new CommandDef( QStringLiteral("closeWorkspace"), tr("Close Workspace"), QStringLiteral("workspace"), {QStringLiteral("file")}, this); closeWorkspace->setTarget(QStringLiteral("workspaces")); m_commands.append(closeWorkspace);
// --- Workspace row commands (target the row the menu was opened on) -------- // Context is {index, sidebar}. Menus-only: paletteVisible=false and no // global shortcut. auto *workspaceRename = new CommandDef( QStringLiteral("workspaceRename"), tr("Rename"), QStringLiteral("workspace"), {QStringLiteral("workspace")}, this); workspaceRename->setPaletteVisible(false)->setTarget( QStringLiteral("workspaces")); m_commands.append(workspaceRename);
auto *workspaceSetDirectory = new CommandDef( QStringLiteral("workspaceSetDirectory"), tr("Set Directory"), QStringLiteral("workspace"), {QStringLiteral("workspace")}, this); workspaceSetDirectory->setPaletteVisible(false)->setTarget( QStringLiteral("workspaces")); m_commands.append(workspaceSetDirectory);
auto *workspaceForgetJj = new CommandDef( QStringLiteral("workspaceForgetJj"), tr("Forget jj Workspace"), QStringLiteral("workspace"), {QStringLiteral("workspace")}, this); workspaceForgetJj->setSeparators({QStringLiteral("workspace")}) ->setPaletteVisible(false) ->setTarget(QStringLiteral("workspaces")); m_commands.append(workspaceForgetJj);
auto *workspaceDelete = new CommandDef( QStringLiteral("workspaceDelete"), tr("Delete Workspace"), QStringLiteral("workspace"), {QStringLiteral("workspace")}, this); workspaceDelete->setSeparators({QStringLiteral("workspace")}) ->setPaletteVisible(false) ->setTarget(QStringLiteral("workspaces")); m_commands.append(workspaceDelete);
for (CommandDef *command : std::as_const(m_commands)) { m_byId.insert(command->commandId(), command); }
recomputeEnabled();
// Resolve effective sequences from the persisted overrides and keep // following them. ensureModelConnected retries via the event loop when // the AppModel singleton is not up yet, so applyOverrides also runs on // the (re)connect path. applyOverrides();
// The app-level command: quitting needs no controller. connect(this, &Commands::runCommand, this, [](const QString &commandId) { if (commandId != QLatin1String("quit")) { return; } if (QGuiApplication *gui = qGuiApp) { // Queued so the dispatch() caller unwinds before the loop exits. QMetaObject::invokeMethod(gui, &QCoreApplication::quit, Qt::QueuedConnection); } });}
Commands::~Commands() { if (s_instance == this) { s_instance = nullptr; }}
Commands *Commands::instance() { return s_instance; }
Commands *Commands::create(QQmlEngine *engine, QJSEngine * /*scriptEngine*/) { auto *commands = new Commands(engine); QQmlEngine::setObjectOwnership(commands, QQmlEngine::CppOwnership); return commands;}
// ---------------------------------------------------------------------------// Registry reads// ---------------------------------------------------------------------------
QVariantList Commands::commands() const { QVariantList out; out.reserve(m_commands.size()); for (CommandDef *command : m_commands) { out.append(QVariant::fromValue(command)); } return out;}
QVariantList Commands::shortcutCommands() const { QVariantList out; for (CommandDef *command : m_commands) { if (!command->sequence().isEmpty()) { out.append(QVariant::fromValue(command)); } } return out;}
QVariantList Commands::paletteCommands() const { QVariantList out; for (CommandDef *command : m_commands) { if (command->paletteVisible()) { out.append(QVariant::fromValue(command)); } } return out;}
QVariantList Commands::byMenu(const QString &menu) const { QVariantList out; for (CommandDef *command : m_commands) { if (command->menus().contains(menu)) { out.append(QVariant::fromValue(command)); } } return out;}
QStringList Commands::whitelistSequences() const { QStringList out; QSet<QString> seen; for (CommandDef *command : m_commands) { const QString &sequence = command->sequence(); if (command->intercept() && !sequence.isEmpty() && !seen.contains(sequence)) { seen.insert(sequence); out.append(sequence); } } return out;}
// ---------------------------------------------------------------------------// Sequence overrides// ---------------------------------------------------------------------------
void Commands::applyOverrides() { ensureModelConnected(); const QVariantMap overrides = (m_model != nullptr && m_model->settings() != nullptr) ? m_model->settings()->commandSequences() : QVariantMap(); bool changed = false; for (CommandDef *command : std::as_const(m_commands)) { const QString overridden = overrides.value(command->commandId()).toString(); const QString next = overridden.isEmpty() ? command->defaultSequence() : overridden; if (command->sequence() != next) { changed = true; } command->applySequence(next); } if (changed) { emit commandsChanged(); }}
QString Commands::commandForSequence(const QString &sequence) const { if (sequence.trimmed().isEmpty()) { return {}; } const QKeySequence wanted(sequence); if (wanted.isEmpty()) { return {}; } for (CommandDef *command : m_commands) { if (!command->sequence().isEmpty() && QKeySequence(command->sequence()) == wanted) { return command->commandId(); } } return {};}
bool Commands::setCommandSequence(const QString &commandId, const QString &sequence) { ensureModelConnected(); CommandDef *command = find(commandId); if (command == nullptr || (m_model == nullptr || m_model->settings() == nullptr)) { return false; } const QString trimmed = sequence.trimmed(); if (trimmed.isEmpty()) { // Clear: restore the built-in literal. m_model->settings()->clearCommandSequence(commandId); return true; } const QKeySequence wanted(trimmed); if (wanted.isEmpty()) { return false; } // Conflict rejection: the sequence must be free or already this // command's (re-binding to itself, or its own literal, is fine). for (CommandDef *other : std::as_const(m_commands)) { if (other != command && !other->sequence().isEmpty() && QKeySequence(other->sequence()) == wanted) { return false; } } m_model->settings()->setCommandSequence(commandId, trimmed); return true;}
void Commands::clearCommandSequence(const QString &commandId) { ensureModelConnected(); if (m_model != nullptr && m_model->settings() != nullptr) { m_model->settings()->clearCommandSequence(commandId); }}
void Commands::clearAllCommandSequences() { ensureModelConnected(); if (m_model != nullptr && m_model->settings() != nullptr) { m_model->settings()->clearAllCommandSequences(); }}
// ---------------------------------------------------------------------------// Per-target state// ---------------------------------------------------------------------------
CommandDef *Commands::find(const QString &commandId) const { return m_byId.value(commandId, nullptr);}
namespace {
// QML hands JS objects across the boundary as QJSValue-carrying variants// (var-property round-trips), not QVariantMaps. Unwrap once so every ctx// gate sees the map form.QVariant unwrapJsCtx(const QVariant &ctx) { if (ctx.metaType() == QMetaType::fromType<QJSValue>()) { return ctx.value<QJSValue>().toVariant(); } return ctx;}
} // namespace
int Commands::workspaceIndexFromCtx(const QVariant &ctx) { const QVariant unwrapped = unwrapJsCtx(ctx); if (!unwrapped.isValid() || unwrapped.isNull()) { return -1; } if (unwrapped.metaType() == QMetaType::fromType<QVariantMap>()) { return unwrapped.toMap().value(QStringLiteral("index"), -1).toInt(); } if (unwrapped.canConvert<QVariantMap>()) { return unwrapped.toMap().value(QStringLiteral("index"), -1).toInt(); } return -1;}
QString Commands::paneIdFromCtx(const QVariant &ctx) { const QVariant unwrapped = unwrapJsCtx(ctx); if (!unwrapped.isValid() || unwrapped.isNull()) { return {}; } // Pane contexts are the TerminalQuickItem itself. if (auto *term = unwrapped.value<TerminalQuickItem *>()) { return PanePool::instance() != nullptr ? PanePool::instance()->paneIdOf(term) : QString(); } if (unwrapped.metaType() == QMetaType::fromType<QVariantMap>()) { return unwrapped.toMap().value(QStringLiteral("paneId")).toString(); } return {};}
bool Commands::globalEnabled(const CommandDef &command) const { if (m_model == nullptr) { return true; } const bool hasFocus = !m_model->focusedPaneId().isEmpty(); const qsizetype workspaceCount = m_model->workspaces().size();
if (command.commandId() == QLatin1String("copy") || command.commandId() == QLatin1String("paste")) { // See the constructor note: selection state does not notify the model; // keep these globally enabled, the ctx gate decides. return true; } if (command.commandId() == QLatin1String("resetTerminal")) { return hasFocus && PanePool::instance() != nullptr && PanePool::instance()->item(m_model->focusedPaneId()) != nullptr; } // Search commands: a focused pane with a live item. if (command.commandId() == QLatin1String("openSearch") || command.commandId() == QLatin1String("closeSearch") || command.commandId() == QLatin1String("searchNext") || command.commandId() == QLatin1String("searchPrevious") || command.commandId() == QLatin1String("enterCopyMode") || command.commandId() == QLatin1String("exitCopyMode")) { return hasFocus && PanePool::instance() != nullptr && PanePool::instance()->item(m_model->focusedPaneId()) != nullptr; } // Pane zoom needs a sibling to hide: a single-pane tab zooming to // itself changes nothing, so the command greys out. Maximize stays // enabled on a single-pane tab — it still collapses the chrome. Checked // BEFORE the generic pane-category gate below, which would otherwise // answer for every pane command first. if (command.commandId() == QLatin1String("togglePaneZoom")) { if (!hasFocus) { return false; } const Workspace *ws = activeWorkspace(); if (ws == nullptr || ws->tabs().isEmpty()) { return false; } const XenoTab *tab = ws->tabs().at(ws->activeTabIndex()); return tab != nullptr && tab->paneIdList().size() > 1; } // Pane-splitting, pane lifecycle, focus movement, swapping. if (command.category() == QLatin1String("pane")) { return hasFocus; } // Tab reorder: needs >=2 tabs and room in the asked direction. Checked // BEFORE the generic tab-category gate below. if (command.commandId() == QLatin1String("moveTabLeft") || command.commandId() == QLatin1String("moveTabRight")) { const Workspace *ws = activeWorkspace(); if (ws == nullptr) { return false; } const int count = static_cast<int>(ws->tabs().size()); if (count < 2) { return false; } const int index = ws->activeTabIndex(); if (command.commandId() == QLatin1String("moveTabLeft")) { return index > 0; } return index < count - 1; } // Tab commands: any workspace exists. if (command.category() == QLatin1String("tab")) { return workspaceCount > 0; } // Workspace navigation: previous/next need >1; selectWorkspaceN needs >=n. if (command.commandId() == QLatin1String("previousWorkspace") || command.commandId() == QLatin1String("nextWorkspace")) { return workspaceCount > 1; } if (command.commandId().startsWith(QLatin1String("selectWorkspace"))) { bool ok = false; QString id = command.commandId(); const int n = id.remove(QLatin1String("selectWorkspace")).toInt(&ok); return ok && workspaceCount >= n; } if (command.commandId() == QLatin1String("closeWorkspace")) { return workspaceCount > 0; } if (command.commandId() == QLatin1String("addJjWorkspace")) { const Workspace *ws = activeWorkspace(); return ws != nullptr && ws->isJjRepo(); } if (command.commandId() == QLatin1String("addGitWorktree")) { const Workspace *ws = activeWorkspace(); return ws != nullptr && ws->isGitRepo(); } // quit, preferences, palette, jump, view, add-menu popups: always true. return true;}
bool Commands::commandEnabledFor(const QString &commandId, const QVariant &ctx) { ensureModelConnected(); CommandDef *command = find(commandId); if (command == nullptr) { return false; } const bool hasCtx = ctx.isValid() && !ctx.isNull(); if (!hasCtx) { return command->enabled(); }
// Context-gated commands (ported from CommandRegistry.qml enabledFor). if (commandId == QLatin1String("copy")) { if (auto *term = ctx.value<TerminalQuickItem *>()) { return term->hasActiveSelection(); } return false; } if (commandId == QLatin1String("paste")) { if (auto *term = ctx.value<TerminalQuickItem *>()) { return !term->hasActiveSelection(); } return false; } if (commandId == QLatin1String("workspaceForgetJj")) { const int index = workspaceIndexFromCtx(ctx); if (index < 0 || m_model == nullptr || index >= m_model->workspaces().size()) { return false; } return m_model->workspaces().at(index)->kind() == WorkspaceKind::JjWorkspace; } if (commandId == QLatin1String("addJjWorkspace") || commandId == QLatin1String("addGitWorktree")) { // Workspace-row context: offer what the CLICKED card's repo supports. // A null ctx (the add menu, the palette) keeps using command->enabled, // which globalEnabled gates on the active workspace. const int index = workspaceIndexFromCtx(ctx); if (index < 0 || m_model == nullptr || index >= m_model->workspaces().size()) { return false; } const Workspace *ws = m_model->workspaces().at(index); return commandId == QLatin1String("addJjWorkspace") ? ws->isJjRepo() : ws->isGitRepo(); } if (commandId == QLatin1String("newTab")) { // Workspace-row context: the row's index must be valid. const int index = workspaceIndexFromCtx(ctx); return index >= 0 || (m_model != nullptr && !m_model->workspaces().isEmpty()); } // Workspace-row commands (rename/setDirectory/forget/delete): need a // valid row index. if (command->menus().contains(QLatin1String("workspace"))) { return workspaceIndexFromCtx(ctx) >= 0; } // Pane-context commands otherwise: the pane must still exist. if (command->menus().contains(QLatin1String("pane"))) { const QString paneId = paneIdFromCtx(ctx); return !paneId.isEmpty(); } return command->enabled();}
bool Commands::commandVisibleFor(const QString &commandId, const QVariant &ctx) { // Ported from CommandRegistry.qml: only workspaceForgetJj overrides the // base (always-visible) implementation, and its visibleFor == enabledFor. if (commandId == QLatin1String("workspaceForgetJj")) { return commandEnabledFor(commandId, ctx); } return true;}
void Commands::dispatch(const QString &commandId, const QVariant &ctx) { // The Commands singleton may be created before AppModel's (singleton // create order is not guaranteed); retry the connection on every // dispatch so model-executed commands never silently no-op. ensureModelConnected(); CommandDef *command = find(commandId); if (command == nullptr) { return; } if (!commandEnabledFor(commandId, ctx)) { return; }
// Pure model/pool commands execute directly inside Commands (they have // model access); everything else publishes to the controllers. const QVariant effectiveCtx = unwrapJsCtx(ctx); const QString focusedPaneId = m_model != nullptr ? m_model->focusedPaneId() : QString(); // Pane-context commands fall back to the focused pane (the QML registry's // `context ? context.paneId : AppModel.focusedPaneId` pattern). const QString targetPaneId = [&effectiveCtx, &focusedPaneId]() { const QString fromCtx = paneIdFromCtx(effectiveCtx); return fromCtx.isEmpty() ? focusedPaneId : fromCtx; }();
if (m_model == nullptr) { emit runCommand(commandId, effectiveCtx); return; }
if (commandId == QLatin1String("splitHorizontal")) { m_model->splitPaneFromString(targetPaneId, QStringLiteral("down")); return; } if (commandId == QLatin1String("splitVertical")) { m_model->splitPaneFromString(targetPaneId, QStringLiteral("right")); return; } if (commandId == QLatin1String("splitWithTerminal")) { m_model->splitPaneWithKind(targetPaneId, QStringLiteral("right"), QStringLiteral("terminal"), QString(), 0, 0); return; } if (commandId == QLatin1String("splitWithEditor")) { // An editor request with no path becomes a file-only picker, so this key // opens a chooser and never an empty editor. m_model->splitPaneWithKind(targetPaneId, QStringLiteral("right"), QStringLiteral("editor"), QString(), 0, 0); return; } if (commandId == QLatin1String("closePane")) { if (m_model->paneDirty(targetPaneId)) { emit dirtyPaneCloseRequested(targetPaneId); return; } m_model->closePane(targetPaneId); return; } if (commandId == QLatin1String("keepOnlyPane")) { if (m_model->paneDirty(targetPaneId)) { emit dirtyPaneCloseRequested(targetPaneId); return; } m_model->keepOnlyPane(targetPaneId); return; } if (commandId == QLatin1String("focusNextPane")) { m_model->focusNextPane(); return; } if (commandId == QLatin1String("togglePaneZoom")) { m_model->togglePaneZoom(); return; } if (commandId == QLatin1String("maximizePane")) { m_model->maximizePane(); return; } if (commandId == QLatin1String("focusPaneLeft")) { m_model->focusDirectionFromString(targetPaneId, QStringLiteral("left")); return; } if (commandId == QLatin1String("focusPaneRight")) { m_model->focusDirectionFromString(targetPaneId, QStringLiteral("right")); return; } if (commandId == QLatin1String("focusPaneUp")) { m_model->focusDirectionFromString(targetPaneId, QStringLiteral("up")); return; } if (commandId == QLatin1String("focusPaneDown")) { m_model->focusDirectionFromString(targetPaneId, QStringLiteral("down")); return; } if (commandId == QLatin1String("swapPaneLeft")) { if (PanePool *pool = PanePool::instance()) { pool->swapFocusedDirection(QStringLiteral("left")); } return; } if (commandId == QLatin1String("swapPaneRight")) { if (PanePool *pool = PanePool::instance()) { pool->swapFocusedDirection(QStringLiteral("right")); } return; } if (commandId == QLatin1String("swapPaneUp")) { if (PanePool *pool = PanePool::instance()) { pool->swapFocusedDirection(QStringLiteral("up")); } return; } if (commandId == QLatin1String("swapPaneDown")) { if (PanePool *pool = PanePool::instance()) { pool->swapFocusedDirection(QStringLiteral("down")); } return; } if (commandId == QLatin1String("resetTerminal")) { if (auto *term = qobject_cast<TerminalQuickItem *>( PanePool::instance() != nullptr ? PanePool::instance()->item(targetPaneId) : nullptr)) { term->resetTerminal(); } return; } if (commandId == QLatin1String("copy")) { if (ctx.isValid() && !ctx.isNull()) { if (auto *term = ctx.value<TerminalQuickItem *>()) { term->copySelection(); } return; } if (auto *term = qobject_cast<TerminalQuickItem *>( PanePool::instance() != nullptr ? PanePool::instance()->item(focusedPaneId) : nullptr)) { term->copySelection(); } return; } if (commandId == QLatin1String("paste")) { if (ctx.isValid() && !ctx.isNull()) { if (auto *term = ctx.value<TerminalQuickItem *>()) { term->pasteFromClipboard(); } return; } if (auto *term = qobject_cast<TerminalQuickItem *>( PanePool::instance() != nullptr ? PanePool::instance()->item(focusedPaneId) : nullptr)) { term->pasteFromClipboard(); } return; } if (commandId == QLatin1String("newTab")) { const int index = workspaceIndexFromCtx(effectiveCtx); if (index >= 0) { m_model->newTab(index); return; } m_model->newTab(m_model->workspaceIndexOf(m_model->activeWorkspaceId())); return; } if (commandId == QLatin1String("closeTab")) { const int wsIndex = m_model->workspaceIndexOf(m_model->activeWorkspaceId()); const QList<Workspace*>& workspaces = m_model->workspaces(); if (wsIndex >= 0 && wsIndex < workspaces.size()) { const int tabIndex = workspaces.at(wsIndex)->activeTabIndex(); if (!m_model->dirtyEditorPanesInTab(wsIndex, tabIndex).isEmpty()) { emit dirtyTabCloseRequested(wsIndex, tabIndex); return; } m_model->closeTab(wsIndex, tabIndex); } return; } if (commandId == QLatin1String("nextTab")) { m_model->nextTab(); return; } if (commandId == QLatin1String("previousTab")) { m_model->previousTab(); return; } if (commandId == QLatin1String("moveTabLeft") || commandId == QLatin1String("moveTabRight")) { const int wsIndex = m_model->workspaceIndexOf(m_model->activeWorkspaceId()); const QList<Workspace*>& workspaces = m_model->workspaces(); if (wsIndex < 0 || wsIndex >= workspaces.size()) { return; } const int tabIndex = workspaces.at(wsIndex)->activeTabIndex(); m_model->moveTab(wsIndex, tabIndex, tabIndex + (commandId == QLatin1String("moveTabLeft") ? -1 : 1)); return; } if (commandId == QLatin1String("renameTab")) { // A dialog decision point: publish to the controllers. emit runCommand(commandId, effectiveCtx); return; } if (commandId == QLatin1String("previousWorkspace")) { m_model->previousWorkspace(); return; } if (commandId == QLatin1String("nextWorkspace")) { m_model->nextWorkspace(); return; } if (commandId.startsWith(QLatin1String("selectWorkspace"))) { bool ok = false; QString id = commandId; const int n = id.remove(QLatin1String("selectWorkspace")).toInt(&ok); if (ok) { m_model->selectWorkspaceByNumber(n); } return; } if (commandId == QLatin1String("resetLayout")) { m_model->resetLayout(); return; } if (commandId == QLatin1String("equalizeSplits")) { m_model->equalizeSplits(); return; }
// Everything else (jumpToPane, commandPalette, resetLayout aside — // handled above — toggleSidebar, missionControl, preferences, quit, // newWorkspace, the add-workspace sources, closeWorkspace, and the // workspace-row commands) needs page/dialog/menu state that Commands // does not know about: publish to the controllers. emit runCommand(commandId, effectiveCtx);}
// ---------------------------------------------------------------------------// Enabled recompute// ---------------------------------------------------------------------------
Workspace *Commands::activeWorkspace() { AppModel *model = AppModel::instance(); if (model == nullptr) { return nullptr; } const QString activeId = model->activeWorkspaceId(); if (activeId.isEmpty()) { return nullptr; } for (Workspace *ws : model->workspaces()) { if (ws != nullptr && ws->id() == activeId) { return ws; } } return nullptr;}
void Commands::ensureModelConnected() { if (m_modelConnected && m_model != nullptr) { return; } AppModel *model = AppModel::instance(); if (model == nullptr) { return; } if (m_modelConnected) { // The previous connection target died; reconnect to the new instance. m_modelConnected = false; } m_model = model; connect(model, &AppModel::focusedPaneChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::activeWorkspaceChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::workspacesChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::tabsChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::activeTabChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::structureChanged, this, &Commands::recomputeEnabled); connect(model, &AppModel::workspaceDataChanged, this, &Commands::recomputeEnabled); if (model->settings() != nullptr) { connect(model->settings(), &XenoSettings::commandSequencesChanged, this, &Commands::applyOverrides); } m_modelConnected = true; applyOverrides();}
void Commands::recomputeEnabled() { ensureModelConnected(); for (CommandDef *command : std::as_const(m_commands)) { command->setEnabled(globalEnabled(*command)); }}