Sync config from arch

- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
This commit is contained in:
asepharyana
2026-09-23 15:19:12 +07:00
commit 1cdb82a76f
300 changed files with 78143 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import QtQuick
import Quickshell
// Standalone harness for the background auto-check logic added to
// Panel.qml (autoCheckEnabled, autoCheckIntervalHours, and the
// pendingUpdateCount filter). Panel.qml itself only loads inside the full
// Omarchy shell (qs.Ui, qs.Commons, the live PluginRegistry), so this copies
// the pure expressions to exercise them in isolation. Keep the three
// `readonly property` blocks below byte-for-byte in sync with Panel.qml if
// that logic changes.
ShellRoot {
id: root
property var settings: {
var raw = Quickshell.env("OMAPLUG_TEST_SETTINGS")
return raw ? JSON.parse(raw) : {}
}
// Mirrors qs.Ui/Panel.qml's setting(name, fallback).
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
// Kept byte-identical to Panel.qml by auto-check-test.sh's sync guard;
// edit both or the build fails.
// AUTOCHECK-SETTINGS-BEGIN
readonly property bool autoCheckEnabled: root.setting("autoCheckUpdates", false) === true
// real, not int: an int property truncates any fractional hours value
// (e.g. 0.5) towards zero, which would silently turn into a zero-interval
// Timer below and spin checkUpdates() in a tight loop.
readonly property real autoCheckIntervalHours: {
var hours = Number(root.setting("autoCheckIntervalHours", 6))
return (isFinite(hours) && hours > 0) ? hours : 6
}
// AUTOCHECK-SETTINGS-END
property var updateStates: JSON.parse(Quickshell.env("OMAPLUG_TEST_UPDATE_STATES") || "{}")
property var pluginRepos: JSON.parse(Quickshell.env("OMAPLUG_TEST_PLUGIN_REPOS") || "{}")
// Kept byte-identical to Panel.qml by auto-check-test.sh's sync guard;
// edit both or the build fails.
// PENDING-UPDATE-COUNT-BEGIN
readonly property int pendingUpdateCount: {
var n = 0
for (var k in root.updateStates) {
if (root.pluginRepos[k] === undefined) continue
if (root.updateStates[k] === "UPDATE") n++
}
n
}
// PENDING-UPDATE-COUNT-END
Component.onCompleted: {
console.log("RESOLVED_ENABLED", root.autoCheckEnabled)
console.log("RESOLVED_HOURS", root.autoCheckIntervalHours)
console.log("RESOLVED_PENDING", root.pendingUpdateCount)
}
// The property under test: same shape as Panel.qml's autoUpdateCheckTimer.
// running/interval stay bound to autoCheckEnabled/autoCheckIntervalHours so
// a live settings change (see toggleTimer below) reaches this Timer the
// same way persistAutoCheckSetting()'s write to root.settings does in the
// real Panel.qml - no restart, just the binding re-evaluating.
Timer {
interval: root.autoCheckIntervalHours * 3600000
running: root.autoCheckEnabled
repeat: true
triggeredOnStart: true
onTriggered: console.log("TIMER_FIRED")
}
// Simulates the user flipping the panel's toggle (or picking a new
// interval) while the shell is already running: OMAPLUG_TEST_TOGGLE_AFTER_MS
// schedules a one-shot settings mutation partway through the run, proving
// the Timer above reacts live rather than only at startup.
property real toggleAfterMs: Number(Quickshell.env("OMAPLUG_TEST_TOGGLE_AFTER_MS") || "0")
Timer {
interval: root.toggleAfterMs
running: root.toggleAfterMs > 0
onTriggered: {
var next = {}
for (var k in root.settings) next[k] = root.settings[k]
next.autoCheckUpdates = Quickshell.env("OMAPLUG_TEST_TOGGLE_TO") === "true"
root.settings = next
console.log("TOGGLED_ENABLED", root.autoCheckEnabled)
}
}
// Bounds how long the process waits for triggers before exiting; must be
// well under any interval used above so a firing timer wins the race, and
// comfortably after toggleAfterMs so the post-toggle behavior is observed.
Timer {
interval: 1200
running: true
onTriggered: Qt.quit()
}
}
+21
View File
@@ -0,0 +1,21 @@
import QtQuick
import Quickshell
ShellRoot {
Component.onCompleted: {
Quickshell.execDetached([
Quickshell.env("OMAPLUG_TEST_HELPER"),
Quickshell.env("OMAPLUG_TEST_STATUS"),
"job-quickshell",
"slow",
"good"
])
exitTimer.start()
}
Timer {
id: exitTimer
interval: 50
onTriggered: Qt.quit()
}
}
@@ -0,0 +1,86 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
TMP=$(mktemp -d)
trap 'rm -rf -- "$TMP"' EXIT
SCRIPTDIR="$TMP/scriptdir"
mkdir -p -- "$SCRIPTDIR"
cp -- "$ROOT/auto-check-coordinator.sh" "$SCRIPTDIR/"
cp -- "$ROOT/runtime-state.py" "$SCRIPTDIR/"
chmod +x "$SCRIPTDIR/auto-check-coordinator.sh"
CALLS="$TMP/calls.log"
: > "$CALLS"
# Stands in for plugin-state.sh: records that it ran, takes long enough that
# a second, concurrent coordinator invocation is guaranteed to still find
# the lock held, and prints one fixed, recognizable result line.
cat > "$SCRIPTDIR/plugin-state.sh" <<EOF
#!/bin/bash
printf '%s\n' "\$\$" >> "$CALLS"
sleep 0.4
printf 'UPDATE\tfixture\thttps://example.invalid/fixture.git\tbehind\n'
EOF
chmod +x "$SCRIPTDIR/plugin-state.sh"
export XDG_RUNTIME_DIR="$TMP/xdg"
mkdir -p -- "$XDG_RUNTIME_DIR"
PLUGINS_DIR="$TMP/plugins"
mkdir -p -- "$PLUGINS_DIR"
EXPECTED=$'UPDATE\tfixture\thttps://example.invalid/fixture.git\tbehind'
# Two "monitors" ticking their auto-check timer at effectively the same
# moment. The second is staggered by 50ms - well under the stub's 400ms
# hold time - so it deterministically finds the lock already taken instead
# of racing for it, making this a real leader/follower run rather than a
# coin flip.
"$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR" > "$TMP/out1" 2>"$TMP/err1" &
pid1=$!
sleep 0.05
"$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR" > "$TMP/out2" 2>"$TMP/err2" &
pid2=$!
wait "$pid1"
wait "$pid2"
call_count=$(wc -l < "$CALLS")
if [[ $call_count -ne 1 ]]; then
printf 'FAIL: expected plugin-state.sh to run exactly once for 2 concurrent monitors, ran %s times\n' "$call_count" >&2
cat "$CALLS" >&2
exit 1
fi
for f in out1 out2; do
if [[ $(cat "$TMP/$f") != "$EXPECTED" ]]; then
printf 'FAIL: %s did not receive the leader'"'"'s result\n' "$f" >&2
printf 'got: %q\n' "$(cat "$TMP/$f")" >&2
cat "$TMP/err1" "$TMP/err2" >&2
exit 1
fi
done
# A lock directory left behind by a process that no longer exists (e.g.
# Panel.qml's checkWatchdog SIGKILLing a coordinator mid-run, which skips
# any EXIT trap) must not wedge every future auto-check.
: > "$CALLS"
LOCK="$XDG_RUNTIME_DIR/omaplug/auto-check.flock"
printf 'stale\n' > "$LOCK"
OUT=$("$SCRIPTDIR/auto-check-coordinator.sh" "$PLUGINS_DIR")
if [[ $OUT != "$EXPECTED" ]]; then
printf 'FAIL: stale lock (dead pid) blocked a fresh check instead of being reclaimed\n' >&2
printf 'got: %q\n' "$OUT" >&2
exit 1
fi
[[ $(wc -l < "$CALLS") -eq 1 ]] || {
printf 'FAIL: stale-lock recovery did not actually run plugin-state.sh\n' >&2
exit 1
}
# The flock inode is intentionally retained; the kernel releases ownership
# when the process exits, avoiding stale-PID deletion races.
printf 'auto-check-coordinator-test: ok\n'
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
if ! command -v quickshell >/dev/null 2>&1; then
printf 'auto-check-test: skipped (quickshell not installed)\n'
exit 0
fi
run() {
QT_QPA_PLATFORM=offscreen \
OMAPLUG_TEST_SETTINGS="${1:-{\}}" \
OMAPLUG_TEST_UPDATE_STATES="${2:-{\}}" \
OMAPLUG_TEST_PLUGIN_REPOS="${3:-{\}}" \
OMAPLUG_TEST_TOGGLE_AFTER_MS="${4:-0}" \
OMAPLUG_TEST_TOGGLE_TO="${5:-false}" \
quickshell --no-color -p "$ROOT/tests/AutoCheckLogic.qml" 2>&1
}
assert_contains() {
local needle="$1"
local haystack="$2"
local label="$3"
if [[ $haystack != *"$needle"* ]]; then
printf 'FAIL: %s: expected output to contain %q\n' "$label" "$needle" >&2
printf '%s\n' "$haystack" >&2
exit 1
fi
}
assert_not_contains() {
local needle="$1"
local haystack="$2"
local label="$3"
if [[ $haystack == *"$needle"* ]]; then
printf 'FAIL: %s: expected output to NOT contain %q\n' "$label" "$needle" >&2
printf '%s\n' "$haystack" >&2
exit 1
fi
}
# True if a TIMER_FIRED line appears strictly after the first line matching
# marker, i.e. after the point in the run's timeline where the setting changed.
found_fire_after() {
local marker="$1"
local haystack="$2"
awk -v m="$marker" '
$0 ~ m { seen=1; next }
seen && /TIMER_FIRED/ { found=1 }
END { exit !found }
' <<<"$haystack"
}
assert_no_fire_after() {
local marker="$1" haystack="$2" label="$3"
if found_fire_after "$marker" "$haystack"; then
printf 'FAIL: %s: TIMER_FIRED appeared after %q\n' "$label" "$marker" >&2
printf '%s\n' "$haystack" >&2
exit 1
fi
}
assert_fire_after() {
local marker="$1" haystack="$2" label="$3"
if ! found_fire_after "$marker" "$haystack"; then
printf 'FAIL: %s: expected TIMER_FIRED after %q but none seen\n' "$label" "$marker" >&2
printf '%s\n' "$haystack" >&2
exit 1
fi
}
# Pulls the text between "<marker>-BEGIN" and "<marker>-END" comment lines.
extract_block() {
local file="$1" marker="$2"
awk -v b="${marker}-BEGIN" -v e="${marker}-END" '
$0 ~ b { flag=1; next }
$0 ~ e { flag=0 }
flag
' "$file"
}
# Panel.qml only loads inside the full Omarchy shell, so AutoCheckLogic.qml
# duplicates its auto-check expressions to test them standalone (see the
# header comment in each file). A copy that's never re-checked against the
# original is worse than no test: it would keep passing while the real
# Panel.qml logic silently drifts. This fails loudly the moment they disagree.
assert_blocks_match() {
local marker="$1" label="$2"
local a b
a=$(extract_block "$ROOT/Panel.qml" "$marker")
b=$(extract_block "$ROOT/tests/AutoCheckLogic.qml" "$marker")
if [[ -z $a || -z $b ]]; then
printf 'FAIL: %s: marker %q missing from Panel.qml or AutoCheckLogic.qml\n' "$label" "$marker" >&2
exit 1
fi
if [[ $a != "$b" ]]; then
printf 'FAIL: %s: Panel.qml and tests/AutoCheckLogic.qml have drifted apart\n' "$label" >&2
diff <(printf '%s\n' "$a") <(printf '%s\n' "$b") >&2 || true
exit 1
fi
}
assert_blocks_match AUTOCHECK-SETTINGS auto-check-settings-in-sync
assert_blocks_match PENDING-UPDATE-COUNT pending-update-count-in-sync
# The sync guard above only proves the settings math agrees; it says nothing
# about what the real Timer's onTriggered calls. Pin that down directly so a
# future edit can't quietly swap in a new/duplicate check function instead of
# the one the manual "Check for updates" button already uses.
grep -Fq 'onTriggered: root.checkUpdates(root.autoCheckCoordinatorPath)' "$ROOT/Panel.qml" || {
printf 'FAIL: Panel.qml autoUpdateCheckTimer is not wired to the existing checkUpdates()\n' >&2
exit 1
}
# Defaults: disabled, 6h interval, stays quiet until enabled.
OUT=$(run '{}')
assert_contains 'RESOLVED_ENABLED false' "$OUT" default-disabled
assert_contains 'RESOLVED_HOURS 6' "$OUT" default-hours
assert_not_contains 'TIMER_FIRED' "$OUT" default-does-not-fire
# Explicitly disabled: never fires, even once, within the test window.
OUT=$(run '{"autoCheckUpdates":false}')
assert_contains 'RESOLVED_ENABLED false' "$OUT" disabled-flag
assert_not_contains 'TIMER_FIRED' "$OUT" disabled-does-not-fire
# Custom interval passes through unchanged.
OUT=$(run '{"autoCheckIntervalHours":3}')
assert_contains 'RESOLVED_HOURS 3' "$OUT" custom-hours
# A fractional value must survive as-is, not truncate to 0 (which an `int`
# property would silently do, turning into a zero-interval, tight-looping
# Timer - not reachable through the shipped 1/3/6/12/24h picker, but settings
# are hand-editable in shell.json).
OUT=$(run '{"autoCheckIntervalHours":0.5}')
assert_contains 'RESOLVED_HOURS 0.5' "$OUT" fractional-hours-not-truncated
# Zero, negative, and non-numeric intervals all fall back to 6h rather than
# producing a zero/negative-interval Timer (which would fire in a tight loop).
OUT=$(run '{"autoCheckIntervalHours":0}')
assert_contains 'RESOLVED_HOURS 6' "$OUT" zero-hours-fallback
OUT=$(run '{"autoCheckIntervalHours":-2}')
assert_contains 'RESOLVED_HOURS 6' "$OUT" negative-hours-fallback
OUT=$(run '{"autoCheckIntervalHours":"banana"}')
assert_contains 'RESOLVED_HOURS 6' "$OUT" non-numeric-hours-fallback
# pendingUpdateCount only counts UPDATE states for keys plugin-state.sh also
# reported a repo URL for (mirrors the real property's join against
# pluginRepos, which is what keeps a stale/removed plugin out of the count).
OUT=$(run '{}' '{"a":"UPDATE","b":"CURRENT","c":"UPDATE","d":"UPDATE"}' '{"a":"url","b":"url","d":"url"}')
assert_contains 'RESOLVED_PENDING 2' "$OUT" pending-count-filters-unknown-repo
# Live toggle off: this is what happens when the user flips the panel's
# switch off while the shell keeps running - no restart, so the same process
# must stop firing right away. A sub-second interval (0.0001h) guarantees a
# refire would land inside the test window if disabling didn't take effect.
OUT=$(run '{"autoCheckIntervalHours":0.0001}' '{}' '{}' 500 false)
assert_contains 'TOGGLED_ENABLED false' "$OUT" live-disable-toggled
assert_no_fire_after 'TOGGLED_ENABLED false' "$OUT" live-disable-stops-firing
# Live toggle on: starts disabled (proven quiet up to the toggle line), then
# flips on mid-run - the resumed Timer's triggeredOnStart must fire again
# immediately rather than waiting for the next shell restart.
OUT=$(run '{"autoCheckUpdates":false}' '{}' '{}' 300 true)
assert_not_contains 'TIMER_FIRED' "${OUT%%TOGGLED_ENABLED*}" live-enable-quiet-before-toggle
assert_contains 'TOGGLED_ENABLED true' "$OUT" live-enable-toggled
assert_fire_after 'TOGGLED_ENABLED true' "$OUT" live-enable-resumes-firing
printf 'auto-check-test: ok\n'
@@ -0,0 +1,51 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
PANEL="$ROOT/Panel.qml"
HELPER="$ROOT/marketplace-catalog.sh"
TMP=$(mktemp -d)
trap 'rm -rf -- "$TMP"' EXIT
grep -Fq 'Qt.resolvedUrl("marketplace-catalog.sh")' "$PANEL"
grep -Fq 'marketplaceProcess.command = [root.marketplaceHelperPath]' "$PANEL"
grep -Fq 'if (exitCode !== 0)' "$PANEL"
grep -Fq '!Array.isArray(catalog.plugins)' "$PANEL"
grep -Fq -- '--max-filesize 16777216' "$HELPER"
if grep -Fq 'head -c 4194304' "$PANEL"; then
printf 'FAIL: marketplace fetch must reject oversized catalogs, not truncate them\n' >&2
exit 1
fi
FIXTURE="$TMP/catalog.json"
jq -n '{
ignored: ("x" * 4500000),
plugins: [
{
id: "fixture",
verificationStatus: "verified",
verificationCommit: "abc",
upstreamObservedCommit: "def",
repositoryRelease: {url: "https://example.invalid/release"},
extra: ("y" * 10000)
}
]
}' > "$FIXTURE"
test "$(wc -c < "$FIXTURE")" -gt 4194304
SUMMARY=$("$HELPER" "file://$FIXTURE")
jq -e '.plugins | length == 1' <<< "$SUMMARY" >/dev/null
jq -e '.plugins[0] == {
id: "fixture",
verificationStatus: "verified",
verificationCommit: "abc",
verificationSnapshotStatus: null,
verificationCoverage: null,
upstreamObservedCommit: "def",
repositoryRelease: {url: "https://example.invalid/release"}
}' <<< "$SUMMARY" >/dev/null
test "$(wc -c <<< "$SUMMARY")" -lt 4096
printf 'catalog-fetch-test: ok\n'
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
PANEL="$ROOT/Panel.qml"
assert_contains() {
local needle="$1"
grep -Fq -- "$needle" "$PANEL" || {
printf 'FAIL: missing %q in %s\n' "$needle" "$PANEL" >&2
exit 1
}
}
assert_contains 'function leadingIconGlyph(text)'
assert_contains 'var first = s.charCodeAt(0)'
assert_contains 'return codePoint >= 0xF0000 && codePoint <= 0x10FFFD ? s.substring(0, 2) : ""'
assert_contains 'var live = root.leadingIconGlyph(root.liveGlyphFor(id))'
printf 'icon-glyph-test: ok\n'
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
HELPER="$ROOT/nested-widget-toggle.sh"
TASK_DIR=$(mktemp -d)
trap 'rm -rf -- "$TASK_DIR"' EXIT
export CALL_LOG="$TASK_DIR/calls"
export CONFIG BAR_FAIL=0
# Executable stubs also intercept the helper's final exec command.
omarchy-shell() {
[[ $* == 'shell listShellConfig' ]] || return 90
printf '%s\n' "$CONFIG"
}
omarchy() {
jq -cn --args '$ARGS.positional' -- "$@" >> "$CALL_LOG"
if [[ ${1:-} == bar && $BAR_FAIL == 1 ]]; then return 42; fi
}
mkdir "$TASK_DIR/bin"
printf '#!/bin/bash\n%s\nomarchy-shell "$@"\n' "$(declare -f omarchy-shell)" > "$TASK_DIR/bin/omarchy-shell"
printf '#!/bin/bash\n%s\nomarchy "$@"\n' "$(declare -f omarchy)" > "$TASK_DIR/bin/omarchy"
chmod +x "$TASK_DIR/bin/omarchy-shell" "$TASK_DIR/bin/omarchy"
export PATH="$TASK_DIR/bin:$PATH"
failures=0
check() {
local label=$1
shift
if ! "$@" > /dev/null; then
printf 'FAIL: %s\n' "$label" >&2
failures=$((failures + 1))
fi
}
run_helper() {
: > "$CALL_LOG"
result=0
bash "$HELPER" "$@" > "$TASK_DIR/stdout" 2> "$TASK_DIR/stderr" || result=$?
}
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["before","child","after","child"]}],"right":[{"id":"other","widgets":["untouched"]}]}}}'
run_helper child disable
check 'nested disable succeeds' test "$result" -eq 0
check 'remove only requested child before disabling it' jq -se '
. == [["bar","set","host","widgets","[\"before\",\"after\"]","--json"],
["plugin","disable","child"]]
' "$CALL_LOG"
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["other"]}]}}}'
run_helper standalone disable
check 'non-nested disable succeeds' test "$result" -eq 0
check 'non-nested disable does not modify hosts' jq -se '. == [["plugin","disable","standalone"]]' "$CALL_LOG"
CONFIG='{not valid json'
run_helper child disable
check 'invalid JSON fails' test "$result" -ne 0
check 'invalid JSON never disables a plugin' test ! -s "$CALL_LOG"
CONFIG='{"bar":{"layout":42}}'
run_helper child disable
check 'invalid layout fails' test "$result" -ne 0
check 'jq failure never disables a plugin' test ! -s "$CALL_LOG"
CONFIG='{"bar":{"layout":{"left":[{"id":"host","widgets":["child","other"]}]}}}'
BAR_FAIL=1
run_helper child disable
check 'host update error propagates' test "$result" -eq 42
check 'failed host update prevents plugin disable' jq -se '
. == [["bar","set","host","widgets","[\"other\"]","--json"]]
' "$CALL_LOG"
(( failures == 0 )) || exit 1
printf 'nested-widget-toggle-test: ok\n'
@@ -0,0 +1,115 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const vm = require('node:vm');
const { execFileSync } = require('node:child_process');
const source = fs.readFileSync(path.join(__dirname, '../Panel.qml'), 'utf8');
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'omaplug-metadata-'));
try {
const context = vm.createContext({
console, pluginRows: [], marketplaceMap: {},
pluginManifestProcess: {}, scanPluginRepos() {},
Quickshell: { env: () => temporary }
});
context.root = context;
for (const name of ['applyPluginList', 'applyPluginMetadata', 'mergeMarketplaceMetadata', 'applyMarketplaceCatalog']) {
const start = source.indexOf(' function ' + name + '(');
assert.notEqual(start, -1);
const end = source.indexOf('\n }', start) + 4;
vm.runInContext(source.slice(start, end), context);
}
const manifests = [
['shell/plugins/panels/clock/clock.manifest.json', 'omarchy.clock'],
['.config/omarchy/plugins/test plugin/manifest.json', 'test.plugin']
];
for (const [relative, id] of manifests) {
const file = path.join(temporary, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify({ id, description: 'Installed ' + id }));
}
const catalog = JSON.stringify([
{ id: 'omarchy.clock', firstParty: true, enabled: true, kinds: ['bar-widget'] },
{ id: 'test.plugin', enabled: false, kinds: ['panel'] },
{ id: 'catalog.only', enabled: true, kinds: ['panel'] }
]);
context.marketplaceMap = {
'test.plugin': { description: 'Marketplace description' },
'catalog.only': { description: 'Cached description' }
};
// Repeat to cover a refresh after the marketplace has already loaded.
for (let refresh = 0; refresh < 2; refresh++) {
context.applyPluginList(catalog);
const [command, ...args] = context.pluginManifestProcess.command;
const output = execFileSync(command, args, {
env: { ...process.env, HOME: temporary }, encoding: 'utf8'
});
context.applyPluginMetadata(output);
const rows = Object.fromEntries(context.pluginRows.map(row => [row.id, row]));
assert.equal(rows['omarchy.clock'].description, 'Installed omarchy.clock');
assert.equal(rows['test.plugin'].description, 'Installed test.plugin');
assert.equal(rows['catalog.only'].description, 'Cached description');
assert.equal(rows['test.plugin'].enabled, false);
assert.equal(rows['omarchy.clock'].enabled, true);
// A later marketplace response must not replace installed metadata.
context.mergeMarketplaceMetadata(context.marketplaceMap);
assert.equal(context.pluginRows.find(row => row.id === 'test.plugin').description,
'Installed test.plugin');
}
console.log('plugin-metadata-test: ok');
const page = fs.readFileSync(path.join(__dirname, '../panel/updates/Page.qml'), 'utf8');
const start = page.indexOf(' function verificationText(');
vm.runInContext(page.slice(start, page.indexOf('\n }', start) + 4), context);
context.applyMarketplaceCatalog(JSON.stringify({ plugins: [{
id: 'listed', verificationStatus: 'unverified',
verificationSnapshotStatus: 'verified', verificationCoverage: 'update-unverified'
}] }));
assert.equal(context.marketplaceMap.listed.snapshotStatus, 'update-unverified');
context.updateStates = { folder: 'UPDATE' };
context.marketplaceFetching = false;
context.marketplaceFetchFailed = false;
assert.equal(context.verificationText('listed', 'folder'), 'Update Unverified');
assert.equal(context.verificationText('unlisted', 'folder'), 'Not listed');
context.updateStates.folder = 'CURRENT';
assert.equal(context.verificationText('listed', 'folder'), 'Update Unverified');
context.marketplaceMap.listed.verified = true;
assert.equal(context.verificationText('listed', 'folder'), 'Verified');
context.marketplaceMap.listed.verified = false;
context.marketplaceMap.listed.snapshotStatus = '';
assert.equal(context.verificationText('listed', 'folder'), 'Unverified');
context.updateStates.folder = 'UPDATE';
assert.equal(context.verificationText('listed', 'folder'), 'Unverified');
console.log('update-verification-test: ok');
const reopenStart = source.indexOf(' function keepOpenAcrossRebuild(');
vm.runInContext(source.slice(reopenStart, source.indexOf('\n }', reopenStart) + 4), context);
context.keepOpenFlagWrite = {};
context.layoutPageOpen = true;
context.keepOpenAcrossRebuild();
assert.equal(context.keepOpenFlagWrite.command.at(-1), 'layout');
assert.equal(context.keepOpenFlagWrite.running, true);
context.layoutPageOpen = false;
context.keepOpenAcrossRebuild();
assert.equal(context.keepOpenFlagWrite.command.at(-1), 'main');
assert.match(source, /^ keepOpenFlagRead\.running = true$/m);
console.log('panel-reopen-test: ok');
const settingsStart = source.indexOf(' function persistAutoCheckSetting(');
vm.runInContext(source.slice(settingsStart, source.indexOf('\n }', settingsStart) + 4), context);
context.autoCheckSettingsProcess = { running: false };
context.moduleName = 'omaplug';
context.persistAutoCheckSetting({ autoCheckUpdates: false });
assert.deepEqual(Array.from(context.autoCheckSettingsProcess.command),
['omarchy', 'bar', 'set', 'omaplug', 'autoCheckUpdates', 'false', '--json']);
assert.equal(context.autoCheckSettingsProcess.running, true);
context.persistAutoCheckSetting({ autoCheckIntervalHours: 3 });
assert.equal(context.autoCheckSettingsProcess.command[4], 'autoCheckUpdates');
context.autoCheckSettingsProcess.running = false;
context.persistAutoCheckSetting({ autoCheckIntervalHours: 3 });
assert.deepEqual(Array.from(context.autoCheckSettingsProcess.command),
['omarchy', 'bar', 'set', 'omaplug', 'autoCheckIntervalHours', '3', '--json']);
console.log('auto-check-settings-cli-test: ok');
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
TMP=$(mktemp -d)
trap 'rm -rf -- "$TMP"' EXIT
PLUGINS="$TMP/plugins"
SEED="$TMP/seed"
REMOTE="$TMP/origin.git"
mkdir -p -- "$PLUGINS"
git init --bare -q "$REMOTE"
git -C "$REMOTE" symbolic-ref HEAD refs/heads/main
git init -q -b main "$SEED"
git -C "$SEED" config user.name "Omaplug Tests"
git -C "$SEED" config user.email "omaplug-tests@example.invalid"
printf '{"id":"fixture"}\n' > "$SEED/manifest.json"
git -C "$SEED" add manifest.json
git -C "$SEED" commit -qm "initial"
git -C "$SEED" remote add origin "$REMOTE"
# Fixture pushes must not inherit the user's global hooks. Those hooks may
# intentionally reject non-GitHub remotes, while this test uses a local bare
# repository by design.
git -C "$SEED" -c core.hooksPath=/dev/null push -q -u origin main
# Keep one checkout at the initial commit. It becomes behind after the second
# upstream commit, and a local commit on a copy of it creates a divergence.
git clone -q "$REMOTE" "$PLUGINS/behind"
git clone -q "$REMOTE" "$PLUGINS/diverged"
printf '{"id":"fixture","version":2}\n' > "$SEED/manifest.json"
git -C "$SEED" add manifest.json
git -C "$SEED" commit -qm "upstream update"
git -C "$SEED" -c core.hooksPath=/dev/null push -q
git clone -q "$REMOTE" "$PLUGINS/current"
git clone -q "$REMOTE" "$PLUGINS/dirty"
git clone -q "$REMOTE" "$PLUGINS/ahead"
git clone -q "$REMOTE" "$PLUGINS/fetch-error"
git clone -q "$REMOTE" "$PLUGINS/detached"
printf '\n' >> "$PLUGINS/dirty/manifest.json"
git -C "$PLUGINS/detached" checkout -q --detach
git -C "$PLUGINS/ahead" config user.name "Omaplug Tests"
git -C "$PLUGINS/ahead" config user.email "omaplug-tests@example.invalid"
printf '{"id":"fixture","version":3}\n' > "$PLUGINS/ahead/manifest.json"
git -C "$PLUGINS/ahead" add manifest.json
git -C "$PLUGINS/ahead" commit -qm "local commit"
git -C "$PLUGINS/diverged" config user.name "Omaplug Tests"
git -C "$PLUGINS/diverged" config user.email "omaplug-tests@example.invalid"
printf '{"id":"fixture","local":true}\n' > "$PLUGINS/diverged/manifest.json"
git -C "$PLUGINS/diverged" add manifest.json
git -C "$PLUGINS/diverged" commit -qm "divergent local commit"
git -C "$PLUGINS/fetch-error" remote set-url origin "$TMP/missing.git"
mkdir -p -- "$PLUGINS/plain" "$PLUGINS/no-origin"
printf '{"id":"plain"}\n' > "$PLUGINS/plain/manifest.json"
printf '{"id":"no-origin"}\n' > "$PLUGINS/no-origin/manifest.json"
git init -q "$PLUGINS/no-origin"
git -C "$PLUGINS/no-origin" config user.name "Omaplug Tests"
git -C "$PLUGINS/no-origin" config user.email "omaplug-tests@example.invalid"
git -C "$PLUGINS/no-origin" add manifest.json
git -C "$PLUGINS/no-origin" commit -qm "local plugin"
ln -s -- "$PLUGINS/current" "$PLUGINS/symlinked"
OUTPUT=$($ROOT/plugin-state.sh "$PLUGINS")
state_for() {
local id="$1"
awk -F '\t' -v id="$id" '$1 != "CHECK" && $2 == id { print $1 }' <<< "$OUTPUT"
}
detail_for() {
local id="$1"
awk -F '\t' -v id="$id" '$1 != "CHECK" && $2 == id { print $4 }' <<< "$OUTPUT"
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
if [[ $actual != "$expected" ]]; then
printf 'FAIL: %s: expected %q, got %q\n' "$label" "$expected" "$actual" >&2
printf '%s\n' "$OUTPUT" >&2
exit 1
fi
}
assert_eq CURRENT "$(state_for current)" current
assert_eq UPDATE "$(state_for behind)" behind
assert_eq LOCAL_CHANGES "$(state_for dirty)" dirty
assert_eq modified "$(detail_for dirty)" dirty-detail
assert_eq LOCAL_CHANGES "$(state_for detached)" detached
assert_eq detached "$(detail_for detached)" detached-detail
assert_eq LOCAL_CHANGES "$(state_for ahead)" ahead
assert_eq ahead "$(detail_for ahead)" ahead-detail
assert_eq LOCAL_CHANGES "$(state_for diverged)" diverged
assert_eq diverged "$(detail_for diverged)" diverged-detail
assert_eq LOCAL "$(state_for plain)" plain
assert_eq LOCAL "$(state_for no-origin)" no-origin
assert_eq LOCAL "$(state_for symlinked)" symlinked
assert_eq ERROR "$(state_for fetch-error)" fetch-error
current_url=$(awk -F '\t' '$1 == "CURRENT" && $2 == "current" { print $3 }' <<< "$OUTPUT")
assert_eq "$REMOTE" "$current_url" origin-url-without-trailing-space
printf 'plugin-state-test: ok\n'
@@ -0,0 +1,52 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
if ! command -v quickshell >/dev/null 2>&1; then
printf 'quickshell-detached-test: skipped (quickshell not installed)\n'
exit 0
fi
TMP=$(mktemp -d)
trap 'rm -rf -- "$TMP"' EXIT
BIN="$TMP/bin"
TEST_HOME="$TMP/home"
PLUGINS="$TEST_HOME/.config/omarchy/plugins"
STATUS="$TMP/update.status"
mkdir -p -- "$BIN" "$PLUGINS"
for id in slow good; do
dir="$PLUGINS/$id"
git init -q -b main "$dir"
git -C "$dir" config user.name "Omaplug Tests"
git -C "$dir" config user.email "omaplug-tests@example.invalid"
printf '{"id":"%s"}\n' "$id" > "$dir/manifest.json"
git -C "$dir" add manifest.json
git -C "$dir" commit -qm "fixture"
done
cat > "$BIN/omarchy" <<'EOF'
#!/bin/bash
[[ ${3:-} == slow ]] && sleep 0.4
printf 'updated %s\n' "${3:-}"
EOF
chmod +x "$BIN/omarchy"
PATH="$BIN:$PATH" \
HOME="$TEST_HOME" \
OMAPLUG_TEST_HELPER="$ROOT/update-helper.sh" \
OMAPLUG_TEST_STATUS="$STATUS" \
QT_QPA_PLATFORM=offscreen \
quickshell --no-color -p "$ROOT/tests/DetachedLaunch.qml" >/dev/null 2>&1
for _ in {1..200}; do
grep -Fqx $'done\t2\t0' "$STATUS" 2>/dev/null && break
sleep 0.01
done
grep -Fqx $'job\tjob-quickshell' "$STATUS"
grep -Fqx $'done\t2\t0' "$STATUS"
printf 'quickshell-detached-test: ok\n'
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
"$ROOT/plugin-state-test.sh"
"$ROOT/update-helper-test.sh"
"$ROOT/quickshell-detached-test.sh"
bash "$ROOT/verification-status-test.sh"
node "$ROOT/plugin-metadata-test.cjs"
"$ROOT/auto-check-test.sh"
"$ROOT/auto-check-coordinator-test.sh"
"$ROOT/nested-widget-toggle-test.sh"
python3 "$ROOT/security-regression-test.py"
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Isolated filesystem regressions; never touches installed plugins."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class StateSafety(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix="omaplug-security-")
self.addCleanup(self.tmp.cleanup)
self.path = Path(self.tmp.name)
self.env = dict(os.environ, XDG_RUNTIME_DIR=str(self.path))
(self.path / "omaplug").mkdir(mode=0o700)
(self.path / "plugins").mkdir()
self.victim = self.path / "victim"
self.victim.write_text("keep me")
def run_helper(self, *args):
return subprocess.run(args, env=self.env, capture_output=True, timeout=10)
def test_cache_symlink_never_truncates_target(self):
(self.path / "omaplug/auto-check.cache.tmp").symlink_to(self.victim)
self.run_helper("bash", str(ROOT / "auto-check-coordinator.sh"), str(self.path / "plugins"))
self.assertEqual(self.victim.read_text(), "keep me")
def test_update_status_symlink_is_rejected(self):
status = self.path / "update.status"
status.symlink_to(self.victim)
result = self.run_helper("bash", str(ROOT / "update-helper.sh"), str(status), "test", "invalid/id")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.victim.read_text(), "keep me")
if __name__ == "__main__":
unittest.main()
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
TMP=$(mktemp -d)
trap 'rm -rf -- "$TMP"' EXIT
BIN="$TMP/bin"
LOG="$TMP/calls.log"
STATUS="$TMP/update.status"
TEST_HOME="$TMP/home"
PLUGINS="$TEST_HOME/.config/omarchy/plugins"
mkdir -p -- "$BIN" "$PLUGINS"
make_plugin() {
local id="$1"
local dir="$PLUGINS/$id"
git init -q -b main "$dir"
git -C "$dir" config user.name "Omaplug Tests"
git -C "$dir" config user.email "omaplug-tests@example.invalid"
printf '{"id":"%s"}\n' "$id" > "$dir/manifest.json"
git -C "$dir" add manifest.json
git -C "$dir" commit -qm "fixture"
}
for id in good fail slow dirty detached; do make_plugin "$id"; done
printf '\n' >> "$PLUGINS/dirty/manifest.json"
git -C "$PLUGINS/detached" checkout -q --detach
ln -s -- "$PLUGINS/good" "$PLUGINS/linked"
cat > "$BIN/omarchy" <<'EOF'
#!/bin/bash
printf '%s\n' "$*" >> "$OMAPLUG_TEST_LOG"
case "${3:-}" in
slow) sleep 0.4 ;;
esac
if [[ ${3:-} == fail ]]; then
printf 'could not update %s\n' "$3" >&2
exit 7
fi
printf 'updated %s\n' "${3:-}"
EOF
chmod +x "$BIN/omarchy"
export PATH="$BIN:$PATH"
export OMAPLUG_TEST_LOG="$LOG"
export HOME="$TEST_HOME"
# A prior run may have left a status file created under a permissive umask.
# The helper must tighten it before writing any job details.
: > "$STATUS"
chmod 0644 "$STATUS"
assert_line() {
local line="$1"
local file="$2"
grep -Fqx -- "$line" "$file" || {
printf 'FAIL: missing line %q in %s\n' "$line" "$file" >&2
cat "$file" >&2
exit 1
}
}
if "$ROOT/update-helper.sh" "$STATUS" job-direct good fail dirty detached linked 'bad/id'; then
printf 'FAIL: a partially failed update job returned success\n' >&2
exit 1
fi
assert_line $'job\tjob-direct' "$STATUS"
assert_line $'start\tgood' "$STATUS"
assert_line $'ok\tgood\tupdated good' "$STATUS"
assert_line $'start\tfail' "$STATUS"
assert_line $'failed\tfail\tcould not update fail' "$STATUS"
assert_line $'failed\tdirty\tplugin gained local changes after the update check' "$STATUS"
assert_line $'failed\tdetached\tplugin is on a detached HEAD' "$STATUS"
assert_line $'failed\tlinked\tplugin became a symlink after the update check' "$STATUS"
assert_line $'failed\tbad/id\tinvalid plugin id' "$STATUS"
assert_line $'done\t1\t5' "$STATUS"
[[ $(stat -c '%a' "$STATUS") == 600 ]] || {
printf 'FAIL: status file mode is %s instead of 600\n' "$(stat -c '%a' "$STATUS")" >&2
exit 1
}
[[ $(tail -n 1 "$STATUS") == $'done\t1\t5' ]] || {
printf 'FAIL: done is not the final status record\n' >&2
cat "$STATUS" >&2
exit 1
}
assert_line 'plugin update good --yes' "$LOG"
assert_line 'plugin update fail --yes' "$LOG"
[[ $(wc -l < "$LOG") -eq 2 ]] || {
printf 'FAIL: invalid plugin id reached omarchy\n' >&2
cat "$LOG" >&2
exit 1
}
# A live runner owns the stable status path. A second runner must fail without
# replacing or appending to that job's status stream.
: > "$LOG"
"$ROOT/update-helper.sh" "$STATUS" job-first slow &
first_pid=$!
for _ in {1..100}; do
[[ -f $STATUS.flock ]] && break
sleep 0.01
done
sleep 0.05
if "$ROOT/update-helper.sh" "$STATUS" job-second good; then
printf 'FAIL: a concurrent update runner acquired the same lock\n' >&2
exit 1
else
second_rc=$?
fi
[[ $second_rc -eq 3 ]] || {
printf 'FAIL: concurrent runner returned %s instead of 3\n' "$second_rc" >&2
exit 1
}
wait "$first_pid"
assert_line $'job\tjob-first' "$STATUS"
if grep -Fq $'job\tjob-second' "$STATUS"; then
printf 'FAIL: concurrent runner clobbered the active status\n' >&2
exit 1
fi
printf 'update-helper-test: ok\n'
@@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
PAGE="$ROOT/panel/updates/Page.qml"
PANEL="$ROOT/Panel.qml"
assert_contains() {
local needle="$1"
local file="$2"
grep -Fq -- "$needle" "$file" || {
printf 'FAIL: missing %q in %s\n' "$needle" "$file" >&2
exit 1
}
}
assert_contains 'required property var marketplaceMap' "$PAGE"
assert_contains 'required property bool marketplaceFetchFailed' "$PAGE"
assert_contains 'function verificationText(id, sourceKey)' "$PAGE"
assert_contains 'page.verificationText(updateRow.modelData.id, updateRow.modelData.sourceKey)' "$PAGE"
assert_contains 'marketplaceMap: root.marketplaceMap' "$PANEL"
assert_contains 'marketplaceFetching: root.marketplaceFetching' "$PANEL"
assert_contains 'marketplaceFetchFailed: root.marketplaceFetchFailed' "$PANEL"
assert_contains 'marketplaceProcess.command = [root.marketplaceHelperPath]' "$PANEL"
assert_contains '--max-filesize 16777216' "$ROOT/marketplace-catalog.sh"
printf 'verification-status-test: ok\n'