Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/omarchy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# The Omarchy plugin cannot be loaded on this runner (no omarchy-shell).
# What can be checked offline is the contract verify.sh owns: manifest
# shape, no SQLite, no display-name filters, and the real query against
# examples/demo.db. Registered the same way .github/workflows/scoop.yml
# and .github/workflows/aur.yml register their verify scripts: path-scoped,
# because nothing outside contrib/omarchy/ can change the answer.
#
# verify.sh builds gadak, so this job needs Go. It does not compare git
# tags, so checkout stays shallow (the AUR workflow sets fetch-depth: 0
# only because check-pkgver.sh reads `git describe --tags`).
name: Omarchy

on:
pull_request:
paths: ['contrib/omarchy/**', '.github/workflows/omarchy.yml']
push:
branches: [main]
paths: ['contrib/omarchy/**', '.github/workflows/omarchy.yml']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: omarchy-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
plugin:
name: Omarchy plugin offline checks
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Offline plugin checks
run: ./contrib/omarchy/verify.sh
156 changes: 156 additions & 0 deletions contrib/omarchy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Omarchy shell plugin

A bar widget for [omarchy-shell](https://omarchy.org/manual/shell-plugins/)
that answers one question offline: how much of the local gadak mirror is
open, and how much of that has been sitting in the same status for more
than seven days.

This is not a “my assigned tickets” widget. A standalone workspace has no
account (`gadak init --standalone` clears `AccountID`), and the competing
community plugins (`tmn73.jira`, Linear, Todoist, ClickUp, `37signals.basecamp`)
all need a cloud token and a live network. This one runs `gadak sql --json`
against the on-disk mirror and nothing else.

**No Omarchy machine has run this plugin yet.** The gate below is offline
(manifest, grep contracts, the real query against `examples/demo.db`). Load,
placement, click-to-open, and `omarchy-plugin-validate` on a guest are
unproven.

## What the widget shows

Two numbers in a short bar label, `open·stuck`:

| number | meaning |
| --- | --- |
| **open** | rows in `issues_full` whose `status_category` is not `done` |
| **stuck** | those same rows whose `status_changed_at` is older than 7 days |

`status_category` is one of `new`, `inprogress`, `done`. Do not rewrite the
query to filter on a status display name — that is empty on a Korean-language
account (`AGENTS.md`, `CLAUDE.md`). `time-in-status` is not a stored column;
age is `julianday('now') - julianday(status_changed_at)`. NULL
`status_changed_at` is not counted as stuck.

The query lives in one file: [`gadak/query.sql`](gadak/query.sql).
`BarWidget.qml` runs that file through `gadak sql --json`. `verify.sh`
runs the same file. There is no second copy of the SQL.

`--json` is NDJSON (one object per line, `cmd/gadak/sql.go`). A stale-mirror
warning goes to stderr; the widget parses stdout only.

### Degrade states (never an empty badge)

| condition | badge | tooltip |
| --- | --- | --- |
| `gadak` not on `PATH` | `no gadak` | install the Linux tarball from GitHub Releases |
| `gadak` exits with `no mirror at … — run gadak sync` (`cmd/gadak/sql.go`) | `not synced` | run `gadak sync` |
| non-zero exit, or stdout is not NDJSON with `open`/`stuck` | `sql err` | the command failed |
| first poll still in flight | `…` | reading |
| success | `12·3` | `12 open · 3 stuck >7d` |

Left click opens `http://127.0.0.1:7777`. That is the default `--addr` of
`gadak serve` (`cmd/gadak/serve.go:41`). Right click re-runs the query.

`gadak://` is not the click target. `gadak views open` prints a
`gadak://view/…` deeplink (`cmd/gadak/views.go` `deepLinkURL`) that needs
the macOS app bundle to register the scheme. Nothing on this desktop does.
The bind is still `127.0.0.1:7777`; `gadak.localhost:7777` is only a
display URL when the resolver maps that name to loopback
(`cmd/gadak/main.go` `prettyOpenURL`).

The plugin makes no outbound network call. Telemetry is forbidden
(`SECURITY.md`).

## Data contract

Read **only** with `gadak sql --json`. The mirror file is a disposable
cache whose schema is not a 0.x promise (`specs/000-product/data-model.md`).
Opening it from QML would break on a migration and can read a half-written
file mid-sync. The three 0.x promises are `issues_full` + the RECIPES
queries, the `gadak sql` stdout format, and `views open --keys -`.

## Install

On an Omarchy machine, from a clone of this repository:

```bash
bash contrib/omarchy/install.sh
```

The script:

- refuses with one line if `/etc/os-release` `ID` is not `omarchy`
(`docs/runbooks/omarchy-vm.md`)
- copies `gadak/` to `~/.config/omarchy/plugins/io.github.midagedev.gadak/`
(a copy, not a symlink — `omarchy-plugin-validate` rejects symlinks
inside a plugin folder)
- runs `omarchy-plugin-validate` when that CLI exists
- `omarchy-shell shell rescanPlugins` then `omarchy-plugin-enable`
- offers `omarchy-webapp-install gadak http://127.0.0.1:7777 web-browser`
(three arguments; fewer opens an interactive `gum` prompt —
`bin/omarchy-webapp-install`)
- prints `gadak install-service` as the way to keep `serve` up (systemd
**user** unit on Linux; `cmd/gadak/service.go`). It does not install
that unit itself.

`omarchy-plugin-clone` clones a **built-in** plugin into
`~/.config/omarchy/plugins/<user>.<id>/`. It is the wrong verb here.
`omarchy-plugin-add` clones a git URL whose root is a `manifest.json`;
this plugin is a subdirectory of the gadak repo, so add cannot see it
until it is published as its own repository.

If `gadak` is missing, the script prints the install options that exist
today and still copies the plugin (the badge will say `no gadak`):

- **not** an AUR package — new AUR registration is closed
(`docs/INSTALL.md`)
- Linux tarball from
<https://github.com/midagedev/gadak/releases/latest>:
`gadak_<version>_linux_amd64.tar.gz` / `linux_arm64` plus
`checksums.txt` (`README.md`)
- `brew install midagedev/tap/gadak-cli`
- in-repo `contrib/aur/gadak-bin` + `makepkg -si` (fetches that tarball)

Idempotent: a second run overwrites the copy, skips enable if already
enabled, skips the desktop file if it exists.

```bash
bash contrib/omarchy/uninstall.sh
```

Disables and removes the plugin (`omarchy-plugin-remove --yes`) and the
`gadak.desktop` web app. It does not remove the gadak binary or an
`install-service` unit.

The hyphenated CLIs (`omarchy-plugin-enable`, `-validate`, `-remove`,
`-list`) live in `basecamp/omarchy` `bin/` on branch `quattro`.
`omarchy plugin …` is the documented dispatcher for the same scripts
(`omarchy:alias` headers). This recipe calls the hyphenated names.

Plugins run **unsandboxed** inside `omarchy-shell`. Read the plugin
before you enable it
([Omarchy manual](https://omarchy.org/manual/shell-plugins/)).

## Offline gate

```bash
bash contrib/omarchy/verify.sh
```

Runs on macOS and Linux with no Omarchy present. Builds this repo's
`gadak`, points a temp `GADAK_HOME` at a copy of `examples/demo.db`,
and executes `gadak/query.sql`. CI:
[`.github/workflows/omarchy.yml`](../../.github/workflows/omarchy.yml)
(`actions/setup-go` + this script, path-scoped like the Scoop and AUR
workflows). `qmllint` is skipped with a printed reason when the tool
is absent.

## Community directory (later, not this round)

Listing at [omarchyplugins.com](https://omarchyplugins.com) /
[HANCORE-linux/omarchy-plugin-marketplace](https://github.com/HANCORE-linux/omarchy-plugin-marketplace)
is a lead step after this directory exists. Submission is an issue form
plus maintainer review. The marketplace states that it does **not**
security-audit plugins; they run as unsandboxed code. A listing also
wants a standalone public git repo with `manifest.json` at the root,
which this in-repo path is not.
147 changes: 147 additions & 0 deletions contrib/omarchy/gadak/BarWidget.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui

// Bar label: "<open>·<stuck>" from `gadak sql --json` of query.sql.
// Data enters only through that command; this file never opens the mirror.
// Click opens the gadak serve default bind (cmd/gadak/serve.go:41), not
// gadak:// — that scheme is registered by the macOS app bundle only
// (cmd/gadak/views.go deepLinkURL).
BarWidget {
id: root
moduleName: "io.github.midagedev.gadak"

// serve.go default --addr. Named here so the port is not a magic number.
readonly property string serveUrl: "http://127.0.0.1:7777"
readonly property string queryPath: String(Qt.resolvedUrl("query.sql")).replace(/^file:\/\//, "")

// loading | ok | no-gadak | not-synced | sql-err.
// Not named `state`: QQuickItem already declares that property, and
// redeclaring it is a QML compile error we have no local qmllint to catch.
property string viewState: "loading"
property int openCount: 0
property int stuckCount: 0

readonly property string displayText: {
if (viewState === "ok") return openCount + "·" + stuckCount
if (viewState === "no-gadak") return "no gadak"
if (viewState === "not-synced") return "not synced"
if (viewState === "sql-err") return "sql err"
return "…"
}

readonly property string tooltipText: {
if (viewState === "ok")
return openCount + " open · " + stuckCount + " stuck >7d"
if (viewState === "no-gadak")
return "gadak is not on PATH. Install the Linux tarball from https://github.com/midagedev/gadak/releases/latest (gadak_<version>_linux_amd64.tar.gz or linux_arm64, plus checksums.txt)."
if (viewState === "not-synced")
return "no mirror — run gadak sync"
if (viewState === "sql-err")
return "gadak sql --json failed or was not NDJSON"
return "reading gadak sql --json"
}

function refresh() {
if (queryProc.running) return
if (queryPath === "") {
viewState = "sql-err"
return
}
// One argv is the query file; stdout is NDJSON. stderr is ignored for parse
// (a stale-mirror warning is expected there).
queryProc.command = ["bash", "-c", "gadak sql --json \"$(cat \"$1\")\"", "gadak-omarchy", queryPath]
queryProc.running = true
}

function openGadak() {
if (root.bar && typeof root.bar.run === "function") {
root.bar.run("omarchy-launch-webapp " + serveUrl)
return
}
Quickshell.execDetached(["omarchy-launch-webapp", serveUrl])
}

function applyResult(exitCode, stdout, stderr) {
var err = String(stderr || "")
var out = String(stdout || "").trim()
var code = Number(exitCode)

// "no mirror" first: gadak ran, so it is not the missing-binary case.
if (/no mirror/i.test(err)) {
viewState = "not-synced"
return
}
// 127 is the shell's "command not found". The message test names gadak
// explicitly, because `cat` reports a missing query.sql with the same
// "No such file or directory" and that is a broken install, not a
// missing binary.
if (code === 127 || /gadak: command not found/i.test(err)) {
viewState = "no-gadak"
return
}
if (code !== 0) {
viewState = "sql-err"
return
}

var line = out.split("\n")[0] || ""
var obj
try {
obj = JSON.parse(line)
} catch (e) {
viewState = "sql-err"
return
}
if (!obj || typeof obj !== "object" || obj.open === undefined || obj.stuck === undefined) {
viewState = "sql-err"
return
}
var o = Number(obj.open)
var s = Number(obj.stuck)
if (!isFinite(o) || !isFinite(s)) {
viewState = "sql-err"
return
}
openCount = o
stuckCount = s
viewState = "ok"
}

implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight

Timer {
interval: 60000
repeat: true
running: true
triggeredOnStart: true
onTriggered: root.refresh()
}

Process {
id: queryProc
running: false
command: []
stdout: StdioCollector { id: queryOut; waitForEnd: true }
stderr: StdioCollector { id: queryErr; waitForEnd: true }
onExited: function (exitCode) {
var code = (exitCode === undefined || exitCode === null) ? queryProc.exitCode : exitCode
root.applyResult(code, String(queryOut.text || ""), String(queryErr.text || ""))
}
}

WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.displayText
tooltipText: root.tooltipText
onPressed: function (b) {
if (b === Qt.RightButton) root.refresh()
else root.openGadak()
}
}
}
21 changes: 21 additions & 0 deletions contrib/omarchy/gadak/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"id": "io.github.midagedev.gadak",
"name": "gadak",
"version": "0.1.0",
"author": "midagedev",
"description": "Open and stuck issue counts from the local gadak mirror, offline",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "BarWidget.qml"
},
"barWidget": {
"displayName": "gadak",
"description": "Open and stuck counts from `gadak sql --json` (no network)",
"category": "Info",
"allowMultiple": false,
"defaultSection": "right"
}
}
15 changes: 15 additions & 0 deletions contrib/omarchy/gadak/query.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Single owner of the bar-widget query. BarWidget.qml and verify.sh both
-- run this file through `gadak sql --json`. Do not open the mirror file.
--
-- open = issues whose status_category is not `done`
-- stuck = those same rows whose status_changed_at is older than 7 days
-- (time-in-status is not a stored column; compute it here)
-- NULL status_changed_at cannot be aged, so it is not stuck.
SELECT
COUNT(*) AS open,
COALESCE(SUM(CASE
WHEN status_changed_at IS NOT NULL
AND julianday('now') - julianday(status_changed_at) > 7
THEN 1 ELSE 0 END), 0) AS stuck
FROM issues_full
WHERE status_category != 'done';
Loading
Loading