Files
logos-app/app/MainUIBackend.cpp
T
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00

370 lines
19 KiB
C++

#include "MainUIBackend.h"
#include "AppsModel.h"
#include "CoreModuleManager.h"
#include "ModuleInstanceModel.h"
#include "UIPluginManager.h"
#include "PackageCoordinator.h"
#include "BuildInfo.h"
#include <QDebug>
#include <QTimer>
MainUIBackend::MainUIBackend(LogosAPI* logosAPI, logos::qt::QtLogosCore* core, QObject* parent)
: QObject(parent)
, m_currentActiveSectionIndex(0)
, m_logosAPI(logosAPI)
, m_core(core)
, m_ownsLogosAPI(false)
, m_coreModuleManager(nullptr)
, m_uiPluginManager(nullptr)
, m_packageCoordinator(nullptr)
, m_uiModulesModel(new ModuleInstanceModel(this))
, m_coreModulesModel(new ModuleInstanceModel(this))
{
if (!m_logosAPI) {
m_logosAPI = new LogosAPI("core", this);
m_ownsLogosAPI = true;
}
// Order matters: CoreModuleManager must exist before UIPluginManager so
// the latter's ctor can receive a valid pointer; UIPluginManager must
// exist before PackageCoordinator for the same reason. Qt tears children
// down in reverse order at destruction, so PackageCoordinator dies first
// (stops talking to the module), then UIPluginManager (tears down
// widgets while CoreModuleManager's C API handle is still valid).
m_appsModel = new AppsModel(this);
m_coreModuleManager = new CoreModuleManager(m_logosAPI, m_core, this);
m_uiPluginManager = new UIPluginManager(m_logosAPI, m_coreModuleManager, this);
m_packageCoordinator = new PackageCoordinator(m_logosAPI, m_coreModuleManager, m_uiPluginManager, m_appsModel, this);
m_appsModel->setInstallRegistry(m_packageCoordinator->installRegistry());
// Setter-injection closes the cycle — UIPluginManager queries
// PackageCoordinator for installType / missing-deps when building its
// uiModules() list, and consumes uiPluginsFetched for its UI-specific
// metadata cache. See UIPluginManager::setPackageCoordinator for the signal
// connections it sets up internally.
m_uiPluginManager->setPackageCoordinator(m_packageCoordinator);
// Forward manager signals into our own signals of the same name. QML
// binds to these; by funneling through the facade we keep a stable
// surface regardless of which manager emitted them.
//
// UIPluginManager drives uiModulesChanged/launcherAppsChanged on load/
// unload events; PackageCoordinator's own uiModulesChanged/launcherAppsChanged
// already flow through UIPluginManager (wired in setPackageCoordinator) so
// we only need to listen to UIPluginManager here. The refresh slot
// repopulates m_uiModulesModel in one shot; QML consumers listen to the
// model's own dataChanged/modelReset signals rather than any signal here.
connect(m_uiPluginManager, &UIPluginManager::uiModulesChanged,
this, &MainUIBackend::refreshUiModulesModel);
// Clear the Modules Reload overlay once the user-initiated UI refresh
// lands its first uiModulesChanged (list is already updated by then).
connect(m_uiPluginManager, &UIPluginManager::uiModulesChanged,
this, [this]() {
if (!m_pendingUiModulesRefresh) return;
m_pendingUiModulesRefresh = false;
endModulesLoading();
});
connect(m_uiPluginManager, &UIPluginManager::launcherAppsChanged,
this, &MainUIBackend::launcherAppsChanged);
connect(m_uiPluginManager, &UIPluginManager::loadingModulesChanged,
this, &MainUIBackend::loadingModulesChanged);
connect(m_uiPluginManager, &UIPluginManager::currentVisibleAppChanged,
this, &MainUIBackend::currentVisibleAppChanged);
connect(m_uiPluginManager, &UIPluginManager::navigateToApps,
this, &MainUIBackend::navigateToApps);
connect(m_uiPluginManager, &UIPluginManager::navigateToRepositoriesRequested,
this, &MainUIBackend::navigateToRepositoriesRequested);
connect(m_uiPluginManager, &UIPluginManager::packageInstallFailedNotice,
this, &MainUIBackend::installFailureNoticeRequested);
connect(m_uiPluginManager, &UIPluginManager::missingDepsPopupRequested,
this, &MainUIBackend::missingDepsPopupRequested);
connect(m_uiPluginManager, &UIPluginManager::unloadCascadeConfirmationRequested,
this, &MainUIBackend::unloadCascadeConfirmationRequested);
connect(m_uiPluginManager, &UIPluginManager::pluginWindowRequested,
this, &MainUIBackend::pluginWindowRequested);
connect(m_uiPluginManager, &UIPluginManager::pluginWindowRemoveRequested,
this, &MainUIBackend::pluginWindowRemoveRequested);
connect(m_uiPluginManager, &UIPluginManager::pluginWindowActivateRequested,
this, &MainUIBackend::pluginWindowActivateRequested);
// Forward PackageCoordinator dialog signals. One uninstallPlanRequested
// serves all four initiators — see buildPlanPayload.
connect(m_packageCoordinator, &PackageCoordinator::uninstallPlanRequested,
this, &MainUIBackend::uninstallPlanRequested);
connect(m_packageCoordinator, &PackageCoordinator::dependencyDataReadyChanged,
this, &MainUIBackend::dependencyDataReadyChanged);
// Distinct upgrade/downgrade/reinstall cascade signal — the dialog
// shape is the same as the uninstall variant, but the title + body
// need the target releaseTag + UpgradeMode (so a downgrade doesn't
// look like a bare uninstall). PackageCoordinator emits this from
// onBeforeUpgrade; OverlayDialogs.qml renders it via the
// "upgradeCascade" mode of ConfirmationDialog.
connect(m_packageCoordinator, &PackageCoordinator::upgradeCascadeConfirmationRequested,
this, &MainUIBackend::upgradeCascadeConfirmationRequested);
connect(m_packageCoordinator, &PackageCoordinator::installGateConfirmationRequested,
this, &MainUIBackend::installGateConfirmationRequested);
connect(m_packageCoordinator, &PackageCoordinator::requestOpenAddApplicationDialog,
this, &MainUIBackend::requestOpenAddApplicationDialog);
connect(m_packageCoordinator, &PackageCoordinator::addApplicationDataUpdated,
this, &MainUIBackend::addApplicationDataUpdated);
connect(m_packageCoordinator, &PackageCoordinator::launchAppRequested,
this, &MainUIBackend::launchAppRequested);
// The resolver's required packages are cached here and published as a
// property; QML binds a shell-declared AppsFilterProxy to it. Nothing on
// this side holds a pointer to that proxy.
connect(m_packageCoordinator, &PackageCoordinator::requiredPackagesResolved,
this, [this](const QVariantList& entries) {
if (m_requiredPackages == entries) return;
m_requiredPackages = entries;
emit requiredPackagesChanged();
});
// Widened to int at this hop rather than connected signal-to-signal, so
// the conversion is written down instead of relying on the implicit
// enum-to-int the connect check would otherwise permit silently.
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallStageChanged,
this, [this](const QString& name, InstallStage::Value stage) {
emit catalogInstallStageChanged(name, static_cast<int>(stage));
});
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFinished,
this, &MainUIBackend::catalogInstallFinished);
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallFailed,
this, &MainUIBackend::catalogInstallFailed);
// Package repositories — pure re-emits so QML binding to backend.*
connect(m_packageCoordinator, &PackageCoordinator::repositoriesChanged,
this, &MainUIBackend::repositoriesChanged);
connect(m_packageCoordinator, &PackageCoordinator::repositoriesLoadingChanged,
this, &MainUIBackend::repositoriesLoadingChanged);
connect(m_packageCoordinator, &PackageCoordinator::appsLoadingChanged,
this, &MainUIBackend::appsLoadingChanged);
connect(m_packageCoordinator, &PackageCoordinator::repositoryOperationCompleted,
this, &MainUIBackend::repositoryOperationCompleted);
// Any of the three managers can trigger coreModulesChanged:
// * CoreModuleManager on stats-tick / refresh
// * UIPluginManager on cascade-induced state changes (re-emits
// PackageCoordinator's coreModulesChanged as part of that wiring)
// Both fan into the same refresh slot. Qt coalesces redundant dataChanged
// notifies within a frame, so the two-connect layout doesn't cause
// visible flicker.
connect(m_uiPluginManager, &UIPluginManager::coreModulesChanged,
this, &MainUIBackend::refreshCoreModulesModel);
connect(m_coreModuleManager, &CoreModuleManager::coreModulesChanged,
this, &MainUIBackend::refreshCoreModulesModel);
// Kick the first catalog scan now that all wiring is in place. We do
// this AFTER setPackageCoordinator (and its signal connections) so the
// resulting uiPluginsFetched / uiModulesChanged land on live slots.
QTimer::singleShot(0, this, [this]() {
m_packageCoordinator->refresh();
});
qDebug() << "MainUIBackend created";
}
MainUIBackend::~MainUIBackend() = default;
QAbstractItemModel* MainUIBackend::appsModel() const
{
return m_appsModel;
}
void MainUIBackend::beginShutdown()
{
if (m_uiPluginManager) {
m_uiPluginManager->shutdown();
}
}
int MainUIBackend::currentActiveSectionIndex() const
{
return m_currentActiveSectionIndex;
}
void MainUIBackend::setCurrentActiveSectionIndex(int index)
{
// Section list is owned by QML (SidebarPanel). The upper bound is
// self-policed there; we only guard against negative indices.
// Per-section side effects (e.g., the Modules-view auto-refresh) live
// in the QML view that becomes visible, not here.
if (m_currentActiveSectionIndex != index && index >= 0) {
m_currentActiveSectionIndex = index;
emit currentActiveSectionIndexChanged();
}
}
// --- coreModules() composer ------------------------------------------------
//
// coreModules is the one QML-visible property that spans multiple managers.
// Known + loaded + stats come from CoreModuleManager (raw liblogos state);
// installType comes from PackageCoordinator (populated during its dep-info
// refresh). We compose here so neither manager has to know about the other's
// schema.
QVariantList MainUIBackend::buildCoreModulesSnapshot() const
{
QVariantList modules;
if (!m_coreModuleManager) return modules;
const QStringList known = m_coreModuleManager->knownModules();
const QStringList loaded = m_coreModuleManager->loadedModules();
for (const QString& name : known) {
QVariantMap module;
module["name"] = name;
module["displayName"] = m_packageCoordinator ? m_packageCoordinator->displayNameFor(name) : name;
module["isLoaded"] = loaded.contains(name);
// installType populated lazily by refreshDependencyInfo's full-scan
// pass on PackageCoordinator. Empty means "not known yet" — QML treats
// that as a non-user module and hides Uninstall, which is the safe
// default.
module["installType"] = m_packageCoordinator ? m_packageCoordinator->installType(name) : QString();
const QVariantMap stats = m_coreModuleManager->moduleStats(name);
if (!stats.isEmpty()) {
module["cpu"] = stats["cpu"];
module["memory"] = stats["memory"];
} else {
module["cpu"] = "0.0";
module["memory"] = "0.0";
}
modules.append(module);
}
return modules;
}
// --- Manager delegations ---------------------------------------------------
//
// Each slot is a one-liner routing to the right manager. These stay on
// MainUIBackend so the QML `backend.foo(...)` contract is untouched by the
// refactor (QML still sees one receiver).
QVariantList MainUIBackend::buildUiModulesSnapshot() const { return m_uiPluginManager->uiModules(); }
// Refresh slots — fold "compute snapshot + push into model" into one call
// so the ctor can wire signal→slot directly rather than routing through a
// lambda for each source of change.
void MainUIBackend::refreshUiModulesModel()
{
m_uiModulesModel->replaceRows(buildUiModulesSnapshot());
}
void MainUIBackend::refreshCoreModulesModel()
{
m_coreModulesModel->replaceRows(buildCoreModulesSnapshot());
}
QVariantList MainUIBackend::launcherApps() const { return m_uiPluginManager->launcherApps(); }
QString MainUIBackend::currentVisibleApp() const{ return m_uiPluginManager->currentVisibleApp(); }
QStringList MainUIBackend::loadingModules() const { return m_uiPluginManager->loadingModules(); }
// UIPluginManager — UI plugin widget lifecycle + local unload cascade.
void MainUIBackend::loadUiModule(const QString& n) { m_uiPluginManager->loadUiModule(n); }
void MainUIBackend::unloadUiModule(const QString& n) { m_uiPluginManager->unloadUiModule(n); }
void MainUIBackend::activateApp(const QString& n) { m_uiPluginManager->activateApp(n); }
void MainUIBackend::confirmUnloadCascade(const QString& n) { m_uiPluginManager->confirmUnloadCascade(n); }
void MainUIBackend::loadCoreModule(const QString& n) { m_uiPluginManager->loadCoreModule(n); }
void MainUIBackend::unloadCoreModule(const QString& n) { m_uiPluginManager->unloadCoreModule(n); }
void MainUIBackend::refreshUiModules()
{
if (!m_pendingUiModulesRefresh) {
beginModulesLoading();
m_pendingUiModulesRefresh = true;
}
m_uiPluginManager->refreshUiModules();
}
void MainUIBackend::onAppLauncherClicked(const QString& n) { m_uiPluginManager->onAppLauncherClicked(n); }
void MainUIBackend::setCurrentVisibleApp(const QString& n) { m_uiPluginManager->setCurrentVisibleApp(n); }
// PackageCoordinator — package_manager IPC and package-lifecycle cascade.
QString MainUIBackend::displayNameFor(const QString& n) const {
return m_packageCoordinator ? m_packageCoordinator->displayNameFor(n) : n;
}
void MainUIBackend::uninstallUiModule(const QString& n) { m_packageCoordinator->uninstallUiModule(n); }
void MainUIBackend::uninstallApp(const QString& n, const QString& repositoryUrl)
{ m_packageCoordinator->uninstallApp(n, repositoryUrl); }
void MainUIBackend::uninstallCoreModule(const QString& n) { m_packageCoordinator->uninstallCoreModule(n); }
void MainUIBackend::confirmUninstallCascade(const QString& n) { m_packageCoordinator->confirmUninstallCascade(n); }
void MainUIBackend::confirmUninstallMultiCascade(const QStringList& names) { m_packageCoordinator->confirmUninstallMultiCascade(names); }
void MainUIBackend::cancelMultiUninstall(const QStringList& names) { m_packageCoordinator->cancelMultiUninstall(names); }
void MainUIBackend::cancelPendingUninstallApp(const QString& name) { m_packageCoordinator->cancelPendingUninstallApp(name); }
void MainUIBackend::confirmInstallGate(const QString& n) { m_packageCoordinator->confirmInstallGate(n); }
void MainUIBackend::cancelInstallGate(const QString& n) { m_packageCoordinator->cancelInstallGate(n); }
void MainUIBackend::openApp(const QString& name, const QString& repositoryUrl, const QVariantMap& versionPins, bool allowFastLaunch)
{ m_packageCoordinator->openApp(name, repositoryUrl, versionPins, allowFastLaunch); }
void MainUIBackend::confirmCatalogInstall(const QString& name, const QString& repositoryUrl, const QVariantMap& versionPins)
{ m_packageCoordinator->confirmCatalogInstall(name, repositoryUrl, versionPins); }
void MainUIBackend::notifyAddApplicationDialogClosed()
{ m_packageCoordinator->notifyAddApplicationDialogClosed(); }
// cancelPendingAction is the one slot that doesn't route to a single manager:
// a pending action lives on either UIPluginManager (local unload cascade) or
// PackageCoordinator (uninstall/upgrade cascade) but not both. Fan out to both —
// the un-involved manager no-ops on name-mismatch. This preserves the QML
// contract (single `backend.cancelPendingAction(name)` call for either dialog).
void MainUIBackend::cancelPendingAction(const QString& n) {
m_uiPluginManager->cancelUnloadCascade(n);
m_packageCoordinator->cancelPendingAction(n);
}
// Package repositories — delegations + cache pass-through.
QVariantList MainUIBackend::repositories() const { return m_packageCoordinator->repositories(); }
bool MainUIBackend::repositoriesLoading() const { return m_packageCoordinator->repositoriesLoading(); }
bool MainUIBackend::appsLoading() const
{ return !m_packageCoordinator || m_packageCoordinator->appsLoading(); }
bool MainUIBackend::dependencyDataReady() const
{ return m_packageCoordinator && m_packageCoordinator->dependencyDataReady(); }
bool MainUIBackend::modulesLoading() const
{ return m_modulesLoadingCount > 0; }
void MainUIBackend::beginModulesLoading()
{
if (++m_modulesLoadingCount == 1)
emit modulesLoadingChanged();
}
void MainUIBackend::endModulesLoading()
{
if (m_modulesLoadingCount <= 0) return;
if (--m_modulesLoadingCount == 0)
emit modulesLoadingChanged();
}
void MainUIBackend::refreshRepositories() { m_packageCoordinator->refreshRepositories(); }
void MainUIBackend::refreshAppCatalog() { m_packageCoordinator->remoteRefresh(); }
void MainUIBackend::addRepository(const QString& url) { m_packageCoordinator->addRepository(url); }
void MainUIBackend::removeRepository(const QString& url) { m_packageCoordinator->removeRepository(url); }
void MainUIBackend::setRepositoryEnabled(const QString& url, bool enabled) { m_packageCoordinator->setRepositoryEnabled(url, enabled); }
// --- CoreModuleManager delegations ----------------------------------------
void MainUIBackend::refreshCoreModules()
{
beginModulesLoading();
m_coreModuleManager->refresh();
QTimer::singleShot(0, this, [this]() { endModulesLoading(); });
}
QString MainUIBackend::getCoreModuleMethods(const QString& n) { return m_coreModuleManager->getMethods(n); }
QString MainUIBackend::getCoreModuleEvents(const QString& n) { return m_coreModuleManager->getEvents(n); }
QString MainUIBackend::callCoreModuleMethod(const QString& n,
const QString& m,
const QString& a) { return m_coreModuleManager->callMethod(n, m, a); }
// --- Build info -----------------------------------------------------------
//
// Thin QML-facing wrappers over the shared LogosBasecampBuildInfo helper
// (app/utils/BuildInfo.h), which reads the nix-generated logos_build_info.h.
QString MainUIBackend::buildVersion() const { return LogosBasecampBuildInfo::version(); }
bool MainUIBackend::isPortableBuild() const { return LogosBasecampBuildInfo::isPortableBuild(); }
QVariantList MainUIBackend::buildCommits() const { return LogosBasecampBuildInfo::commits(); }