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 +) 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. 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" +} diff --git a/plugins/sourceos/src/sourceos.cpp b/plugins/sourceos/src/sourceos.cpp new file mode 100644 index 00000000..46007286 --- /dev/null +++ b/plugins/sourceos/src/sourceos.cpp @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2026 SociOS-Linux +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace { + +static inline double clamp01(double s) +{ + if (s < 0.0) return 0.0; + if (s > 1.0) return 1.0; + return s; +} + +static inline double matchScore(const QString &query, const QString &candidate) +{ + albert::Matcher m(candidate); + const albert::Match mm = m.match(query); + if (!mm) + return 0.0; + return clamp01(mm.score()); +} + +} // 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 + { + (void)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 + { + const QString q = context.query().trimmed(); + const bool empty = q.isEmpty(); + + struct ActionDef { + 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"}, + {"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"}}, + }; + + std::vector out; + out.reserve(actions.size()); + + for (const auto &a : actions) { + double score = 0.0; + + 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_factory = [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_factory, + std::move(acts), + a.key + ); + + out.emplace_back(item, score); + } + + return out; + } +};