mirror of
https://github.com/logos-co/logos-app.git
synced 2026-08-30 19:01:09 +00:00
* test(deps): pin what the load gate actually admits
The gate in front of a UI plugin load reads
package_manager.resolveFlatDependencies and classifies each row BY
EXCLUSION:
if (m.value("status").toString() == "not_installed") missing << s;
else installed << s;
`status` is a closed vocabulary owned by logos-package-manager, and it
gained a fourth word. Anything invented after that line was written lands
in `installed`, so a plugin is admitted on top of a dependency the
resolver rejected — with no diagnostic anywhere.
Lift the predicate into app/utils/DependencyBlocker.h VERBATIM, alongside
the message-building it will need, and put the desired behaviour under
test. The three fixtures are wire payloads captured from a real logoscore
daemon over a real installed tree, parsed with QJsonDocument so the suite
consumes exactly the bytes the module emits.
Red at this commit — the classification cases fail, which is master's
behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(deps): refuse a load on a version-mismatched dependency, and say which
The gate in front of a UI plugin load treated every status except
"not_installed" as satisfied, so a dependency the resolver had just
rejected for being the wrong version was admitted — the plugin mounted on
top of it, or liblogos refused with a bare "plugin load failed".
Name the blocking statuses instead of excluding one, and carry the reason
as far as the user:
* PackageCoordinator gains m_blockingDepsByModule alongside the existing
name list — same membership, filled in the same pass, one map per
blocking dependency. missingDepsOf keeps returning names, so the
sidebar marker, AppsModel::setMissingDeps and installStatus are
untouched; blockingDepsOf is for callers that must TELL the user
something.
* ConfirmationDialog says one of three sentences. "cannot be loaded
because the following modules are not installed" is a lie about a
module that is installed at the wrong version, and it sends the user
to reinstall something they already have. Each row now names the
constraint and what was found — "depsvc — requires ^2.0.0, found
1.0.0" — because a complaint the user cannot act on is barely better
than silence.
* The sidebar marker keeps the red cross for an absent dependency and
draws an amber "!" for a version conflict. "mixed" keeps the cross:
something IS absent, and that is the fact to act on first.
* ModuleInstanceModel's badge reads "Version conflict" rather than
"Missing deps" for the same reason. depBlockKind joins the diffRoles
mask because replaceRows SKIPS a row whose mask is empty, so a row
going absent -> mismatch would otherwise keep the stale word forever.
Deliberately still admitted, both pinned by tests: "cycle" (never driven
against a real cyclic install — blocking it would be a behaviour change
made blind) and an unrecognised status (this gate is advisory in front of
liblogos' own resolver; refusing on a word this build does not know would
block loads that work).
Consequence worth naming: a version-mismatched app now demotes to
installStatus NotInstalled in the App Manager, exactly as one with an
absent dependency already did. The remedy the button offers — reinstall,
which re-resolves dependencies — is right for both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(plugins): pin what the core-dependency loader actually reads
PluginLoader::loadCoreDependencies read each `dependencies[]` entry as
QString depName = dep.toString();
if (depName.isEmpty()) continue;
QVariant::toString() on a QVariantMap returns a NULL QString — no
diagnostic, no exception. So the moment the module ABI is widened to send
the object form {"name": …, "version": "^2.0.0", "signer": "did:…"} that
the LGX spec already allows and lgpm already parses, every CONSTRAINED
core dependency is silently skipped and the ui plugin mounts on top of an
unloaded dependency.
This commit changes no behaviour. It gives that decision a name
(logos::readDependencyEntry) and a test, so the next commit's fix is
visible as a diff in outcomes rather than a diff in expressions.
Red, as expected — 5 of 14 cases fail against the extracted behaviour:
FAIL! : object_entry_yields_its_name() kind 1, want 0
FAIL! : object_entry_without_constraints_yields_its_name() kind 1, want 0
FAIL! : json_object_entry_yields_its_name() kind 1, want 0
FAIL! : hash_object_entry_yields_its_name() kind 1, want 0
FAIL! : a_number_is_unrecognised() kind 0, want 1
(1 = Unrecognised, i.e. skipped. The last one is the mirror image: a
non-string scalar is currently stringified and loaded as a module name.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(plugins): read object-form core dependency entries, refuse the rest
The load gate now understands both forms the LGX spec allows, and reports
what it cannot understand instead of walking past it.
Green — 14/14, was 9/14:
PASS : object_entry_yields_its_name()
PASS : object_entry_without_constraints_yields_its_name()
PASS : json_object_entry_yields_its_name()
PASS : hash_object_entry_yields_its_name()
PASS : a_number_is_unrecognised()
Totals: 14 passed, 0 failed
Two behaviour changes at PluginLoader::loadCoreDependencies:
* {"name": "wallet_module", "version": "^2.0.0", "signer": "did:…"} now
loads wallet_module. Before, QVariant::toString() returned a null
QString for it and the `if (depName.isEmpty()) continue;` skipped the
dependency with no error and no log line — the ui plugin then mounted
on top of a core module that was never loaded, and the first symptom
was a dead endpoint somewhere else entirely.
Latent today only because the module ABI still sends bare strings
(package_manager_impl.cpp's toLogosMap flattens the constraint away).
It arms itself the moment that is widened. Every other reader in the
fleet already handles both forms — logos-module's module_metadata.cpp,
logos-standalone-app's mainwindow.cpp, logos-cpp-sdk's
metadata_dependencies.h, lgpm's manifest scan. Basecamp was the outlier.
* An entry that is neither a name nor an object with a string "name" is
now a hard, logged load failure rather than a silent skip. Same reason:
we cannot honour a dependency we cannot name, and mounting anyway hides
it. This also stops a stray scalar being stringified into a module
name — QVariant(42).toString() is "42", and the old loop would have
gone looking for a module called that.
Unreachable from `lgpm install` today: logos-package's Manifest::fromJson
rejects a non-string/non-object entry outright, and lgpm's own scan warns
and drops what gets past it. The branch exists for the manifests that
bypass both — embedded installs written at build time, and anything
edited in place afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(deps): refuse a load on a dependency published by a different signer
A dependency can now fail its dependant in a third way, and it needs a third
sentence. The gate already separated "not installed" from "installed at a
version it does not accept", because "install it" and "get another version" are
different instructions. `signer_mismatch` is a package installed under the RIGHT
NAME by the WRONG PUBLISHER — somebody else's package — and neither of those
instructions works on it. Installing gets the same package back; no version of
what is on disk is the package the module named.
blockSummary headline body
"absent" Missing Dependencies "…are not installed"
"mismatch" Incompatible Dependencies "…installed at a version it does
not accept"
"signer" Unexpected Publisher "…were published by a different
signer than it requires … reinstall
these from the publisher the module
names"
"mixed" Missing Dependencies "…missing, the wrong version, or from
a different publisher"
Per row, the detail clause names BOTH DIDs: "published by a different signer;
requires <pin>, found <observed>". The pin alone does not say what went wrong;
the observation alone is an accusation with no charge attached.
THE DESIGN CALL, and it is pinned by a test rather than left as an omission.
`signer_unknown` — the edge pins a publisher and NOTHING RECORDS who published
the installed package — does NOT block. Absence of evidence is not evidence of
mismatch, and this is the expected state for two whole populations: every
EMBEDDED package (placed by the build, never through the installer that records
a publisher, so it can never acquire one) and everything installed before the
record existed. Blocking here would make a pin on an embedded dependency
unsatisfiable by construction, forever, with no action a user could take.
logos-package-manager owns the call and can flip it in one place
(UnknownSignerPolicy::Strict makes its scanner emit signer_mismatch, which this
gate already blocks), which is exactly why this gate must not second-guess it.
Three places in this header classified by testing ONE value and sweeping the
rest into a default, and all three are now switches or named alternatives:
readDependencyBlocker the trailing `else` that made version_mismatch
count as satisfied — already fixed, extended
dependencyBlockerToMap `VersionMismatch ? … : "not_installed"`, which
would have crossed into QML labelling a signer
mismatch "not_installed"
summariseDependencyBlockers tested one kind and swept the rest into `absent`,
so a pure signer set summarised as "not installed"
dependencyIsPresent stays written by exclusion, and that is the one place it is
right: exactly one kind means "nothing under this name", so a kind added later
is by definition about a package that IS installed and should count as present
by default. The comment now says so.
The sidebar marker generalises from `_versionConflictOnly` to
`_presentButRejected` — the amber "!" means "everything needed is on disk and
this app rejects it", which is true of both mismatch kinds, while "mixed" keeps
the red cross because something really is absent. The marker's objectName
splits three ways so a UI test can tell the two amber states apart, which a
screenshot cannot. ModuleInstanceModel's badge gains "Signer conflict".
The signer wire rows in dependency_gate_test.cpp are VERBATIM payloads captured
from a real logoscore daemon over a real installed tree, like the rows beside
them, varying only the `signer` sidecar in the dependency's install directory.
RED (signer_mismatch unrecognised, and the summariser back to one kind):
Totals: 27 passed, 6 failed
a_signer_mismatch_blocks b.kind 0, expected 3
a_signer_mismatched_dependency_blocks_… b.kind 0, expected 3
a_signer_mismatch_says_publisher_not_version… detail was empty
the_wire_map_carries_the_signer_kind… kind "not_installed"
summary_of_only_signer_blockers_is_signer summary "absent"
summary_of_a_signer_and_an_absent_blocker… summary "absent"
GREEN, every check individually (nix build --print-out-paths; empty = FAILED):
unit-tests /nix/store/kdml49yg3pcjazdynnq29311wvk7s03d-logos-basecamp-unit-tests-0.0.0
qml-tests /nix/store/k5gmfqwa749nbipam0w9sfdq8cnrdraj-logos-basecamp-qml-tests-0.0.0
smoke-test /nix/store/qhilqrrmlxgi1r6rkk3f2xdhzad1khbs-logos-basecamp-smoke-test
integration-test /nix/store/rda92q559abgp57y5nqn5db1n0p9zwjd-logos-basecamp-integration-test
sandbox-test /nix/store/ip1flrwh4sf05jn747rj9fgrfn8zk7h8-logos-basecamp-sandbox-test-0.0.0
shutdown-test /nix/store/q9a0z0d0c83kl3z17r1d3pps6qp1fgs0-logos-basecamp-shutdown-test
host-services-test /nix/store/9szcjfih4hvc40wvlsihf4yy2l7616gc-logos-basecamp-host-services-test
symbol-gate /nix/store/pbflp7xdnf5py676ii0mwqi0xpkdfg1c-logos-basecamp-symbol-gate
symbol-gate-negative /nix/store/v5vmswffkfq82qhnl9z9q17zz17smbp9-logos-basecamp-symbol-gate-negative
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(deps): follow observedSigner to signerDid, and say what actually failed
logos-package-manager stopped recording who it verified at install and started
carrying the package's own manifest.sig into the install tree. The row key that
reaches this header changed name because it changed meaning, and the message
built from it was describing the old one.
`observedSigner` was the DID a signature had been VERIFIED against by the
installer. `signerDid` is what the installed signature says about itself, once
checked against the key its own DID carries. Nothing outside the document
corroborates it, so "found <did>" overstated it: it read as an established
publisher when it is the package's own claim, backed by a real key but not by
anything that says which key is the right one.
The detail line now says what failed:
signed by a different key; requires <pin>, signed by <did>
not signed by the required key; requires <pin> (nothing usable installed)
signed by a different key (neither DID available)
And a note this header needs to carry, because the shortcut is inviting and
unsound: do NOT re-derive the verdict by comparing `signerDid` to
`requiredSigner`. A signature document supplies both a DID and a signature, so
it can always be made to agree with itself; that comparison accepts somebody
else's genuine signature relabelled to name the pinned DID. The scanner decides
by verifying the installed signature under the PIN's key, which is why a
signer_mismatch row legitimately carries a signerDid that differs from
requiredSigner.
The dialog headline is unchanged: "published by a different signer" is the
right register for a user, and the mechanism belongs on the row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(deps): re-capture the signer rows against real signatures, and say what a satisfied pin looks like
These rows are documented as verbatim wire payloads, and they were — of a rig
that no longer exists. They were captured by varying a `signer` sidecar, which
logos-package-manager has deleted; and the DID they pinned,
did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5In0, decodes to
{"kty":"OKP","crv":"Ed25519"} — no `x` member, so NO KEY AT ALL.
Against the sidecar that string was only ever COMPARED, so a keyless DID worked
fine. Against a signature it is an UNPARSEABLE PIN, which the library also
reports as signer_mismatch (fail-closed, with a warning naming the depending
manifest). So this suite would have stayed green while every signer row
documented the wrong cause: "this pin cannot be parsed" wearing the label
"a different key signed it".
Re-captured through the real stack — a real logoscore daemon, package_manager
built against the branch, depsvc INSTALLED FROM A REALLY-SIGNED .lgx so the
manifest.sig in its install directory is the one `lgx sign` produced. Nothing
is hand-planted; the only thing that varies is the `signer` pin in the
depending manifest, which is a developer declaration and is meant to vary. Both
DIDs are now real keypairs, and the mismatch row is a package really signed by
one key against a pin naming another.
Adds the row that was missing: a SATISFIED pin. It carries a signerDid, because
that is a property of the package rather than of the edge, and a gate that read
"signer information is present" as a signal in itself would refuse every
correctly-signed dependency in the fleet. Proven by mutation.
Also proven by mutation, and the reason NO comparison guard is added here:
replacing the status check with `signerDid != requiredSigner` — the inviting
shortcut, and the unsound one — turns a_signer_that_cannot_be_checked_does_not_block
red, because a signer_unknown row has a pin and no signerDid. The existing
suite already holds that line; the gate needs no new defence, it needs to keep
deciding from `status`.
The dialog now says what is actually known. "Published by a different signer"
reported a record the installer had written and could be no better than that
record. The scanner now extracts the key from the DID the module itself names
and checks the installed signature against it, so the claim is "not signed by
the key it requires" — which no relabelling on the package side can change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(deps): cut this PR's comments back to what a reader needs
608 added comment lines -> 351. Comments only; no code line moved or changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): relock onto the merged package-manager chain
Takes logos-package-manager-module 5feeb67 (#64) and logos-package-manager
1218d74 (#38), so the gate this PR adds is no longer inert.
Also adds logos-package-manager-ui.inputs.package_manager.follows: without it
the UI brought its own package_manager and the closure carried two, with the
UI that drives installs on the one that has no VersionMismatch. Same shape as
the package_downloader follows in #360.
Verified in the artifact, not the lock: the shipped
modules/package_manager/libpackage_manager_lib.so carries version_mismatch,
signer_mismatch and dependencyConstraints, and is md5-identical to the
relocked lib output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(deps): drive every dependency-gate verdict against the real binary
Adds basecamp-dependency-gate.test.yaml: one launch covering all five —
satisfied admits (exact and range), signer-satisfied admits, signer-unknown
admits, absent blocks, version blocks, signer blocks — each asserting its own
message, not just that something was refused.
Fixtures are real: two Ed25519 keys, both anchored, so the signer refusal is
identity and not trust. The spec fails if either DID carries no `x` member,
and if the app ever logs an unparseable pin — a keyless pin reports
signer_mismatch too, so without that check the suite stays green documenting
the wrong cause.
Also fixes a regression this PR introduced: an absent dependency with no
declared range added "— not installed" to its row, which broke
basecamp-missing-deps ("• demo_core_module" is an exact match) and changed the
text for every bare dependency in the fleet. The dialog heading already says
it; the detail is now empty unless there is a range to name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(deps): one package_downloader in the closure too, not two
Folds in #360, which is closed. Same defect as the package_manager follows
directly above: logos-package-manager-ui declares neither, so the closure
carried two logos-package-downloader revisions with the UI that drives
installs on the older one.
That one matters because lgpd's resolver is the only component in the stack
that evaluates a version range or a signer pin at all. After: downloader
b6624a3, carrying both the empty-signer-pin fix and the signer binding that
never checked the signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(deps): one liblgx in the closure, not two
b75bae8 moved the bundled package_manager onto logos-package 49151f00,
which adds lgx_get_manifest_sig_json and lgx_check_manifest_signature.
logos-liblogos still pinned its own older logos-package-manager, and
liblogos_core links libpackage_manager_lib, so liblogos put the older
liblgx into the bundle's flat lib/.
The two platforms then resolved that ambiguity in opposite directions:
bundle.sh rewrites a Mach-O @rpath dep to a flat @loader_path/../../lib
path, while its ELF arm leaves a bare NEEDED with $ORIGIN first. macOS
bound package_manager to the 34-export lib/ copy and Linux to the
36-export sibling — so the module crashed at its first signature check
on macOS with every Linux job green. Mach-O binds lazily, so it loaded
cleanly and died at the call, three hops from anything that named it.
Follows removes the second revision rather than adding one: no root
input rev changes, 18 nodes drop out of the lock, and lib/liblgx gains
the two symbols. Fixes the macOS package-lifecycle, dependency-gate and
persistence doctests, all 8 failures.
* test(deps): assert the plugin binary by the platform's own suffix
expect_contains matched "depsvc_plugin.so" verbatim — expand_vars runs
over run: but not over the expected strings, so {ext} cannot be used
there. Assert on a string the script emits instead.
The install itself was fine on macOS; the step listed depsvc_plugin.dylib
and exited 0.
* test(deps): gate a library staged twice against symbol skew
Where the bundle stages one library name in lib/ AND beside a module,
both copies must satisfy that module's imports — macOS binds to lib/,
Linux to the sibling, so a skew is fatal on exactly one platform. The
gate intersects each consumer's undefined symbols against both copies
and fails when the two answers differ.
Intersecting against bundled definers keeps host and system symbols out
of scope. Vacuity guards abort if no duplicated name is found or nm
reads nothing, so the gate cannot pass by measuring zero.
On the pre-fix bundle it fails on BOTH platforms, including the Linux
one CI called green. Wired into build-appimage and build-macos-app,
which already build this bundle; not on Windows, where a PE has no
nm -D symbol table.
* fix(ci): the link gate must not clobber ./result
nix build without --out-link writes ./result, so the gate replaced the
bundle symlink that "Rename AppImage with architecture" and "Package app
bundle as tarball" both find their artifact through. The gate itself
passed on aarch64-linux; the step after it failed.
--no-link: the gate's output is a marker, nothing reads it.
* fix(deps): the load gate must not read the cache before it is filled
blockingDepsOf returns an empty list for a cache MISS, and loadUiModule
read that as "nothing blocks this". PackageCoordinator.h states the
opposite contract — "Empty when the async refresh chain hasn't completed
yet; treat empty as not known — show safe defaults" — and the tiles are
published before the dependency fan-out is dispatched, so the window is
real: clicking inside it loads the plugin.
Pre-existing, but this PR is what makes it matter. The only verdict that
could slip through before was not_installed, which liblogos refuses on
its own. version_mismatch and signer_mismatch have no enforcement below
Basecamp at all — DependencyEntry carries {kind, name}, and the resolver
tests isKnown() — so losing the race now mounts a plugin against a
dependency of a rejected version.
Park the load on dependencyDataReadyChanged, mirroring
PackageCoordinator::uninstallApp: same disconnect-prior, same QPointer
guard, last click wins.
* chore(deps): take the staging install (lgpm d88abaa)
Install stages beside the destination and swaps it in rather than copying
over the existing tree, so a failed install leaves no merged directory,
an upgrade drops files the new package stopped shipping, and a module
dir left 0555 no longer wedges the package.
Both pins move together: basecamp declares logos-package-manager
directly AND gets one through the module, with no follows between them,
so moving one would leave two lgpm builds in one closure. Verified they
converge on d88abaa, and liblogos still follows the root pin.
Folded in here rather than raised separately because this branch already
touches flake.lock.
* fix(intents): the missing-deps lambda must match the widened signal
Master's intent broker connects to missingDepsPopupRequested with
(QString, QStringList) — the signature before this branch widened it to
(QString, QVariantList blockers, QString summary). Git merges both sides
cleanly and the result does not compile:
qobject.h:244: no type named 'type' in FunctorReturnType<lambda, List<>>
The lambda only needs the name, so it takes the two extra parameters
unnamed. Also relocks onto master's flake.lock, which has moved
logos-liblogos to a2da65f; the follows here still resolves liblogos'
lgpm to the root pin (d88abaa) and keeps the lock at 3574 nodes against
master's 5152.
* chore(deps): logos-liblogos ca0d5bf — the runtime range gate
This PR gates a UI plugin at the Basecamp layer; liblogos#196 gates any
module at load, on the declared version range. Same feature, two layers,
and until now the app only had the upper one.
The follows still resolves liblogos' lgpm to the root pin (d88abaa) and
the lock grows by one node, the logos-package that logos-module now
carries.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
714 lines
35 KiB
C++
714 lines
35 KiB
C++
#include "MainUIBackend.h"
|
|
|
|
#include <QDebug>
|
|
|
|
#include "IntentBridgeAdapter.h"
|
|
#include "IntentBroker.h"
|
|
#include "IntentRegistry.h"
|
|
#include "ShellIntentChooser.h"
|
|
#include "ShellIntentInstaller.h"
|
|
#include "ShellIntentEndpoint.h"
|
|
#include "UIPluginPresenter.h"
|
|
#include "AppsModel.h"
|
|
#include "BasecampModelRoles.h"
|
|
#include "CoreModuleManager.h"
|
|
#include "ModuleInstanceModel.h"
|
|
#include "UIPluginManager.h"
|
|
#include "PackageCoordinator.h"
|
|
#include "BuildInfo.h"
|
|
|
|
#include <QDebug>
|
|
#include <QJSValue>
|
|
#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);
|
|
|
|
// ── Intents: constructed FIRST, and the order is load-bearing ───────
|
|
//
|
|
// Qt destroys children in reverse construction order, and IntentBridgeAdapter
|
|
// tracks bridges that UIPluginManager owns indirectly. Built after it, the
|
|
// adapter would die first and those bridge destructors would call
|
|
// bridgeDestroyed() on freed memory. See ~IntentBridgeAdapter.
|
|
m_intentRegistry = new IntentRegistry(this);
|
|
m_intentBroker = new IntentBroker(m_intentRegistry, /*presenter=*/nullptr, this);
|
|
m_intentAdapter = new IntentBridgeAdapter(m_intentBroker, 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);
|
|
|
|
// Bridges become intent-capable from here on. PluginLoader attaches each
|
|
// ui_qml app's bridge as it loads, before its QML runs.
|
|
m_uiPluginManager->setIntentAdapter(m_intentAdapter);
|
|
|
|
m_intentPresenter = new UIPluginPresenter(m_uiPluginManager, this);
|
|
m_intentBroker->setPresenter(m_intentPresenter);
|
|
wireIntents();
|
|
|
|
// 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::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::presentAppRequested,
|
|
this, &MainUIBackend::presentAppRequested);
|
|
|
|
// 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.
|
|
void MainUIBackend::wireIntents()
|
|
{
|
|
// Registry rebuilds follow the UI-plugin metadata cache, which
|
|
// PackageCoordinator already keeps fresh on every install/uninstall via its
|
|
// event subscriptions (100 ms debounce) and on refresh(). Label and icon
|
|
// are injected as callables so the chooser can never disagree with the
|
|
// sidebar about what an app is called or what it looks like.
|
|
connect(m_uiPluginManager, &UIPluginManager::uiPluginMetadataChanged,
|
|
this, &MainUIBackend::rebuildIntentRegistry);
|
|
|
|
// Activation queue drains. appReady is the success edge; the other two are
|
|
// the failure edges. Without the missing-deps one a request sits for the
|
|
// full activation deadline while the user stares at a modal dialog.
|
|
connect(m_uiPluginManager, &UIPluginManager::appReady,
|
|
m_intentBroker, &IntentBroker::onAppReady);
|
|
connect(m_uiPluginManager, &UIPluginManager::pluginLoadFailedNotice,
|
|
this, [this](const QString& name, const QString&) {
|
|
m_intentBroker->onAppUnavailable(name);
|
|
});
|
|
connect(m_uiPluginManager, &UIPluginManager::missingDepsPopupRequested,
|
|
this, [this](const QString& name, const QVariantList&, const QString&) {
|
|
m_intentBroker->onAppUnavailable(name);
|
|
});
|
|
|
|
// Chooser signals are re-emitted for QML. The broker itself is NOT put into
|
|
// the QML context — that would break the facade contract this class holds.
|
|
connect(m_intentBroker, &IntentBroker::chooserRequested,
|
|
this, &MainUIBackend::intentChooserRequested);
|
|
connect(m_intentBroker, &IntentBroker::chooserDismissed,
|
|
this, &MainUIBackend::intentChooserDismissed);
|
|
|
|
// …and the seam that tells the broker whether anyone is actually listening
|
|
// to those signals. Counting receivers on the broker would always report
|
|
// the two connections just made above.
|
|
m_intentChooser = std::make_unique<ShellIntentChooser>(
|
|
[this](const QString& dispatchId, const QString& intent,
|
|
const QString& requesterName, const QVariantList& providers) {
|
|
const int receivers = this->receivers(
|
|
SIGNAL(intentChooserRequested(QString, QString, QString, QVariantList)));
|
|
if (receivers > 0)
|
|
emit intentChooserRequested(dispatchId, intent, requesterName, providers);
|
|
return receivers;
|
|
},
|
|
[this](const QString& dispatchId) {
|
|
emit intentChooserDismissed(dispatchId);
|
|
});
|
|
m_intentBroker->setChooser(m_intentChooser.get());
|
|
|
|
// The install suggestion. No receiver count and no withdraw: the request
|
|
// that prompted this is already answered, so a prompt that fails to mount
|
|
// costs nothing and there is nothing to take back.
|
|
m_intentInstaller = std::make_unique<ShellIntentInstaller>(
|
|
[this](const QString& intent, const QStringList& candidates) {
|
|
// Carry WHERE each candidate comes from. A package suggested by a
|
|
// repo the user added for something unrelated deserves to be
|
|
// recognised as such before it is installed.
|
|
QVariantList detailed;
|
|
for (const QString& name : candidates) {
|
|
detailed.append(QVariantMap{
|
|
{QStringLiteral("moduleName"), name},
|
|
{QStringLiteral("displayName"), displayNameFor(name)},
|
|
{QStringLiteral("repositoryUrl"), repositoryUrlFor(name)},
|
|
});
|
|
}
|
|
emit intentInstallOffered(intent, candidates, detailed);
|
|
});
|
|
m_intentBroker->setInstaller(m_intentInstaller.get());
|
|
|
|
// Catalog-sourced providers follow the apps model, which is what carries
|
|
// packages that are NOT installed. Kept in a table the resolver never
|
|
// consults — see IntentRegistry::setInstallableProviders.
|
|
if (m_appsModel) {
|
|
// ALL FOUR SIGNALS. Catalog rows arrive via beginInsertRows /
|
|
// endInsertRows, not a model reset — connecting only modelReset and
|
|
// dataChanged meant a freshly fetched catalog never reached the
|
|
// registry, so an intent nothing installed could service went straight
|
|
// to `unavailable` even with a package for it sitting in the catalog.
|
|
connect(m_appsModel, &QAbstractItemModel::modelReset,
|
|
this, &MainUIBackend::rebuildInstallableProviders);
|
|
connect(m_appsModel, &QAbstractItemModel::dataChanged,
|
|
this, &MainUIBackend::rebuildInstallableProviders);
|
|
connect(m_appsModel, &QAbstractItemModel::rowsInserted,
|
|
this, &MainUIBackend::rebuildInstallableProviders);
|
|
connect(m_appsModel, &QAbstractItemModel::rowsRemoved,
|
|
this, &MainUIBackend::rebuildInstallableProviders);
|
|
rebuildInstallableProviders();
|
|
}
|
|
|
|
// The shell's endpoint. deliverRequest returns the receiver count of the
|
|
// QML-facing signal: zero means nothing in ContentViews.qml is listening,
|
|
// and the broker must turn that into a timeout rather than waiting forever
|
|
// on a handler that does not exist.
|
|
m_shellEndpoint = std::make_unique<ShellIntentEndpoint>(
|
|
[this](const QString& dispatchId, const QString& intent,
|
|
const QVariantMap& params, const QString& requesterName) {
|
|
const int receivers =
|
|
this->receivers(SIGNAL(shellIntentRequested(QString, QString, QVariantMap, QString)));
|
|
if (receivers > 0)
|
|
emit shellIntentRequested(dispatchId, intent, params, requesterName);
|
|
return receivers;
|
|
});
|
|
m_intentBroker->registerEndpoint(QStringLiteral("main_ui"), m_shellEndpoint.get());
|
|
|
|
// The shell's own provided capabilities. main_ui is the only module allowed
|
|
// a "logos.*" name; IntentRegistry refuses that name from any disk record.
|
|
// What the shell itself asks apps for. Declared in code because the shell
|
|
// has no metadata.json, but checked by the broker exactly like an app's.
|
|
m_intentRegistry->registerShellUses(
|
|
QStringLiteral("main_ui"), {QStringLiteral("packages.show")});
|
|
|
|
m_intentRegistry->registerShellProvider(
|
|
QStringLiteral("main_ui"),
|
|
{QStringLiteral("logos.repositories.manage")},
|
|
QStringLiteral("Logos"),
|
|
QStringLiteral("qrc:/qt/qml/Basecamp/Icons/assets/settings.svg"));
|
|
|
|
rebuildIntentRegistry();
|
|
}
|
|
|
|
void MainUIBackend::rebuildIntentRegistry()
|
|
{
|
|
if (!m_intentRegistry || !m_uiPluginManager) return;
|
|
m_intentRegistry->rebuild(
|
|
m_uiPluginManager->uiPluginMetadataSnapshot(),
|
|
[this](const QString& name) { return displayNameFor(name); },
|
|
[this](const QString& name) {
|
|
return m_uiPluginManager->pluginIconUrl(name);
|
|
});
|
|
|
|
// Surface what was skipped or refused. Without this a malformed `uses`
|
|
// block is invisible: the app's request comes back `not_declared` and the
|
|
// only clue is on the requester's side, which cannot see why.
|
|
const QStringList problems = m_intentRegistry->diagnostics();
|
|
for (const QString& problem : problems)
|
|
qWarning().noquote() << "IntentRegistry:" << problem;
|
|
}
|
|
|
|
void MainUIBackend::rebuildInstallableProviders()
|
|
{
|
|
if (!m_intentRegistry || !m_appsModel) return;
|
|
|
|
QMap<QString, QStringList> byModule;
|
|
for (int row = 0; row < m_appsModel->rowCount(); ++row) {
|
|
const QModelIndex idx = m_appsModel->index(row, 0);
|
|
|
|
// Installed packages are answered by the real registry. Offering to
|
|
// install something already present would be nonsense.
|
|
if (idx.data(AppsModelRoles::IsInstalledRole).toBool())
|
|
continue;
|
|
|
|
const QStringList provides =
|
|
idx.data(AppsModelRoles::ProvidesRole).toStringList();
|
|
if (provides.isEmpty())
|
|
continue;
|
|
|
|
byModule.insert(idx.data(AppsModelRoles::NameRole).toString(), provides);
|
|
}
|
|
m_intentRegistry->setInstallableProviders(byModule);
|
|
|
|
// One line per rebuild, and only when something is actually offerable —
|
|
// silence here is what an empty catalog looks like, which is exactly the
|
|
// state that made an install offer never appear.
|
|
if (!byModule.isEmpty()) {
|
|
qWarning().noquote() << "IntentRegistry: installable providers ="
|
|
<< QStringList(byModule.keys()).join(QStringLiteral(", "));
|
|
}
|
|
}
|
|
|
|
// ── Intent surface for QML — all delegating one-liners ──────────────────────
|
|
|
|
void MainUIBackend::respondToShellIntent(const QString& requestId, bool ok,
|
|
const QVariant& data, const QString& error)
|
|
{
|
|
// `data` is an untyped QVariant, so QML hands over the QJSValue wrapper
|
|
// untouched — and a QJSValue is engine-bound, hence non-canonical, so the
|
|
// broker would drop the response and mint `failed` at a caller that did
|
|
// nothing wrong. LogosQmlBridge::respond flattens for the same reason on
|
|
// the app path; this is the shell's copy of that boundary.
|
|
const QVariant flat = data.userType() == qMetaTypeId<QJSValue>()
|
|
? data.value<QJSValue>().toVariant()
|
|
: data;
|
|
|
|
if (m_intentBroker && m_shellEndpoint)
|
|
m_intentBroker->submitResponse(m_shellEndpoint.get(), requestId, ok, flat, error);
|
|
}
|
|
|
|
void MainUIBackend::resolveIntentChooser(const QString& dispatchId,
|
|
const QString& providerName)
|
|
{
|
|
if (m_intentBroker) m_intentBroker->resolveChooser(dispatchId, providerName);
|
|
}
|
|
|
|
void MainUIBackend::cancelIntentChooser(const QString& dispatchId)
|
|
{
|
|
if (m_intentBroker) m_intentBroker->cancelChooser(dispatchId);
|
|
}
|
|
|
|
QString MainUIBackend::repositoryUrlFor(const QString& packageName) const
|
|
{
|
|
if (!m_appsModel) return QString();
|
|
for (int row = 0; row < m_appsModel->rowCount(); ++row) {
|
|
const QModelIndex idx = m_appsModel->index(row, 0);
|
|
if (idx.data(AppsModelRoles::NameRole).toString() == packageName)
|
|
return idx.data(AppsModelRoles::RepositoryUrlRole).toString();
|
|
}
|
|
return QString(); // installed but not in any catalog we know
|
|
}
|
|
|
|
QVariantMap MainUIBackend::providerDetailsFor(const QString& packageName) const
|
|
{
|
|
QVariantMap out;
|
|
out.insert(QStringLiteral("moduleName"), packageName);
|
|
out.insert(QStringLiteral("displayName"), displayNameFor(packageName));
|
|
|
|
// Every package is unsigned today: the catalog carries no signatures and
|
|
// `trustedSigners` is empty. Stated outright rather than omitted, because a
|
|
// detail panel that lists a version and an origin and says nothing about
|
|
// provenance reads as reassurance it has not earned. When signing lands
|
|
// this becomes a real answer instead of a constant.
|
|
out.insert(QStringLiteral("verified"), false);
|
|
|
|
if (m_packageCoordinator) {
|
|
out.insert(QStringLiteral("installType"),
|
|
m_packageCoordinator->installType(packageName));
|
|
|
|
// DELIBERATELY NOT the content hash, though it is available
|
|
// (PackageCoordinator::installedRootHash). A hash is only evidence when
|
|
// the reader holds an independent, trusted value to compare it against,
|
|
// and with nothing signed the catalog that serves the package serves
|
|
// the hash too — so an attacker controlling one controls the other. A
|
|
// tampered copy does not keep the original digest, but it does not need
|
|
// to: it simply presents its own.
|
|
//
|
|
// Shown here it would be worse than absent. A 64-character digest beside
|
|
// the word "Unsigned" reads as rigour, and weakens the one honest
|
|
// statement on the panel. When signing lands it stops being decoration,
|
|
// because a signature is exactly the trusted reference it lacks today.
|
|
}
|
|
|
|
if (!m_appsModel)
|
|
return out;
|
|
|
|
for (int row = 0; row < m_appsModel->rowCount(); ++row) {
|
|
const QModelIndex idx = m_appsModel->index(row, 0);
|
|
if (idx.data(AppsModelRoles::NameRole).toString() != packageName)
|
|
continue;
|
|
|
|
out.insert(QStringLiteral("version"),
|
|
idx.data(AppsModelRoles::InstalledVersionRole).toString());
|
|
out.insert(QStringLiteral("repositoryUrl"),
|
|
idx.data(AppsModelRoles::RepositoryUrlRole).toString());
|
|
out.insert(QStringLiteral("description"),
|
|
idx.data(AppsModelRoles::DescriptionRole).toString());
|
|
break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void MainUIBackend::showPackageDetails(const QString& packageName)
|
|
{
|
|
if (packageName.isEmpty()) return;
|
|
|
|
// Ask whoever provides `packages.show` — in practice the Package Manager,
|
|
// which renders it in its own details panel.
|
|
//
|
|
// An intent rather than a direct call into package_manager_ui on purpose.
|
|
// basecamp used to carry exactly one hardcoded edge into that app — a .rep
|
|
// SIGNAL, a replica connect, two forwarding C++ signals and a QML handler,
|
|
// all to reach one settings screen — and it has since been deleted in
|
|
// favour of the generic path. Adding a second would be walking backwards.
|
|
// This also means the shell dogfoods the mechanism it asks apps to use.
|
|
if (m_intentBroker && m_shellEndpoint
|
|
&& m_registryDeclaresPackagesShow()) {
|
|
m_intentBroker->submit(m_shellEndpoint.get(),
|
|
QStringLiteral("shell-details-") + packageName,
|
|
QStringLiteral("packages.show"),
|
|
QVariantMap{{QStringLiteral("name"), packageName}});
|
|
return;
|
|
}
|
|
|
|
showPackageDetailsFallback(packageName);
|
|
}
|
|
|
|
bool MainUIBackend::m_registryDeclaresPackagesShow() const
|
|
{
|
|
return m_intentRegistry
|
|
&& m_intentRegistry->resolve(QStringLiteral("packages.show")).status
|
|
!= IntentRegistry::None;
|
|
}
|
|
|
|
void MainUIBackend::showPackageDetailsFallback(const QString& packageName)
|
|
{
|
|
if (!m_packageCoordinator) return;
|
|
|
|
// Nothing provides packages.show — no Package Manager installed, or an
|
|
// older build of it. The App Manager's own detail dialog answers the same
|
|
// question (version, publisher, dependencies, install state), so the
|
|
// Details button still does something rather than silently failing.
|
|
//
|
|
// allowFastLaunch=false is the point: the user asked to LOOK, not to run.
|
|
m_packageCoordinator->openApp(packageName, repositoryUrlFor(packageName),
|
|
{}, /*allowFastLaunch=*/false);
|
|
}
|
|
|
|
void MainUIBackend::beginIntentInstall(const QString& providerName)
|
|
{
|
|
if (!m_packageCoordinator || providerName.isEmpty())
|
|
return;
|
|
|
|
// The normal install path, gate dialog and all. Being prompted by an intent
|
|
// buys no shortcut past the confirmation the user would see anywhere else —
|
|
// it is the same install, started for a different reason.
|
|
m_packageCoordinator->openApp(providerName, repositoryUrlFor(providerName), {},
|
|
/*allowFastLaunch=*/false);
|
|
}
|
|
|
|
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(); }
|
|
|