diff --git a/pkgbuilds/omarchy-task-manager/.omarchy/package.json b/pkgbuilds/omarchy-task-manager/.omarchy/package.json new file mode 100644 index 0000000..8b66e2d --- /dev/null +++ b/pkgbuilds/omarchy-task-manager/.omarchy/package.json @@ -0,0 +1,11 @@ +{ + "source": "local", + "channels": ["edge"], + "upstream": { + "watch": { + "github": "tcballard/omarchy-task-manager", + "pattern": "v(?P[0-9]+\\.[0-9]+\\.[0-9]+)", + "allow_prerelease": true + } + } +} diff --git a/pkgbuilds/omarchy-task-manager/PKGBUILD b/pkgbuilds/omarchy-task-manager/PKGBUILD new file mode 100644 index 0000000..94f01cd --- /dev/null +++ b/pkgbuilds/omarchy-task-manager/PKGBUILD @@ -0,0 +1,44 @@ +# Canonical recipe. scripts/package-source.sh fills the release source checksum. +# Maintainer: Tom Ballard (tcballard) +pkgname=omarchy-task-manager +pkgver=0.0.3 +pkgrel=3 +url='https://github.com/tcballard/omarchy-task-manager' +pkgdesc='Floating native task manager for Omarchy (preview)' +arch=('x86_64') +license=('MIT') +depends=('qt6-base' 'qt6-declarative' 'qt6-wayland' 'qt6-svg' 'hicolor-icon-theme' 'gcc-libs' 'glibc' 'glib2' 'systemd' 'coreutils') +optdepends=('gdb: live process core dumps' 'nvidia-utils: NVIDIA device telemetry') +makedepends=('cmake' 'ninja' 'rust' 'cargo') +checkdepends=('python' 'desktop-file-utils') +source=("$url/releases/download/v$pkgver/$pkgname-$pkgver.tar.gz" + "pause-resume-test.patch" + "worker-shutdown.patch") +sha256sums=('cc3e24a0bb8fad2b7ce0d4fb780aa774d32b53b8de924b488b55674bdb936c85' + 'ce191fc8dc7e4f7018193aa4982d58fd463f66b37e3129b3acce46b7a1c9a89e' + '1142aaedf8739bf2ed1cb823a03ed7e406d7ff59d7dfb72748815cc81cfda96b') + +# Backport upstream PR #11 (sample observation) and PR #12 (worker shutdown). +prepare() { + cd "$srcdir/$pkgname-$pkgver" + patch -Np1 -i "$srcdir/pause-resume-test.patch" + patch -Np1 -i "$srcdir/worker-shutdown.patch" +} + +build() { + cmake -S "$srcdir/$pkgname-$pkgver" -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib + cmake --build build +} + +check() { + cd "$srcdir/$pkgname-$pkgver" + cargo test --locked + ctest --test-dir "$srcdir/build" --output-on-failure + desktop-file-validate packaging/io.github.tcballard.TaskManager.desktop + python tests/protocol.py "$srcdir/build/omarchy-task-manager-core" +} + +package() { + DESTDIR="$pkgdir" cmake --install build +} diff --git a/pkgbuilds/omarchy-task-manager/pause-resume-test.patch b/pkgbuilds/omarchy-task-manager/pause-resume-test.patch new file mode 100644 index 0000000..e256547 --- /dev/null +++ b/pkgbuilds/omarchy-task-manager/pause-resume-test.patch @@ -0,0 +1,63 @@ +diff --git a/tests/bridge_test.cpp b/tests/bridge_test.cpp +index bab79f2..9bed7be 100644 +--- a/tests/bridge_test.cpp ++++ b/tests/bridge_test.cpp +@@ -79,37 +79,53 @@ private slots: + const auto before = bridge.history().size(); + QTest::qWait(1100); + QCOMPARE(bridge.history().size(), before); +- QSignalSpy samples(&bridge, &Bridge::snapshotChanged); ++ // Capture at emission: QTRY processes events, so another timer sample can ++ // replace bridge.snapshot() before the waiting assertion runs. ++ QVariantList samples; ++ QList historySizes; ++ QObject sampleObserver; // Disconnect before the captured lists are destroyed. ++ connect(&bridge, &Bridge::snapshotChanged, &sampleObserver, [&] { ++ samples.append(bridge.snapshot()); ++ historySizes.append(bridge.history().size()); ++ }); + bridge.setPaused(false); + QTRY_VERIFY_WITH_TIMEOUT(samples.count() > 0, 8000); +- QVERIFY(!bridge.snapshot() ++ QVERIFY(!samples.first().toMap() + .value("system") + .toMap() + .value("continuous") + .toBool()); +- QCOMPARE(bridge.history().size(), 1); ++ QCOMPARE(historySizes.first(), 1); + bridge.active(false); + QTest::qWait(600); + samples.clear(); ++ historySizes.clear(); + bridge.active(true); + QTRY_VERIFY_WITH_TIMEOUT(samples.count() > 0, 8000); +- QVERIFY(!bridge.snapshot() ++ QVERIFY(!samples.first().toMap() + .value("system") + .toMap() + .value("continuous") + .toBool()); + QTRY_VERIFY_WITH_TIMEOUT(!bridge.busy(), 8000); + samples.clear(); ++ historySizes.clear(); + bridge.refresh(); + QVERIFY(bridge.busy()); + bridge.setPaused(true); + bridge.setPaused(false); + QTRY_VERIFY_WITH_TIMEOUT(samples.count() > 0, 8000); +- QVERIFY(!bridge.snapshot() ++ QVERIFY(!samples.first().toMap() + .value("system") + .toMap() + .value("continuous") + .toBool()); ++ QCOMPARE(historySizes.first(), 1); ++ // Keep automatic refresh enabled. The baseline must be followed by a ++ // continuous sample; inspecting only the latest snapshot misses this order. ++ QTRY_VERIFY_WITH_TIMEOUT(samples.count() >= 2, 8000); ++ QVERIFY(samples.at(1).toMap().value("system").toMap() ++ .value("continuous").toBool()); + QTRY_VERIFY_WITH_TIMEOUT(!bridge.busy(), 8000); + QVERIFY(bridge.prepareManagement({{"category", "startup"}}).isEmpty()); + QVERIFY( diff --git a/pkgbuilds/omarchy-task-manager/worker-shutdown.patch b/pkgbuilds/omarchy-task-manager/worker-shutdown.patch new file mode 100644 index 0000000..d71f21b --- /dev/null +++ b/pkgbuilds/omarchy-task-manager/worker-shutdown.patch @@ -0,0 +1,90 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index c940e5e..d1cd40f 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -29,7 +29,8 @@ if(BUILD_TESTING) + target_include_directories(bridge-test PRIVATE ui) + target_link_libraries(bridge-test PRIVATE Qt6::Core Qt6::Gui Qt6::Test) + add_dependencies(bridge-test core) +- add_test(NAME bridge COMMAND bridge-test) ++ add_test(NAME bridge COMMAND bridge-test -nocrashhandler) ++ set_tests_properties(bridge PROPERTIES TIMEOUT 60) + endif() + if(BUILD_TESTING) + qt_add_executable(ui-test tests/ui_test.cpp ui/bridge.cpp ui/bridge.h) +@@ -37,5 +38,6 @@ if(BUILD_TESTING) + qt_add_resources(ui-test test_qml PREFIX "/" FILES ${TASK_MANAGER_QML}) + target_link_libraries(ui-test PRIVATE Qt6::Core Qt6::Gui Qt6::Quick Qt6::Qml Qt6::QuickControls2 Qt6::Test) + add_dependencies(ui-test core) +- add_test(NAME ui COMMAND ui-test) ++ add_test(NAME ui COMMAND ui-test -nocrashhandler) ++ set_tests_properties(ui PROPERTIES TIMEOUT 60) + endif() +diff --git a/tests/bridge_test.cpp b/tests/bridge_test.cpp +index 9bed7be..d6737a4 100644 +--- a/tests/bridge_test.cpp ++++ b/tests/bridge_test.cpp +@@ -3,9 +3,46 @@ + #include + #include + #include ++#include ++#include + class BridgeTest : public QObject { + Q_OBJECT + private slots: ++ void shutdownDoesNotPublish_data() { ++ QTest::addColumn("inFlight"); ++ QTest::addColumn("stopped"); ++ QTest::newRow("idle") << false << false; ++ QTest::newRow("sample-in-flight") << true << false; ++ QTest::newRow("stopped-worker") << true << true; ++ } ++ void shutdownDoesNotPublish() { ++ QFETCH(bool, inFlight); ++ QFETCH(bool, stopped); ++ auto bridge = std::make_unique(); ++ QTRY_VERIFY_WITH_TIMEOUT(!bridge->snapshot().isEmpty(), 8000); ++ bridge->m_timer.stop(); ++ QTRY_VERIFY_WITH_TIMEOUT(!bridge->busy(), 8000); ++ // Stop only the disposable worker owned by this bridge, forcing shutdown ++ // through terminate/kill rather than the normal stdin-EOF exit. ++ if (stopped) { ++ const auto workerPid = bridge->m_worker.processId(); ++ QVERIFY(workerPid > 0); ++ QCOMPARE(::kill(workerPid, SIGSTOP), 0); ++ } ++ if (inFlight) { ++ bridge->refresh(); ++ QVERIFY(bridge->busy()); ++ } ++ QSignalSpy snapshots(bridge.get(), &Bridge::snapshotChanged); ++ QSignalSpy statuses(bridge.get(), &Bridge::statusChanged); ++ QSignalSpy busy(bridge.get(), &Bridge::busyChanged); ++ // Closing the app must not publish late samples or worker-exit errors while ++ // its bridge and UI are being destroyed. Spies outlive the bridge on purpose. ++ bridge.reset(); ++ QCOMPARE(snapshots.count(), 0); ++ QCOMPARE(statuses.count(), 0); ++ QCOMPARE(busy.count(), 0); ++ } + void inspectionErrorsAndDismissal() { + Bridge bridge; + QTRY_VERIFY_WITH_TIMEOUT(!bridge.snapshot().isEmpty(), 8000); +diff --git a/ui/bridge.cpp b/ui/bridge.cpp +index 86b5b47..b1fb303 100644 +--- a/ui/bridge.cpp ++++ b/ui/bridge.cpp +@@ -82,7 +82,12 @@ Bridge::Bridge(QObject *p) : QObject(p), m_rows(this) { + m_clock.start(); + } + Bridge::~Bridge() { ++ // waitForFinished() can emit readyRead/error/finished synchronously. Quiesce ++ // callbacks before waiting, including if QProcess outlives our final wait and ++ // emits from its destructor after the other Bridge members are gone. + m_timer.stop(); ++ m_timeout.stop(); ++ m_worker.disconnect(this); + m_worker.closeWriteChannel(); + if (!m_worker.waitForFinished(600)) { + m_worker.terminate();