diff --git a/libtaskmanager/tasksmodel.cpp b/libtaskmanager/tasksmodel.cppindex 4607b5a..942ff3b 100644--- a/libtaskmanager/tasksmodel.cpp+++ b/libtaskmanager/tasksmodel.cpp@@ -15,6 +15,7 @@#include "launchertasksmodel.h"#include "startuptasksmodel.h"+#include "waylandstartuptasksmodel.h"#include "windowtasksmodel.h"#include "launchertasksmodel_p.h"@@ -189,6 +190,45 @@ void TasksModel::Private::initModels()concatProxyModel->addSourceModel(startupTasksModel);+ // Quine Linux: tell the startup feedback about windows (with their pid),+ // so it can follow a launch past KWin's own notion of it ending. Once per+ // window model: it and the startup models are shared by all TasksModels.+ if (auto *startups = qobject_cast<WaylandStartupTasksModel *>(startupTasksModel->sourceModel());+ startups && !windowTasksModel->property("_quine_startupHook").toBool()) {+ windowTasksModel->setProperty("_quine_startupHook", true);+ auto *windows = windowTasksModel;+ // Queued: removing startup rows while the proxies are still taking in+ // the window's row corrupts the task order (launchers move, take+ // other apps' icons).+ auto report = [startups, windows](int first, int last) {+ for (int i = first; i <= last; ++i) {+ const QModelIndex index = windows->index(i, 0);+ const QString appId = index.data(AbstractTasksModel::AppId).toString();+ const quint32 pid = index.data(AbstractTasksModel::AppPid).toUInt();+ QMetaObject::invokeMethod(+ startups,+ [startups, appId, pid] {+ startups->windowAppeared(appId, pid);+ },+ Qt::QueuedConnection);+ }+ };+ QObject::connect(windowTasksModel, &QAbstractItemModel::rowsInserted, startups, [report](const QModelIndex &parent, int first, int last) {+ if (!parent.isValid()) {+ report(first, last);+ }+ });+ QObject::connect(windowTasksModel,+ &QAbstractItemModel::dataChanged,+ startups,+ [report](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {+ if (!topLeft.parent().isValid()+ && (roles.isEmpty() || roles.contains(AbstractTasksModel::AppId) || roles.contains(AbstractTasksModel::AppPid))) {+ report(topLeft.row(), bottomRight.row());+ }+ });+ }+// If we're in manual sort mode, we need to seed the sort map on pending row// insertions.QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeInserted, q, [this](const QModelIndex &parent, int start, int end) {diff --git a/libtaskmanager/waylandstartuptasksmodel.cpp b/libtaskmanager/waylandstartuptasksmodel.cppindex 760d91d..345ebbd 100644--- a/libtaskmanager/waylandstartuptasksmodel.cpp+++ b/libtaskmanager/waylandstartuptasksmodel.cpp@@ -13,9 +13,19 @@#include <qwayland-plasma-window-management.h>+#include <KLocalizedString>++#include <QDBusArgument>+#include <QDBusConnection>+#include <QDBusMessage>+#include <QDBusObjectPath>+#include <QDBusPendingCallWatcher>+#include <QDBusPendingReply>#include <QDebug>+#include <QFile>#include <QTimer>#include <QUrl>+#include <ctime>#include <QtWaylandClient/QWaylandClientExtensionTemplate>#include <algorithm>@@ -77,23 +87,199 @@ protected:}};+// Quine Linux: the launch feedback follows the launched app instead of a+// fixed timer.+//+// KDE starts an app (KIO) in a systemd unit named after it, as systemd's+// desktop conventions say: app-<id>@<random>.service, and a Flatpak moves it+// on into app-flatpak-<id>-<pid>.scope. While the feedback is up, this polls+// systemd for the app's units, and tells the ones that started since the+// launch (its own) from ones already running (another copy of the app), by+// when systemd started them: a launcher in another process (KRunner) can+// start its unit before this hears of the launch. Then:+// - a window of the app, by app id or by its pid being in one of those+// units, ends it (windowAppeared(), from TasksModel);+// - so does the app registering a tray icon (it started in the background);+// - KWin saying the startup finished doesn't end it while the units run:+// KWin keeps one startup at a time, and ends it when any window opens;+// - the units ending without a window ends it with a notification, "<App>+// didn't open" (and that another copy is running, if one is: a launch+// that hands over to a hung instance looks like this);+// - after StillStartingTimeout (klaunchrc [TaskbarButtonSettings], default+// 180 s) a notification asks: Keep Waiting, or Stop <App>;+// - a launch with no unit to follow (not started through systemd) ends+// after the usual Timeout, as before.+namespace+{+const QString s_systemd = QStringLiteral("org.freedesktop.systemd1");+const QString s_systemdPath = QStringLiteral("/org/freedesktop/systemd1");+const QString s_systemdManager = QStringLiteral("org.freedesktop.systemd1.Manager");+const QString s_notifications = QStringLiteral("org.freedesktop.Notifications");+const QString s_notificationsPath = QStringLiteral("/org/freedesktop/Notifications");+const QString s_sniWatcher = QStringLiteral("org.kde.StatusNotifierWatcher");+const QString s_sniWatcherPath = QStringLiteral("/StatusNotifierWatcher");+constexpr std::chrono::seconds s_pollInterval{1};+// A unit started this long before the launch was heard of is still its own.+constexpr qint64 s_startSlackUsec = 2'000'000;++// CLOCK_MONOTONIC in ยตs, systemd's *TimestampMonotonic clock.+qint64 monotonicUsec()+{+ timespec ts{};+ clock_gettime(CLOCK_MONOTONIC, &ts);+ return qint64(ts.tv_sec) * 1'000'000 + ts.tv_nsec / 1'000;+}++struct UnitInfo {+ QString name, description, loadState, activeState, subState, following;+ QDBusObjectPath path;+ quint32 jobId = 0;+ QString jobType;+ QDBusObjectPath jobPath;+};++const QDBusArgument &operator>>(const QDBusArgument &arg, UnitInfo &unit)+{+ arg.beginStructure();+ arg >> unit.name >> unit.description >> unit.loadState >> unit.activeState >> unit.subState >> unit.following >> unit.path >> unit.jobId >> unit.jobType+ >> unit.jobPath;+ arg.endStructure();+ return arg;+}++// As KIO escapes an app id into a unit name (KProcessRunner::escapeUnitName).+QString escapeUnitName(const QString &input)+{+ QString res;+ const QByteArray bytes = input.toUtf8();+ for (const unsigned char c : bytes) {+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') {+ res += QLatin1Char(c);+ } else {+ res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0'));+ }+ }+ return res;+}++// Whether a unit is one of the app's: app[-<launcher>]-<id>[@<random>].service+// or app[-<launcher>]-<id>-<random>.scope.+bool unitBelongsTo(const QString &unit, const QString &appId)+{+ if (!unit.startsWith(QLatin1String("app-"))) {+ return false;+ }+ QString rest = unit.mid(4);+ if (rest.endsWith(QLatin1String(".service"))) {+ rest.chop(8);+ if (const int at = rest.indexOf(QLatin1Char('@')); at >= 0) {+ rest.truncate(at);+ }+ } else if (rest.endsWith(QLatin1String(".scope"))) {+ rest.chop(6);+ const int dash = rest.lastIndexOf(QLatin1Char('-'));+ if (dash < 0) {+ return false;+ }+ rest.truncate(dash);+ } else {+ return false;+ }+ for (const QString &id : {escapeUnitName(appId), appId}) {+ if (rest == id || rest.endsWith(QLatin1Char('-') + id)) {+ return true;+ }+ }+ return false;+}++// The cgroup path of a process, split: its units are among the parts.+QStringList cgroupOf(quint32 pid)+{+ QFile file(QStringLiteral("/proc/%1/cgroup").arg(pid));+ if (pid == 0 || !file.open(QIODevice::ReadOnly)) {+ return {};+ }+ const auto lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));+ for (const QString &line : lines) {+ if (line.startsWith(QLatin1String("0::"))) { // cgroup v2+ return line.mid(3).split(QLatin1Char('/'), Qt::SkipEmptyParts);+ }+ }+ return {};+}++bool isAlive(const QString &activeState)+{+ return activeState != QLatin1String("inactive") && activeState != QLatin1String("failed");+}+}++// Relays the notification server's signals.+class StartupNotifications : public QObject+{+ Q_OBJECT+public:+ using QObject::QObject;+public Q_SLOTS:+ void onActionInvoked(uint id, const QString &action)+ {+ Q_EMIT actionInvoked(id, action);+ }+ void onClosed(uint id, uint reason)+ {+ Q_UNUSED(reason)+ Q_EMIT closed(id);+ }+ void onTrayItemRegistered(const QString &service)+ {+ Q_EMIT trayItemRegistered(service);+ }+Q_SIGNALS:+ void actionInvoked(uint id, const QString &action);+ void closed(uint id);+ void trayItemRegistered(const QString &service);+};+class Q_DECL_HIDDEN WaylandStartupTasksModel::Private{public:Private(WaylandStartupTasksModel *q);+ struct Startup;+void addActivation(PlasmaActivation *activation);void removeActivation(PlasmaActivation *activation);+ Startup *find(PlasmaActivation *activation);void init();void loadConfig();+ void pollUnits(PlasmaActivation *activation);+ void unitsPolled(PlasmaActivation *activation, const QHash<QString, QDBusObjectPath> &alive);+ void endIfOwns(const QStringList &cgroup);+ void notifyStillStarting(PlasmaActivation *activation);+ void notifyDidNotOpen(const Startup &startup, bool anotherRunning);+ void stopUnits(const QStringList &units);+ void trayItemRegistered(const QString &service);+struct Startup {QString name;QIcon icon;+ QString iconName;QString applicationId;QUrl launcherUrl;std::unique_ptr<PlasmaActivation> activation;+ // Quine Linux: following the launch (above).+ qint64 launchedUsec = 0; // when this heard of the launch (monotonic)+ QHash<QString, qint64> unitStarts; // the app's units: when each started (-1: asking)+ QStringList units; // the launch's own units, as last seen+ bool sawUnit = false;+ bool kwinFinished = false; // KWin said done; trusted once the units end+ bool stopping = false; // the user chose Stop+ uint notificationId = 0; // the "still starting" one+ QTimer *pollTimer = nullptr;+ QTimer *stillStartingTimer = nullptr;};WaylandStartupTasksModel *const q;@@ -101,6 +287,8 @@ public:std::unique_ptr<PlasmaActivationFeedback> feedback = nullptr;std::vector<Startup> startups;std::chrono::seconds startupTimeout = std::chrono::seconds::zero();+ std::chrono::seconds stillStartingTimeout{180};+ StartupNotifications *notifications = nullptr;};WaylandStartupTasksModel::Private::Private(WaylandStartupTasksModel *q)@@ -115,6 +303,54 @@ void WaylandStartupTasksModel::Private::init()loadConfig();});+ notifications = new StartupNotifications(q);+ auto bus = QDBusConnection::sessionBus();+ bus.connect(s_notifications,+ s_notificationsPath,+ s_notifications,+ QStringLiteral("ActionInvoked"),+ notifications,+ SLOT(onActionInvoked(uint, QString)));+ bus.connect(s_notifications,+ s_notificationsPath,+ s_notifications,+ QStringLiteral("NotificationClosed"),+ notifications,+ SLOT(onClosed(uint, uint)));+ bus.connect(s_sniWatcher,+ s_sniWatcherPath,+ s_sniWatcher,+ QStringLiteral("StatusNotifierItemRegistered"),+ notifications,+ SLOT(onTrayItemRegistered(QString)));++ QObject::connect(notifications, &StartupNotifications::actionInvoked, q, [this](uint id, const QString &action) {+ for (auto &startup : startups) {+ if (id == 0 || startup.notificationId != id) {+ continue;+ }+ startup.notificationId = 0;+ if (action == QLatin1String("stop")) {+ startup.stopping = true;+ stopUnits(startup.units);+ } else if (action == QLatin1String("wait")) {+ startup.stillStartingTimer->start(stillStartingTimeout);+ }+ return;+ }+ });+ QObject::connect(notifications, &StartupNotifications::closed, q, [this](uint id) {+ // Dismissed: keep waiting, without asking again.+ for (auto &startup : startups) {+ if (id != 0 && startup.notificationId == id) {+ startup.notificationId = 0;+ }+ }+ });+ QObject::connect(notifications, &StartupNotifications::trayItemRegistered, q, [this](const QString &service) {+ trayItemRegistered(service);+ });+loadConfig();}@@ -132,6 +368,7 @@ void WaylandStartupTasksModel::Private::loadConfig()const KConfigGroup taskbarButtonConfig(configWatcher->config(), u"TaskbarButtonSettings"_s);startupTimeout = std::chrono::seconds(taskbarButtonConfig.readEntry("Timeout", 5));+ stillStartingTimeout = std::chrono::seconds(taskbarButtonConfig.readEntry("StillStartingTimeout", 180));feedback = std::make_unique<PlasmaActivationFeedback>();@@ -167,26 +404,239 @@ void WaylandStartupTasksModel::Private::addActivation(PlasmaActivation *activatistartups.push_back(Startup{.name = appData.name,.icon = appData.icon,+ .iconName = appData.icon.name(),.applicationId = appId,.launcherUrl = launcherUrl,.activation = std::unique_ptr<PlasmaActivation>(activation),+ .launchedUsec = monotonicUsec(),});q->endInsertRows();- // Remove the activation if it doesn't finish within certain time interval.+ // Without a unit to follow, remove the activation if it doesn't+ // finish within certain time interval, as before.auto *timeoutTimer = new QTimer(activation);QObject::connect(timeoutTimer, &QTimer::timeout, q, [this, activation]() {- removeActivation(activation);+ if (auto *startup = find(activation); startup && !startup->sawUnit) {+ qCDebug(TASKMANAGER_DEBUG) << "Startup" << startup->applicationId << "has no unit to follow: timed out";+ removeActivation(activation);+ }});timeoutTimer->setSingleShot(true);timeoutTimer->start(startupTimeout);++ auto &startup = startups.back();+ startup.stillStartingTimer = new QTimer(activation);+ startup.stillStartingTimer->setSingleShot(true);+ QObject::connect(startup.stillStartingTimer, &QTimer::timeout, q, [this, activation]() {+ notifyStillStarting(activation);+ });+ startup.pollTimer = new QTimer(activation);+ startup.pollTimer->setInterval(s_pollInterval);+ QObject::connect(startup.pollTimer, &QTimer::timeout, q, [this, activation]() {+ pollUnits(activation);+ });+ pollUnits(activation);+ startup.pollTimer->start();});QObject::connect(activation, &PlasmaActivation::finished, q, [this, activation]() {+ auto *startup = find(activation);+ if (startup && startup->sawUnit && !startup->units.isEmpty()) {+ // KWin ends its startup when any window opens, or another app+ // starts: keep following the units; trust KWin once they end.+ qCDebug(TASKMANAGER_DEBUG) << "Startup" << startup->applicationId << "still running, though KWin finished it";+ startup->kwinFinished = true;+ return;+ }+ removeActivation(activation);+ });+}++WaylandStartupTasksModel::Private::Startup *WaylandStartupTasksModel::Private::find(PlasmaActivation *activation)+{+ auto it = std::ranges::find_if(startups, [activation](const Startup &startup) {+ return startup.activation.get() == activation;+ });+ return it == startups.end() ? nullptr : &*it;+}++void WaylandStartupTasksModel::Private::pollUnits(PlasmaActivation *activation)+{+ auto message = QDBusMessage::createMethodCall(s_systemd, s_systemdPath, s_systemdManager, QStringLiteral("ListUnitsByPatterns"));+ message << QStringList{} << QStringList{QStringLiteral("app-*")};+ // Parented to the activation: gone, and not answered, once it ends.+ auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), activation);+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, activation](QDBusPendingCallWatcher *watcher) {+ watcher->deleteLater();+ const QDBusMessage reply = watcher->reply();+ auto *startup = find(activation);+ if (!startup || reply.type() != QDBusMessage::ReplyMessage || reply.arguments().isEmpty()) {+ return;+ }+ QHash<QString, QDBusObjectPath> alive;+ const auto arg = reply.arguments().constFirst().value<QDBusArgument>();+ arg.beginArray();+ while (!arg.atEnd()) {+ UnitInfo unit;+ arg >> unit;+ if (isAlive(unit.activeState) && unitBelongsTo(unit.name, startup->applicationId)) {+ alive.insert(unit.name, unit.path);+ }+ }+ arg.endArray();+ unitsPolled(activation, alive);+ });+}++void WaylandStartupTasksModel::Private::unitsPolled(PlasmaActivation *activation, const QHash<QString, QDBusObjectPath> &alive)+{+ auto *startup = find(activation);+ if (!startup) {+ return;+ }++ // When each unit started, asked once per unit.+ for (auto it = alive.cbegin(); it != alive.cend(); ++it) {+ if (startup->unitStarts.contains(it.key())) {+ continue;+ }+ startup->unitStarts.insert(it.key(), -1);+ auto message = QDBusMessage::createMethodCall(s_systemd, it.value().path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get"));+ message << QStringLiteral("org.freedesktop.systemd1.Unit") << QStringLiteral("InactiveExitTimestampMonotonic");+ auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), activation);+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, activation, unit = it.key()](QDBusPendingCallWatcher *watcher) {+ watcher->deleteLater();+ const QDBusPendingReply<QDBusVariant> reply = *watcher;+ if (auto *startup = find(activation)) {+ // Unknown counts as already running: never mistaken for the launch.+ startup->unitStarts.insert(unit, reply.isValid() ? qint64(reply.value().variant().toULongLong()) : 0);+ }+ });+ }++ QStringList launched;+ bool anotherRunning = false;+ bool asking = false;+ for (auto it = alive.cbegin(); it != alive.cend(); ++it) {+ const qint64 started = startup->unitStarts.value(it.key(), -1);+ if (started < 0) {+ asking = true;+ } else if (started >= startup->launchedUsec - s_startSlackUsec) {+ launched << it.key();+ } else {+ anotherRunning = true;+ }+ }+ if (!launched.isEmpty()) {+ startup->units = launched;+ if (!startup->sawUnit) {+ qCDebug(TASKMANAGER_DEBUG) << "Startup" << startup->applicationId << "follows" << launched;+ startup->sawUnit = true;+ startup->stillStartingTimer->start(stillStartingTimeout);+ }+ return;+ }+ if (startup->sawUnit && !asking) {+ // The launch's units ended, and no window of the app appeared.+ qCDebug(TASKMANAGER_DEBUG) << "Startup" << startup->applicationId << "ended without a window; KWin finished:" << startup->kwinFinished;+ if (!startup->kwinFinished && !startup->stopping) {+ notifyDidNotOpen(*startup, anotherRunning);+ }removeActivation(activation);+ }+}++void WaylandStartupTasksModel::Private::endIfOwns(const QStringList &cgroup)+{+ if (cgroup.isEmpty()) {+ return;+ }+ std::vector<PlasmaActivation *> ended;+ for (const auto &startup : startups) {+ if (std::ranges::any_of(startup.units, [&cgroup](const QString &unit) {+ return cgroup.contains(unit);+ })) {+ ended.push_back(startup.activation.get());+ }+ }+ for (auto *activation : ended) {+ qCDebug(TASKMANAGER_DEBUG) << "Startup" << find(activation)->applicationId << "has a window or tray icon in its units";+ removeActivation(activation);+ }+}++void WaylandStartupTasksModel::Private::trayItemRegistered(const QString &service)+{+ if (startups.empty()) {+ return;+ }+ // "org.kde.StatusNotifierItem-<pid>-<n>" or ":1.23", maybe with "/<path>".+ const QString busName = service.section(QLatin1Char('/'), 0, 0);+ auto message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"),+ QStringLiteral("/org/freedesktop/DBus"),+ QStringLiteral("org.freedesktop.DBus"),+ QStringLiteral("GetConnectionUnixProcessID"));+ message << busName;+ auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), q);+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this](QDBusPendingCallWatcher *watcher) {+ watcher->deleteLater();+ const QDBusPendingReply<uint> reply = *watcher;+ if (reply.isValid()) {+ // Started in the background, in the tray: done.+ endIfOwns(cgroupOf(reply.value()));+ }+ });+}++void WaylandStartupTasksModel::Private::notifyStillStarting(PlasmaActivation *activation)+{+ auto *startup = find(activation);+ if (!startup || startup->units.isEmpty() || startup->notificationId != 0) {+ return;+ }+ const int seconds = int(stillStartingTimeout.count());+ const QString body = seconds < 120 ? i18ncp("@info", "It hasn't shown a window after %1 second.", "It hasn't shown a window after %1 seconds.", seconds)+ : i18ncp("@info", "It hasn't shown a window after %1 minute.", "It hasn't shown a window after %1 minutes.", seconds / 60);+ auto message = QDBusMessage::createMethodCall(s_notifications, s_notificationsPath, s_notifications, QStringLiteral("Notify"));+ message << startup->name << uint(0) << startup->iconName << i18nc("@title", "%1 is still starting", startup->name)+ << body+ << QStringList{QStringLiteral("wait"),+ i18nc("@action:button", "Keep Waiting"),+ QStringLiteral("stop"),+ i18nc("@action:button %1 is an app's name", "Stop %1", startup->name)}+ << QVariantMap{{QStringLiteral("urgency"), QVariant::fromValue(uchar(1))}} << int(0);+ auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), activation);+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, activation](QDBusPendingCallWatcher *watcher) {+ watcher->deleteLater();+ const QDBusPendingReply<uint> reply = *watcher;+ if (auto *startup = find(activation); startup && reply.isValid()) {+ startup->notificationId = reply.value();+ }});}+void WaylandStartupTasksModel::Private::notifyDidNotOpen(const Startup &startup, bool anotherRunning)+{+ const QString body = anotherRunning+ ? i18nc("@info %1 is an app's name",+ "It exited without showing a window. Another %1 is already running, and may not be responding.",+ startup.name)+ : i18nc("@info", "It exited without showing a window.");+ auto message = QDBusMessage::createMethodCall(s_notifications, s_notificationsPath, s_notifications, QStringLiteral("Notify"));+ message << startup.name << uint(0) << startup.iconName << i18nc("@title %1 is an app's name", "%1 didn't open", startup.name) << body+ << QStringList{} << QVariantMap{} << int(-1);+ QDBusConnection::sessionBus().asyncCall(message);+}++void WaylandStartupTasksModel::Private::stopUnits(const QStringList &units)+{+ for (const QString &unit : units) {+ auto message = QDBusMessage::createMethodCall(s_systemd, s_systemdPath, s_systemdManager, QStringLiteral("StopUnit"));+ message << unit << QStringLiteral("replace");+ QDBusConnection::sessionBus().asyncCall(message);+ }+}+void WaylandStartupTasksModel::Private::removeActivation(PlasmaActivation *activation){auto it = std::ranges::find_if(startups, [activation](const Startup &startup) {@@ -195,6 +645,11 @@ void WaylandStartupTasksModel::Private::removeActivation(PlasmaActivation *activif (it == startups.end()) {return;}+ if (it->notificationId != 0) {+ auto message = QDBusMessage::createMethodCall(s_notifications, s_notificationsPath, s_notifications, QStringLiteral("CloseNotification"));+ message << it->notificationId;+ QDBusConnection::sessionBus().asyncCall(message);+ }const int position = std::distance(startups.begin(), it);q->beginRemoveRows(QModelIndex(), position, position);startups.erase(it);@@ -244,6 +699,23 @@ int WaylandStartupTasksModel::rowCount(const QModelIndex &parent) constreturn parent.isValid() ? 0 : d->startups.size();}+void WaylandStartupTasksModel::windowAppeared(const QString &appId, quint32 pid)+{+ if (d->startups.empty()) {+ return;+ }+ std::vector<PlasmaActivation *> ended;+ for (const auto &startup : d->startups) {+ if (!appId.isEmpty() && startup.applicationId == appId) {+ ended.push_back(startup.activation.get());+ }+ }+ for (auto *activation : ended) {+ d->removeActivation(activation);+ }+ d->endIfOwns(cgroupOf(pid));+}+} // namespace TaskManager#include "waylandstartuptasksmodel.moc"diff --git a/libtaskmanager/waylandstartuptasksmodel.h b/libtaskmanager/waylandstartuptasksmodel.hindex 0028c3b..9ee4af2 100644--- a/libtaskmanager/waylandstartuptasksmodel.h+++ b/libtaskmanager/waylandstartuptasksmodel.h@@ -23,6 +23,13 @@ public:QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;int rowCount(const QModelIndex &parent = QModelIndex()) const override;+ /**+ * A window appeared, or got its app id or pid: ends the startup it+ * belongs to, by app id or by the pid being in the startup's systemd+ * units. TasksModel calls it for each window. (Quine Linux)+ */+ void windowAppeared(const QString &appId, quint32 pid);+private:class Private;std::unique_ptr<Private> d;