From 8c2109042961244bffa78be55e23f567f199d328 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:07:59 -0400 Subject: [PATCH 1/5] plugins/sourceos: add CMakeLists --- plugins/sourceos/CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 plugins/sourceos/CMakeLists.txt diff --git a/plugins/sourceos/CMakeLists.txt b/plugins/sourceos/CMakeLists.txt new file mode 100644 index 00000000..62178a2a --- /dev/null +++ b/plugins/sourceos/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.19) + +project(sourceos + VERSION 0.1.0 + DESCRIPTION "SourceOS action bus plugin" +) + +# In-tree build: FindAlbert.cmake in the build directory provides the macros. +# Standalone build: find_package(Albert) resolves from system install. +find_package(Albert REQUIRED) + +# Minimal plugin: no UI config yet. +albert_plugin( + QT Core +) From 47d0a218b39812e618a9e957a764339dc730d4be Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:33:32 -0400 Subject: [PATCH 2/5] plugins/sourceos: add metadata --- plugins/sourceos/metadata.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 plugins/sourceos/metadata.json diff --git a/plugins/sourceos/metadata.json b/plugins/sourceos/metadata.json new file mode 100644 index 00000000..71a11145 --- /dev/null +++ b/plugins/sourceos/metadata.json @@ -0,0 +1,13 @@ +{ + "name": "SourceOS", + "description": "SourceOS workstation actions (doctor, profiles, tools)", + "license": "MIT", + "url": "https://github.com/SociOS-Linux/albert", + "readme_url": "https://github.com/SociOS-Linux/albert/tree/dev/plugins/sourceos", + "authors": ["SociOS-Linux"], + "maintainers": ["@mdheller"], + "binary_dependencies": ["sourceos"], + "runtime_dependencies": [], + "plugin_dependencies": [], + "loadtype": "user" +} From 250cc222929571150162ea415c2b254d07a6ea30 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:45:00 -0400 Subject: [PATCH 3/5] plugins/sourceos: add README --- plugins/sourceos/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 plugins/sourceos/README.md diff --git a/plugins/sourceos/README.md b/plugins/sourceos/README.md new file mode 100644 index 00000000..a2a4604f --- /dev/null +++ b/plugins/sourceos/README.md @@ -0,0 +1,24 @@ +# SourceOS Albert plugin + +This plugin adds **keyboard-first SourceOS workstation actions** to Albert. + +Design goals: + +- Treat Albert as an **action bus** (command palette), not as a second filesystem index. +- Keep actions auditable and compatible with SourceOS execution boundaries. + +## Trigger + +Default trigger: `sourceos ` + +## Actions (v0) + +- `doctor` → runs `sourceos doctor workstation-v0` +- `profile apply` → runs `sourceos profile apply workstation-v0` +- `k9s` → launches `k9s` +- `lazygit` → launches `lazygit` + +## Notes + +- This plugin assumes the `sourceos` CLI is installed on PATH (installed by workstation profile tooling). +- A future iteration will add richer parameterization and optional GNOME portal gating. From 6a592b0107e7f402469f38dd369c9bd096fd0934 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:55:21 -0400 Subject: [PATCH 4/5] plugins/sourceos: add initial SourceOS action plugin --- plugins/sourceos/src/sourceos.cpp | 133 ++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 plugins/sourceos/src/sourceos.cpp diff --git a/plugins/sourceos/src/sourceos.cpp b/plugins/sourceos/src/sourceos.cpp new file mode 100644 index 00000000..7f84e73f --- /dev/null +++ b/plugins/sourceos/src/sourceos.cpp @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2026 SociOS-Linux +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +static inline double scoreMatch(const albert::Match &m) +{ + if (!m) + return 0.0; + // m.score() is [0,1] for matches; clamp defensively. + const double s = m.score(); + return s < 0.0 ? 0.0 : (s > 1.0 ? 1.0 : s); +} + +static inline std::vector keywords(const QString &s) +{ + // Keep it simple: keyword list used by matcher scoring. + // (We can evolve to richer tokenization later.) + return {s}; +} + +} // namespace + +class SourceOSPlugin final : public albert::ExtensionPlugin, + public albert::RankedQueryHandler +{ + ALBERT_PLUGIN + +public: + SourceOSPlugin() = default; + + QString id() const override { return "sourceos"; } + QString name() const override { return "SourceOS"; } + QString description() const override { return "SourceOS workstation actions (doctor, profiles, tools)"; } + + QString synopsis(const QString &query) const override + { + Q_UNUSED(query); + return "doctor | profile apply | k9s | lazygit"; + } + + QString defaultTrigger() const override + { + return "sourceos "; + } + + bool supportsFuzzyMatching() const override + { + return true; + } + + std::vector rankItems(albert::QueryContext &context) override + { + // This runs in a worker thread (RankedQueryHandler contract). + const QString q = context.query().trimmed(); + + struct ActionDef { + QString key; // query keyword + QString title; // item label + QString subtitle; // item subtext + QString grapheme; // icon + QStringList cmd; // commandline + }; + + const std::vector actions = { + {"doctor", "SourceOS: doctor", "Run workstation checks (workstation-v0)", "🩺", {"sourceos", "doctor", "workstation-v0"}}, + {"apply", "SourceOS: profile apply", "Apply workstation profile (workstation-v0)", "🧰", {"sourceos", "profile", "apply", "workstation-v0"}}, + {"k9s", "Open k9s", "Kubernetes TUI", "⎈", {"k9s"}}, + {"lazygit", "Open lazygit", "Git TUI", "🌿", {"lazygit"}}, + }; + + // Empty query: show all actions with score 0. + const bool empty = q.isEmpty(); + + std::vector out; + out.reserve(actions.size()); + + for (const auto &a : actions) { + double score = 0.0; + if (!empty) { + albert::Matcher m(a.key); + score = scoreMatch(m.match(q)); + if (score <= 0.0) + continue; + } + + auto icon = [g=a.grapheme]() { + return albert::Icon::grapheme(g, 1.0); + }; + + std::vector acts; + acts.push_back(albert::Action{ + "run", + "Run", + [cmd=a.cmd]() { + albert::runDetachedProcess(cmd); + }, + true + }); + + auto item = albert::StandardItem::make( + QString("sourceos.%1").arg(a.key), + a.title, + a.subtitle, + icon, + std::move(acts), + a.key + ); + + // RankedQueryHandler expects score in (0,1]. For empty query we use 0, but the + // docs say empty string yields score 0; core handles this. + out.emplace_back(item, empty ? 0.0 : score); + } + + return out; + } +}; + +ALBERT_PLUGIN + +// Qt plugin entry point +#include "sourceos.moc" From d4d9441a6dfdc61d4bfef9624dad6bee146ca699 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:00:11 -0400 Subject: [PATCH 5/5] plugins/sourceos: fix plugin entry + improve matching --- plugins/sourceos/src/sourceos.cpp | 99 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/plugins/sourceos/src/sourceos.cpp b/plugins/sourceos/src/sourceos.cpp index 7f84e73f..46007286 100644 --- a/plugins/sourceos/src/sourceos.cpp +++ b/plugins/sourceos/src/sourceos.cpp @@ -10,24 +10,26 @@ #include #include + +#include #include namespace { -static inline double scoreMatch(const albert::Match &m) +static inline double clamp01(double s) { - if (!m) - return 0.0; - // m.score() is [0,1] for matches; clamp defensively. - const double s = m.score(); - return s < 0.0 ? 0.0 : (s > 1.0 ? 1.0 : s); + if (s < 0.0) return 0.0; + if (s > 1.0) return 1.0; + return s; } -static inline std::vector keywords(const QString &s) +static inline double matchScore(const QString &query, const QString &candidate) { - // Keep it simple: keyword list used by matcher scoring. - // (We can evolve to richer tokenization later.) - return {s}; + albert::Matcher m(candidate); + const albert::Match mm = m.match(query); + if (!mm) + return 0.0; + return clamp01(mm.score()); } } // namespace @@ -46,7 +48,7 @@ class SourceOSPlugin final : public albert::ExtensionPlugin, QString synopsis(const QString &query) const override { - Q_UNUSED(query); + (void)query; return "doctor | profile apply | k9s | lazygit"; } @@ -62,40 +64,66 @@ class SourceOSPlugin final : public albert::ExtensionPlugin, std::vector rankItems(albert::QueryContext &context) override { - // This runs in a worker thread (RankedQueryHandler contract). const QString q = context.query().trimmed(); + const bool empty = q.isEmpty(); struct ActionDef { - QString key; // query keyword - QString title; // item label - QString subtitle; // item subtext - QString grapheme; // icon - QStringList cmd; // commandline + QString key; + QString title; + QString subtitle; + QString grapheme; + QStringList cmd; + std::vector terms; }; const std::vector actions = { - {"doctor", "SourceOS: doctor", "Run workstation checks (workstation-v0)", "🩺", {"sourceos", "doctor", "workstation-v0"}}, - {"apply", "SourceOS: profile apply", "Apply workstation profile (workstation-v0)", "🧰", {"sourceos", "profile", "apply", "workstation-v0"}}, - {"k9s", "Open k9s", "Kubernetes TUI", "⎈", {"k9s"}}, - {"lazygit", "Open lazygit", "Git TUI", "🌿", {"lazygit"}}, + {"doctor", + "SourceOS: doctor", + "Run workstation checks (workstation-v0)", + "🩺", + {"sourceos", "doctor", "workstation-v0"}, + {"doctor", "check", "diagnose", "health"}}, + + {"apply", + "SourceOS: profile apply", + "Apply workstation profile (workstation-v0)", + "🧰", + {"sourceos", "profile", "apply", "workstation-v0"}, + {"apply", "profile", "install", "bootstrap"}}, + + {"k9s", + "Open k9s", + "Kubernetes TUI", + "⎈", + {"k9s"}, + {"k9s", "kube", "kubernetes"}}, + + {"lazygit", + "Open lazygit", + "Git TUI", + "🌿", + {"lazygit"}, + {"lazygit", "git"}}, }; - // Empty query: show all actions with score 0. - const bool empty = q.isEmpty(); - std::vector out; out.reserve(actions.size()); for (const auto &a : actions) { double score = 0.0; - if (!empty) { - albert::Matcher m(a.key); - score = scoreMatch(m.match(q)); + + if (empty) { + // Be conservative: RankItem doc says (0,1] even though empty queries are a special case. + score = 0.01; + } else { + for (const auto &term : a.terms) + score = std::max(score, matchScore(q, term)); + if (score <= 0.0) continue; } - auto icon = [g=a.grapheme]() { + auto icon_factory = [g=a.grapheme]() { return albert::Icon::grapheme(g, 1.0); }; @@ -103,9 +131,7 @@ class SourceOSPlugin final : public albert::ExtensionPlugin, acts.push_back(albert::Action{ "run", "Run", - [cmd=a.cmd]() { - albert::runDetachedProcess(cmd); - }, + [cmd=a.cmd]() { albert::runDetachedProcess(cmd); }, true }); @@ -113,21 +139,14 @@ class SourceOSPlugin final : public albert::ExtensionPlugin, QString("sourceos.%1").arg(a.key), a.title, a.subtitle, - icon, + icon_factory, std::move(acts), a.key ); - // RankedQueryHandler expects score in (0,1]. For empty query we use 0, but the - // docs say empty string yields score 0; core handles this. - out.emplace_back(item, empty ? 0.0 : score); + out.emplace_back(item, score); } return out; } }; - -ALBERT_PLUGIN - -// Qt plugin entry point -#include "sourceos.moc"