Plugin marketplace: omarchyplugins.com catalog, Plugin Manager/Library windows, registry.json with bundled plugins

swadhinbiswas committed 2 months ago
12362d0
1 parent 5255918
commit 12362d0
11 files changed
+5067 -225
▼bin/omarchy-plugin-add
+1-1
139
139
mv "$stage" "$target"
140
140
echo "Added $id into $target"
141
141
142
-omarchy-shell shell rescanPlugins >/dev/null
142
+omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
143
143
144
144
if [[ -z $enable_after ]]; then
145
145
if (( ASSUME_YES )) || ! interactive; then
146
146
▼bin/omarchy-plugin-library
+7-0
1
+#!/bin/bash
2
+
3
+# omarchy:summary=Open the Plugin Library and Marketplace window
4
+# omarchy:group=plugin
5
+# omarchy:alias=omarchy plugin library
6
+
7
+exec "${OMARCHY_PATH:-/usr/share/de-omarchy}/bin/omarchy-plugin-manager" "$@"
0
8
▼bin/omarchy-plugin-manager
+79-0
1
+#!/bin/bash
2
+
3
+# omarchy:summary=Open or toggle the graphical Plugin Manager window
4
+# omarchy:group=plugin
5
+# omarchy:alias=omarchy plugin manager
6
+
7
+LOCKDIR="/tmp/omarchy-plugin-manager.lock"
8
+if ! mkdir "$LOCKDIR" 2>/dev/null; then
9
+ lock_age=$(( $(date +%s) - $(stat -c %Y "$LOCKDIR" 2>/dev/null || echo 0) ))
10
+ if (( lock_age > 3 )); then
11
+ rm -rf "$LOCKDIR"
12
+ mkdir "$LOCKDIR" 2>/dev/null || exit 0
13
+ else
14
+ exit 0
15
+ fi
16
+fi
17
+trap 'rm -rf "$LOCKDIR"' EXIT
18
+
19
+# The installed runtime is authoritative — never reference the dev checkout
20
+# here; an installed desktop must not depend on (or prefer) a home git clone.
21
+PATH_CANDIDATES=(
22
+ "${OMARCHY_PATH:-/usr/share/de-omarchy}/plugin-library"
23
+ "/usr/share/de-omarchy/plugin-library"
24
+)
25
+
26
+APP_PATH=""
27
+for p in "${PATH_CANDIDATES[@]}"; do
28
+ if [[ -f "$p/shell.qml" ]]; then
29
+ APP_PATH="$p"
30
+ break
31
+ fi
32
+done
33
+
34
+if [[ -z $APP_PATH ]]; then
35
+ echo "Plugin manager QML path not found!" >&2
36
+ exit 1
37
+fi
38
+
39
+# Check if the Plugin Manager window already exists in Hyprland
40
+existing_window=$(hyprctl clients -j 2>/dev/null | jq -r '.[] | select((.title | test("^(Omarchy Plugin Manager|Plugin Library)$")) or (.class == "org.quickshell" and (.title | test("Plugin")))) | .address' | head -n 1)
41
+active_window=$(hyprctl activewindow -j 2>/dev/null | jq -r '.address // empty')
42
+current_ws=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // "1"')
43
+
44
+if [[ -n "$existing_window" ]]; then
45
+ if [[ "$existing_window" == "$active_window" ]]; then
46
+ # Toggle off if currently focused
47
+ hyprctl dispatch "hl.dsp.window.close({ window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
48
+ pkill -f "quickshell.*plugin-library" 2>/dev/null || true
49
+ else
50
+ # Move to current workspace if needed, focus and raise
51
+ hyprctl dispatch "hl.dsp.window.move({ workspace = \"$current_ws\", window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
52
+ hyprctl dispatch "hl.dsp.focus({ window = \"address:$existing_window\" })" >/dev/null 2>&1 || true
53
+ fi
54
+ exit 0
55
+fi
56
+
57
+# Clean up any stale quickshell plugin-library instances
58
+pkill -f "quickshell.*plugin-library" 2>/dev/null || true
59
+sleep 0.05
60
+
61
+# Launch detached
62
+if command -v uwsm-app >/dev/null 2>&1; then
63
+ uwsm-app -- quickshell -p "$APP_PATH" >/dev/null 2>&1 &
64
+else
65
+ setsid quickshell -p "$APP_PATH" >/dev/null 2>&1 &
66
+fi
67
+
68
+# Ensure window appears on current workspace and gets focused
69
+(
70
+ for (( i = 0; i < 30; i++ )); do
71
+ win_addr=$(hyprctl clients -j 2>/dev/null | jq -r '.[] | select((.title | test("^(Omarchy Plugin Manager|Plugin Library)$")) or (.class == "org.quickshell" and (.title | test("Plugin")))) | .address' | head -n 1)
72
+ if [[ -n "$win_addr" ]]; then
73
+ hyprctl dispatch "hl.dsp.window.move({ workspace = \"$current_ws\", window = \"address:$win_addr\" })" >/dev/null 2>&1 || true
74
+ hyprctl dispatch "hl.dsp.focus({ window = \"address:$win_addr\" })" >/dev/null 2>&1 || true
75
+ break
76
+ fi
77
+ sleep 0.05
78
+ done
79
+) &
0
80
▼bin/omarchy-plugin-remove
+5-5
72
72
[[ -e $target || -L $target ]] || fail "plugin '$id' is not installed"
73
73
74
74
was_enabled=""
75
-shell_plugins=$(omarchy-shell shell listPlugins)
75
+shell_plugins=$(omarchy-shell shell listPlugins 2>/dev/null || true)
76
76
if [[ -n $shell_plugins ]]; then
77
77
was_enabled=$(jq -r --arg id "$id" '
78
78
.[] | select(.id == $id) | .enabled
79
- ' <<<"$shell_plugins")
79
+ ' <<<"$shell_plugins" 2>/dev/null || true)
80
80
fi
81
81
82
82
cloned_from=""
83
83
if [[ -f $target/manifest.json ]]; then
84
- cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json")
84
+ cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json" 2>/dev/null || true)
85
85
fi
86
86
87
87
if [[ -L $target ]]; then
93
93
fi
94
94
95
95
if [[ $was_enabled == "true" ]]; then
96
- omarchy-shell shell setPluginEnabled "$id" false >/dev/null
96
+ omarchy-shell shell setPluginEnabled "$id" false >/dev/null 2>&1 || true
97
97
fi
98
98
99
99
if [[ -L $target ]]; then
114
114
echo "Removed $id. Backup at: $backup"
115
115
fi
116
116
117
-omarchy-shell shell rescanPlugins >/dev/null
117
+omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
118
118
119
119
if [[ -n $cloned_from && $was_enabled == "true" ]]; then
120
120
echo "Restored $cloned_from."
121
121
▼bin/omarchy-plugins
+482-77
1
1
#!/bin/bash
2
-# omarchy-plugins — browse and install plugins from the de-omarchy registry
3
-# Usage: omarchy-plugins [list|install|search|info] [plugin-name]
4
-set -euo pipefail
5
-
6
-REGISTRY="${OMARCHY_PATH:-/usr/share/de-omarchy}/registry.json"
7
-PLUGIN_DIR="$HOME/.config/omarchy/plugins"
8
-
9
-ACTION="${1:-list}"
10
-QUERY="${2:-}"
11
-
12
-log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
13
-err() { printf '\033[1;31mERR:\033[0m %s\n' "$*" >&2; exit 1; }
14
-
15
-[[ -f "$REGISTRY" ]] || err "Registry not found at $REGISTRY"
16
-
17
-list_plugins() {
18
- python3 -c "
19
-import json, sys
20
-with open('$REGISTRY') as f: r = json.load(f)
21
-for p in r.get('plugins', []):
22
- installed = '$PLUGIN_DIR/' + p['id'].split('.')[-1]
23
- import os
24
- status = 'installed' if os.path.isdir(installed) else 'available'
25
- print(f\" {p['id']:40s} {p['category']:12s} {p['status']:8s} [{status}]\")
26
-"
2
+
3
+# omarchy:summary=Manage installed plugins and browse the Omarchy plugin marketplace
4
+# omarchy:group=plugin
5
+# omarchy:args=[list|marketplace|install|remove|enable|disable|update|gui] [args...]
6
+# omarchy:examples=omarchy plugins list | omarchy plugins marketplace | omarchy plugins install game-of-life | omarchy plugins enable omarchy.dropbox
7
+
8
+set -eo pipefail
9
+
10
+OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/de-omarchy}"
11
+PLUGINS_DIR="$HOME/.config/omarchy/plugins"
12
+CACHE_DIR="$HOME/.cache/omarchy"
13
+MARKETPLACE_CACHE="$CACHE_DIR/marketplace_catalog.json"
14
+MARKETPLACE_URL="https://omarchyplugins.com/catalog.json"
15
+REGISTRY_FILE="$OMARCHY_PATH/registry.json"
16
+
17
+mkdir -p "$PLUGINS_DIR" "$CACHE_DIR"
18
+
19
+log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
20
+warn() { printf '\033[1;33m ->\033[0m %s\n' "$*"; }
21
+err() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
22
+
23
+ensure_marketplace_catalog() {
24
+ local force="${1:-false}"
25
+ if [[ $force == "true" || ! -f "$MARKETPLACE_CACHE" || $(find "$MARKETPLACE_CACHE" -mmin +1440 2>/dev/null) ]]; then
26
+ log "Fetching latest marketplace catalog from $MARKETPLACE_URL..."
27
+ if curl -sL --connect-timeout 5 --max-time 15 "$MARKETPLACE_URL" -o "$MARKETPLACE_CACHE.tmp" 2>/dev/null; then
28
+ if jq -e '.plugins | length > 0' "$MARKETPLACE_CACHE.tmp" >/dev/null 2>&1; then
29
+ mv "$MARKETPLACE_CACHE.tmp" "$MARKETPLACE_CACHE"
30
+ else
31
+ rm -f "$MARKETPLACE_CACHE.tmp"
32
+ fi
33
+ else
34
+ rm -f "$MARKETPLACE_CACHE.tmp"
35
+ fi
36
+ fi
37
+
38
+ if [[ ! -f "$MARKETPLACE_CACHE" ]]; then
39
+ if [[ -f "$REGISTRY_FILE" ]]; then
40
+ cp "$REGISTRY_FILE" "$MARKETPLACE_CACHE"
41
+ else
42
+ echo '{"plugins":[]}' > "$MARKETPLACE_CACHE"
43
+ fi
44
+ fi
27
45
}
28
46
29
-search_plugins() {
30
- python3 -c "
31
-import json, sys
32
-with open('$REGISTRY') as f: r = json.load(f)
33
-q = '$QUERY'.lower()
34
-for p in r.get('plugins', []):
35
- searchable = (p['name'] + ' ' + p['description'] + ' ' + ' '.join(p.get('tags', []))).lower()
36
- if q in searchable:
37
- print(f\" {p['id']:40s} {p['name']}\")
38
- print(f\" {p['description']}\")
39
- print()
40
-"
47
+cmd_list() {
48
+ local json="false"
49
+ while (( $# > 0 )); do
50
+ case "$1" in
51
+ --json) json="true" ;;
52
+ *) ;;
53
+ esac
54
+ shift
55
+ done
56
+
57
+ local shell_json
58
+ shell_json=$(omarchy-shell shell listPlugins 2>/dev/null || true)
59
+
60
+ if [[ -z $shell_json || $shell_json == "null" || $shell_json == *"error"* ]]; then
61
+ # Fallback to scanning catalog directly
62
+ shell_json=$(omarchy-plugin-catalog 2>/dev/null | jq '[.[] | {id: .id, name: .name, kinds: .kinds, enabled: false, firstParty: .firstParty}]' 2>/dev/null || echo "[]")
63
+ fi
64
+
65
+ if [[ $json == "true" ]]; then
66
+ printf '%s\n' "$shell_json"
67
+ return 0
68
+ fi
69
+
70
+ jq -r '
71
+ .[] |
72
+ [
73
+ .id,
74
+ (if .enabled then "active" else "inactive" end),
75
+ (if .firstParty then "built-in" else "user" end),
76
+ ((.kinds // []) | join(",")),
77
+ (.name // .id)
78
+ ] | @tsv
79
+ ' <<<"$shell_json" | awk -F '\t' '
80
+ BEGIN { printf "\033[1m%-34s %-10s %-10s %-18s %s\033[0m\n", "ID", "STATE", "SOURCE", "KINDS", "NAME" }
81
+ {
82
+ state_color = ($2 == "active" ? "\033[32m" : "\033[90m")
83
+ src_color = ($3 == "built-in" ? "\033[36m" : "\033[33m")
84
+ printf "%-34s %s%-10s\033[0m %s%-10s\033[0m %-18s %s\n", $1, state_color, $2, src_color, $3, $4, $5
85
+ }
86
+ '
41
87
}
42
88
43
-show_info() {
44
- python3 -c "
45
-import json
46
-with open('$REGISTRY') as f: r = json.load(f)
47
-for p in r.get('plugins', []):
48
- if p['id'] == '$QUERY' or p['name'].lower() == '$QUERY'.lower():
49
- print(f\"Name: {p['name']}\")
50
- print(f\"ID: {p['id']}\")
51
- print(f\"Description: {p['description']}\")
52
- print(f\"Author: {p['author']}\")
53
- print(f\"Category: {p['category']}\")
54
- print(f\"Tags: {', '.join(p.get('tags', []))}\")
55
- print(f\"Status: {p['status']}\")
56
- print(f\"Kind: {p['kind']}\")
57
- print(f\"Install: {p['installCommand']}\")
58
- exit(0)
59
-print('Plugin not found: $QUERY')
60
-"
89
+cmd_marketplace() {
90
+ local query=""
91
+ local category=""
92
+ local json="false"
93
+ local force="false"
94
+
95
+ while (( $# > 0 )); do
96
+ case "$1" in
97
+ --fetch|-f) force="true" ;;
98
+ --json) json="true" ;;
99
+ --query|-q) query="${2:-}"; shift ;;
100
+ --category|-c) category="${2:-}"; shift ;;
101
+ *) [[ -z $query ]] && query="$1" ;;
102
+ esac
103
+ shift
104
+ done
105
+
106
+ ensure_marketplace_catalog "$force"
107
+
108
+ local filtered
109
+ filtered=$(jq \
110
+ --arg q "$query" \
111
+ --arg cat "$category" '
112
+ .plugins
113
+ | map(select(
114
+ ($cat == "" or (.category // "" | ascii_downcase) == ($cat | ascii_downcase)) and
115
+ ($q == "" or
116
+ (.name // "" | ascii_downcase | contains($q | ascii_downcase)) or
117
+ (.id // "" | ascii_downcase | contains($q | ascii_downcase)) or
118
+ (.description // "" | ascii_downcase | contains($q | ascii_downcase)) or
119
+ ((.tags // []) | join(" ") | ascii_downcase | contains($q | ascii_downcase)) or
120
+ (.author // "" | ascii_downcase | contains($q | ascii_downcase))
121
+ )
122
+ ))
123
+ ' "$MARKETPLACE_CACHE")
124
+
125
+ if [[ $json == "true" ]]; then
126
+ printf '%s\n' "$filtered"
127
+ return 0
128
+ fi
129
+
130
+ local count
131
+ count=$(jq 'length' <<<"$filtered")
132
+ log "Marketplace catalog: $count matching plugins"
133
+ echo ""
134
+
135
+ jq -r '
136
+ .[] |
137
+ [
138
+ (.name // .id | gsub("\t|\n"; " ")),
139
+ (.category // "Misc" | gsub("\t|\n"; " ")),
140
+ (.kind // "Plugin" | gsub("\t|\n"; " ")),
141
+ (if ((.stars // 0) > 0) then ("★ " + ((.stars // 0)|tostring)) else "-" end),
142
+ .id,
143
+ (.description // "-" | gsub("\t|\n"; " "))
144
+ ] | @tsv
145
+ ' <<<"$filtered" | while IFS=$'\t' read -r name cat kind stars id desc; do
146
+ printf ' \033[1;35m%-32s\033[0m \033[36m[%s]\033[0m \033[33m%s\033[0m \033[32m%s\033[0m\n' "$name" "$cat" "$kind" "$stars"
147
+ printf ' \033[90mID:\033[0m %-30s \033[90m%s\033[0m\n' "$id" "$desc"
148
+ echo ""
149
+ done
61
150
}
62
151
63
-install_plugin() {
64
- local cmd
65
- cmd=$(python3 -c "
66
-import json
67
-with open('$REGISTRY') as f: r = json.load(f)
68
-for p in r.get('plugins', []):
69
- if p['id'] == '$QUERY' or p['name'].lower() == '$QUERY'.lower():
70
- print(p['installCommand'])
71
- exit(0)
72
-print('NOT_FOUND')
73
-")
74
- [[ "$cmd" == "NOT_FOUND" ]] && err "Plugin not found: $QUERY"
75
- log "Installing: $QUERY"
76
- mkdir -p "$PLUGIN_DIR"
77
- eval "$cmd"
78
- log "Done. Restart the shell to load: pkill -x quickshell"
152
+cmd_install() {
153
+ local target="${1:-}"
154
+ local enable_after=false
155
+ shift || true
156
+
157
+ while (( $# > 0 )); do
158
+ case "$1" in
159
+ --enable|-e) enable_after=true ;;
160
+ *) ;;
161
+ esac
162
+ shift
163
+ done
164
+
165
+ [[ -n $target ]] || err "Usage: omarchy plugins install <plugin-id-or-git-url> [--enable]"
166
+
167
+ # 1. Direct Git URL
168
+ if [[ $target =~ ^https?:// || $target =~ ^git@ ]]; then
169
+ log "Installing plugin from Git URL: $target"
170
+ if [[ $enable_after == "true" ]]; then
171
+ "$OMARCHY_PATH/bin/omarchy-plugin-add" "$target" --yes --enable
172
+ else
173
+ "$OMARCHY_PATH/bin/omarchy-plugin-add" "$target" --yes
174
+ fi
175
+ return 0
176
+ fi
177
+
178
+ # 2. Marketplace / Catalog ID
179
+ ensure_marketplace_catalog false
180
+ local plugin_info
181
+ plugin_info=$(jq -c --arg id "$target" '[.plugins[] | select(.id == $id or (.name | ascii_downcase) == ($id | ascii_downcase))][0] // empty' "$MARKETPLACE_CACHE")
182
+
183
+ if [[ -z $plugin_info || $plugin_info == "null" ]]; then
184
+ # Try fetching fresh
185
+ ensure_marketplace_catalog true
186
+ plugin_info=$(jq -c --arg id "$target" '[.plugins[] | select(.id == $id or (.name | ascii_downcase) == ($id | ascii_downcase))][0] // empty' "$MARKETPLACE_CACHE")
187
+ fi
188
+
189
+ [[ -n $plugin_info && $plugin_info != "null" ]] || err "Plugin '$target' not found in marketplace catalog."
190
+
191
+ local p_id p_name p_repo p_cmd p_manifest p_kind
192
+ p_id=$(jq -r '.id' <<<"$plugin_info")
193
+ p_name=$(jq -r '.name // .id' <<<"$plugin_info")
194
+ p_repo=$(jq -r '.repo // empty' <<<"$plugin_info")
195
+ p_cmd=$(jq -r '.installCommand // empty' <<<"$plugin_info")
196
+ p_manifest=$(jq -r '.manifestPath // empty' <<<"$plugin_info")
197
+ p_kind=$(jq -r '.kind // "plugin"' <<<"$plugin_info")
198
+
199
+ log "Installing '$p_name' ($p_id)..."
200
+
201
+ # If an explicit installCommand is provided, use it
202
+ if [[ -n $p_cmd && $p_cmd != "null" && $p_cmd != "" ]]; then
203
+ if [[ $p_cmd != *"--yes"* ]]; then
204
+ p_cmd="$p_cmd --yes"
205
+ fi
206
+ log "Executing install command: $p_cmd"
207
+ mkdir -p "$PLUGINS_DIR"
208
+ eval "$p_cmd"
209
+ # If monorepo manifestPath is specified (e.g. omni/manifest.json)
210
+ elif [[ -n $p_manifest && -n $p_repo ]]; then
211
+ local subfolder="${p_manifest%/*}"
212
+ local tmp_stage="$PLUGINS_DIR/.stage.$p_id.$$"
213
+ rm -rf "$tmp_stage"
214
+ log "Cloning repository $p_repo..."
215
+ git clone --depth 1 "$p_repo" "$tmp_stage"
216
+
217
+ local source_dir="$tmp_stage"
218
+ [[ -n $subfolder && -d "$tmp_stage/$subfolder" ]] && source_dir="$tmp_stage/$subfolder"
219
+
220
+ if [[ ! -f "$source_dir/manifest.json" ]]; then
221
+ rm -rf "$tmp_stage"
222
+ err "Could not find manifest.json inside $source_dir"
223
+ fi
224
+
225
+ local target_dir="$PLUGINS_DIR/$p_id"
226
+ rm -rf "$target_dir"
227
+ mkdir -p "$target_dir"
228
+ cp -r "$source_dir"/* "$target_dir"/
229
+ rm -rf "$tmp_stage"
230
+ # Otherwise standard repo
231
+ elif [[ -n $p_repo ]]; then
232
+ "$OMARCHY_PATH/bin/omarchy-plugin-add" "$p_repo" --yes
233
+ else
234
+ err "No install command or repository available for '$p_id'."
235
+ fi
236
+
237
+ # Rescan shell plugins
238
+ omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
239
+
240
+ if [[ $enable_after == "true" ]]; then
241
+ log "Enabling $p_id..."
242
+ "$OMARCHY_PATH/bin/omarchy-plugin-enable" "$p_id" 2>/dev/null || true
243
+ fi
244
+
245
+ log "Successfully installed $p_name ($p_id)!"
246
+}
247
+
248
+cmd_remove() {
249
+ local id="${1:-}"
250
+ [[ -n $id ]] || err "Usage: omarchy plugins remove <plugin-id>"
251
+ "$OMARCHY_PATH/bin/omarchy-plugin-remove" "$id" --yes
252
+}
253
+
254
+cmd_enable() {
255
+ local id="${1:-}"
256
+ [[ -n $id ]] || err "Usage: omarchy plugins enable <plugin-id> [placement]"
257
+ shift
258
+ "$OMARCHY_PATH/bin/omarchy-plugin-enable" "$id" "$@"
259
+}
260
+
261
+cmd_disable() {
262
+ local id="${1:-}"
263
+ [[ -n $id ]] || err "Usage: omarchy plugins disable <plugin-id>"
264
+ "$OMARCHY_PATH/bin/omarchy-plugin-disable" "$id"
265
+}
266
+
267
+cmd_update() {
268
+ "$OMARCHY_PATH/bin/omarchy-plugin-update" "$@"
269
+}
270
+
271
+cmd_check_updates() {
272
+ local count=0
273
+ log "Checking git-managed plugins for remote updates..."
274
+ for dir in "$PLUGINS_DIR"/*/; do
275
+ [[ -d "$dir/.git" ]] || continue
276
+ local p_id
277
+ p_id=$(basename "$dir")
278
+ if git -C "$dir" fetch --quiet origin HEAD 2>/dev/null; then
279
+ local local_rev remote_rev
280
+ local_rev=$(git -C "$dir" rev-parse HEAD 2>/dev/null || echo "1")
281
+ remote_rev=$(git -C "$dir" rev-parse FETCH_HEAD 2>/dev/null || echo "2")
282
+ if [[ $local_rev != "$remote_rev" ]]; then
283
+ printf ' \033[1;33mUpdate available:\033[0m %s\n' "$p_id"
284
+ (( count++ )) || true
285
+ fi
286
+ fi
287
+ done
288
+ if (( count == 0 )); then
289
+ log "All installed plugins are up to date."
290
+ else
291
+ log "$count plugin(s) can be updated with: omarchy plugins update"
292
+ fi
293
+}
294
+
295
+cmd_shortcut() {
296
+ local sub="${1:-list}"
297
+ shift || true
298
+ local keymap_file="$HOME/.config/hypr/keymap.lua"
299
+
300
+ case "$sub" in
301
+ list)
302
+ local json="false"
303
+ [[ "${1:-}" == "--json" ]] && json="true"
304
+ local list="[]"
305
+ if [[ -f "$keymap_file" ]]; then
306
+ local raw
307
+ raw=$(grep -E 'omarchy-shell shell (summon|toggle)' "$keymap_file" 2>/dev/null || true)
308
+ if [[ -n "$raw" ]]; then
309
+ list=$(echo "$raw" | while read -r line; do
310
+ [[ -z "$line" ]] && continue
311
+ keys=$(echo "$line" | sed -n 's/.*keys *= *"*\([^",]*\)"*.*/\1/p')
312
+ cmd=$(echo "$line" | sed -n 's/.*dsp *= *exec("\([^"]*\)").*/\1/p')
313
+ desc=$(echo "$line" | sed -n 's/.*desc *= *"*\([^",]*\)"*.*/\1/p')
314
+ id=$(echo "$cmd" | awk '{print $NF}')
315
+ jq -cn --arg id "$id" --arg keys "$keys" --arg desc "$desc" --arg cmd "$cmd" '{id: $id, keys: $keys, desc: $desc, cmd: $cmd}'
316
+ done | jq -s '.')
317
+ fi
318
+ fi
319
+
320
+ if [[ $json == "true" ]]; then
321
+ echo "$list"
322
+ else
323
+ echo "PLUGIN ID KEYBIND DESCRIPTION"
324
+ echo "------------------------------------------------------------------"
325
+ jq -r '.[] | [ .id, .keys, .desc ] | @tsv' <<<"$list" | while IFS=$'\t' read -r id keys desc; do
326
+ printf '%-36s %-16s %s\n' "$id" "$keys" "$desc"
327
+ done
328
+ fi
329
+ ;;
330
+ get)
331
+ local p_id="${1:-}"
332
+ [[ -n $p_id ]] || err "Plugin ID required"
333
+ local found=""
334
+ if [[ -f "$keymap_file" ]]; then
335
+ found=$(grep -F "$p_id" "$keymap_file" 2>/dev/null | grep -v 'omarchy-plugin-manager' 2>/dev/null | sed -n 's/.*keys *= *"*\([^",]*\)"*.*/\1/p' | head -n 1 || true)
336
+ fi
337
+ echo "$found"
338
+ ;;
339
+ set)
340
+ local p_id="${1:-}"
341
+ local keys="${2:-}"
342
+ [[ -n $p_id ]] || err "Plugin ID required"
343
+ [[ -n $keys ]] || err "Key sequence required (e.g. 'SUPER + ALT + D')"
344
+
345
+ if [[ -f "$keymap_file" ]]; then
346
+ grep -v -F "$p_id" "$keymap_file" > "$keymap_file.tmp" || true
347
+ mv "$keymap_file.tmp" "$keymap_file"
348
+ fi
349
+
350
+ keys=$(echo "$keys" | tr '[:lower:]' '[:upper:]' | sed 's/+/ + /g' | sed 's/ */ /g' | sed 's/^ *//;s/ *$//')
351
+ local plugin_name="$p_id"
352
+ local entry="k[#k + 1] = { cat = \"Plugins\", keys = \"$keys\", dsp = exec(\"omarchy-shell shell toggle $p_id\"), desc = \"$plugin_name\" }"
353
+
354
+ if grep -q "return k" "$keymap_file"; then
355
+ sed -i "/return k/i $entry" "$keymap_file"
356
+ else
357
+ echo "$entry" >> "$keymap_file"
358
+ echo "return k" >> "$keymap_file"
359
+ fi
360
+
361
+ log "Set shortcut for '$p_id' to: $keys"
362
+ hyprctl reload >/dev/null 2>&1 || true
363
+ ;;
364
+ remove|clear|unset)
365
+ local p_id="${1:-}"
366
+ [[ -n $p_id ]] || err "Plugin ID required"
367
+ if [[ -f "$keymap_file" ]]; then
368
+ grep -v -F "$p_id" "$keymap_file" > "$keymap_file.tmp" || true
369
+ mv "$keymap_file.tmp" "$keymap_file"
370
+ hyprctl reload >/dev/null 2>&1 || true
371
+ fi
372
+ log "Cleared shortcut for '$p_id'"
373
+ ;;
374
+ esac
375
+}
376
+
377
+cmd_bar() {
378
+ local sub="${1:-list}"
379
+ shift || true
380
+ local shell_cfg="$HOME/.config/omarchy/shell.json"
381
+
382
+ case "$sub" in
383
+ list)
384
+ if [[ ! -f "$shell_cfg" ]]; then
385
+ echo '{"inBar":[]}'
386
+ return
387
+ fi
388
+ jq -c '{
389
+ inBar: (([.bar.layout.left[]?.id, .bar.layout.center[]?.id, .bar.layout.right[]?.id] | unique) // []),
390
+ layout: (.bar.layout // {left:[], center:[], right:[]})
391
+ }' "$shell_cfg" 2>/dev/null || echo '{"inBar":[]}'
392
+ ;;
393
+ add)
394
+ local p_id="${1:-}"
395
+ local section="${2:-right}"
396
+ [[ -n $p_id ]] || err "Plugin ID required"
397
+ [[ $section =~ ^(left|center|right)$ ]] || section="right"
398
+
399
+ if [[ -f "$shell_cfg" ]]; then
400
+ local updated
401
+ updated=$(jq --arg id "$p_id" --arg sec "$section" '
402
+ if (([.bar.layout.left[]?.id, .bar.layout.center[]?.id, .bar.layout.right[]?.id] | index($id)) == null) then
403
+ .bar.layout[$sec] += [{"id": $id}]
404
+ else
405
+ .
406
+ end
407
+ ' "$shell_cfg")
408
+ echo "$updated" > "$shell_cfg"
409
+ fi
410
+ omarchy-shell shell enablePlugin "$p_id" "{\"section\":\"$section\"}" >/dev/null 2>&1 || true
411
+ log "Added '$p_id' to Omarchy top bar ($section section)."
412
+ ;;
413
+ remove)
414
+ local p_id="${1:-}"
415
+ [[ -n $p_id ]] || err "Plugin ID required"
416
+ if [[ -f "$shell_cfg" ]]; then
417
+ local updated
418
+ updated=$(jq --arg id "$p_id" '
419
+ .bar.layout.left = [.bar.layout.left[]? | select(.id != $id)] |
420
+ .bar.layout.center = [.bar.layout.center[]? | select(.id != $id)] |
421
+ .bar.layout.right = [.bar.layout.right[]? | select(.id != $id)]
422
+ ' "$shell_cfg")
423
+ echo "$updated" > "$shell_cfg"
424
+ fi
425
+ omarchy-shell shell setPluginEnabled "$p_id" false >/dev/null 2>&1 || true
426
+ log "Removed '$p_id' from Omarchy top bar."
427
+ ;;
428
+ toggle)
429
+ local p_id="${1:-}"
430
+ local section="${2:-right}"
431
+ [[ -n $p_id ]] || err "Plugin ID required"
432
+ local in_bar="false"
433
+ if [[ -f "$shell_cfg" ]]; then
434
+ in_bar=$(jq -r --arg id "$p_id" '([.bar.layout.left[]?.id, .bar.layout.center[]?.id, .bar.layout.right[]?.id] | index($id)) != null' "$shell_cfg")
435
+ fi
436
+ if [[ $in_bar == "true" ]]; then
437
+ cmd_bar remove "$p_id"
438
+ else
439
+ cmd_bar add "$p_id" "$section"
440
+ fi
441
+ ;;
442
+ esac
79
443
}
80
444
81
-case "$ACTION" in
82
- list) list_plugins ;;
83
- search) [[ -z $QUERY ]] && err "Usage: omarchy-plugins search <query>"; search_plugins ;;
84
- info) [[ -z $QUERY ]] && err "Usage: omarchy-plugins info <plugin-id>"; show_info ;;
85
- install) [[ -z $QUERY ]] && err "Usage: omarchy-plugins install <plugin-id>"; install_plugin ;;
86
- *) echo "Usage: omarchy-plugins [list|install|search|info] [plugin-name]" ;;
445
+cmd_gui() {
446
+ quickshell -n -p "$OMARCHY_PATH/plugin-library" >/dev/null 2>&1 &
447
+}
448
+
449
+# Main router
450
+action="${1:-list}"
451
+shift || true
452
+
453
+case "$action" in
454
+ list|ls) cmd_list "$@" ;;
455
+ marketplace|market|store) cmd_marketplace "$@" ;;
456
+ install|add) cmd_install "$@" ;;
457
+ remove|rm|delete|uninstall) cmd_remove "$@" ;;
458
+ enable|on) cmd_enable "$@" ;;
459
+ disable|off) cmd_disable "$@" ;;
460
+ update|upgrade) cmd_update "$@" ;;
461
+ check-updates|check) cmd_check_updates "$@" ;;
462
+ shortcut|keybind) cmd_shortcut "$@" ;;
463
+ bar) cmd_bar "$@" ;;
464
+ gui|manager|app) cmd_gui "$@" ;;
465
+ --help|-h)
466
+ cat <<EOF
467
+Usage:
468
+ omarchy plugins [command] [options]
469
+
470
+Commands:
471
+ list [--json] List all installed and discovered plugins
472
+ marketplace [--fetch] [--query <q>] Search and browse online plugin marketplace
473
+ install <id-or-url> [--enable] Install a plugin from marketplace or Git URL
474
+ remove <id> Uninstall an installed user plugin
475
+ enable <id> [placement] Enable a plugin in the active shell
476
+ disable <id> Disable a plugin
477
+ shortcut [list|get|set|remove] Manage keyboard shortcuts for plugins
478
+ bar [list|add|remove|toggle] Manage Omarchy top bar placement for plugins
479
+ update [id] Update git-installed plugins to latest release
480
+ check-updates Check if updates are available for installed plugins
481
+ gui Open the graphical Plugin Manager window
482
+EOF
483
+ ;;
484
+ *)
485
+ # Default to install if it looks like a URL or plugin id with args
486
+ if [[ $action == http* || $action == git@* ]]; then
487
+ cmd_install "$action" "$@"
488
+ else
489
+ err "Unknown plugins action: $action. Run 'omarchy plugins --help' for usage."
490
+ fi
491
+ ;;
87
492
esac
88
493
▼plugin-library/shell.qml
+1896-0
1
+import QtQuick
2
+import QtQuick.Controls
3
+import QtQuick.Layouts
4
+import Quickshell
5
+import Quickshell.Io
6
+
7
+ShellRoot {
8
+ id: root
9
+
10
+ // ---------------------------------------------------------------- Visual Design System (Tokyo Night / Catppuccin inspired)
11
+ readonly property color bgBase: "#11121d"
12
+ readonly property color bgSurface: "#161726"
13
+ readonly property color bgCard: "#1d1f33"
14
+ readonly property color bgCardHover: "#252842"
15
+ readonly property color bgElevated: "#2a2e4a"
16
+ readonly property color borderSubtle: "#282b45"
17
+ readonly property color borderActive: "#3d426b"
18
+
19
+ readonly property color fgMain: "#cdd6f4"
20
+ readonly property color fgMuted: "#a6adc8"
21
+ readonly property color fgDim: "#585b70"
22
+
23
+ readonly property color accent: "#89b4fa"
24
+ readonly property color accentGlow: Qt.rgba(137/255, 180/255, 250/255, 0.15)
25
+
26
+ readonly property color greenSuccess: "#a6e3a1"
27
+ readonly property color greenGlow: Qt.rgba(166/255, 227/255, 161/255, 0.15)
28
+
29
+ readonly property color redDanger: "#f38ba8"
30
+ readonly property color redGlow: Qt.rgba(243/255, 139/255, 168/255, 0.15)
31
+
32
+ readonly property color amberWarn: "#fab387"
33
+ readonly property color amberGlow: Qt.rgba(250/255, 179/255, 135/255, 0.15)
34
+
35
+ readonly property color purpleTag: "#cba6f7"
36
+ readonly property color purpleGlow: Qt.rgba(203/255, 166/255, 247/255, 0.15)
37
+
38
+ // ---------------------------------------------------------------- Iconography
39
+ //
40
+ // Vector glyphs from JetBrainsMono Nerd Font — the family the bar, panels,
41
+ // and terminal already render with — so icons stay monochrome, crisp at
42
+ // every size, and tintable instead of colored emoji. Most Nerd Font
43
+ // codepoints live beyond the BMP, so they are stored as ints and resolved
44
+ // with String.fromCodePoint rather than \u escapes.
45
+ readonly property string nfFontFamily: "JetBrainsMono Nerd Font"
46
+ function nf(code) { return String.fromCodePoint(code) }
47
+
48
+ // Chrome icons used by this window's own controls.
49
+ readonly property var ic: ({
50
+ puzzle: 0xF0327, // md-puzzle plugin manager brand
51
+ package: 0xF03D3, // md-package installed tab
52
+ globe: 0xF00AC, // fa-globe marketplace tab / web search
53
+ plus: 0xF0415, // md-plus add / pin actions
54
+ update: 0xF06B0, // md-update update-all button
55
+ bolt: 0xF140B, // md-lightning_bolt restart shell
56
+ magnify: 0xF0349, // md-magnify search fields
57
+ close: 0xF0156, // md-close clear buttons
58
+ check: 0xF012C, // md-check success states
59
+ closeCircle: 0xF0159, // md-close_circle failure states
60
+ checkCircle: 0xF05E0, // md-check_circle success toasts
61
+ pin: 0xF0403, // md-pin bar placement
62
+ keyboard: 0xF097B, // md-keyboard_outline shortcut keycap
63
+ trash: 0xF1F8, // fa-trash remove user plugin
64
+ star: 0xF04CE, // md-star marketplace stars
65
+ externalLink: 0xF005C, // md-arrow_top_right repo link
66
+ arrowLeft: 0xF004D, // md-arrow_left section picker
67
+ arrowRight: 0xF0054 // md-arrow_right section picker
68
+ })
69
+
70
+ // ---------------------------------------------------------------- State
71
+ property string currentTab: "installed" // "installed" | "marketplace"
72
+ property var installedPlugins: []
73
+ property var marketplacePlugins: []
74
+ property var filteredInstalled: []
75
+ property var filteredMarketplace: []
76
+ property var shortcutsMap: ({})
77
+ property var barLayoutData: ({ inBar: [] })
78
+
79
+ property string installedSearch: ""
80
+ property string installedFilter: "all" // "all" | "active" | "inactive" | "bar" | "builtin" | "user"
81
+
82
+ property string marketSearch: ""
83
+ property string marketCategory: "All"
84
+ property string marketSort: "stars" // "stars" | "name" | "date"
85
+
86
+ property string installingId: ""
87
+ property string statusMessage: ""
88
+ property string statusType: "info" // "info" | "success" | "error"
89
+ property bool updatingAll: false
90
+
91
+ // Modals
92
+ property bool urlDialogOpen: false
93
+ property string urlInput: ""
94
+
95
+ property bool confirmUninstallOpen: false
96
+ property var pendingUninstallPlugin: null
97
+
98
+ property bool restartDialogOpen: false
99
+
100
+ property bool shortcutDialogOpen: false
101
+ property var shortcutTargetPlugin: null
102
+ property string shortcutInput: ""
103
+
104
+ property bool barSectionDialogOpen: false
105
+ property var barTargetPlugin: null
106
+
107
+ Component.onCompleted: {
108
+ loadInstalled()
109
+ loadMarketplace()
110
+ loadShortcuts()
111
+ loadBarLayout()
112
+ }
113
+
114
+ // ---------------------------------------------------------------- Data Loaders
115
+ function loadInstalled() {
116
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
117
+ installedProc.command = ["bash", "-c", "omarchy-shell shell listPlugins 2>/dev/null || " + om + "/bin/omarchy-plugins list --json 2>/dev/null || omarchy plugins list --json 2>/dev/null || echo '[]'"]
118
+ installedProc.running = true
119
+ }
120
+
121
+ function loadMarketplace() {
122
+ var home = Quickshell.env("HOME")
123
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
124
+ marketProc.command = ["bash", "-c", "cat " + home + "/.cache/omarchy/marketplace_catalog.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || " + om + "/bin/omarchy-plugins marketplace --json 2>/dev/null || cat " + om + "/registry.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || echo '[]'"]
125
+ marketProc.running = true
126
+ }
127
+
128
+ function loadShortcuts() {
129
+ shortcutsProc.command = ["bash", "-c", "omarchy plugins shortcut list --json 2>/dev/null || echo '[]'"]
130
+ shortcutsProc.running = true
131
+ }
132
+
133
+ function loadBarLayout() {
134
+ barProc.command = ["bash", "-c", "omarchy plugins bar list 2>/dev/null || echo '{\"inBar\":[]}'"]
135
+ barProc.running = true
136
+ }
137
+
138
+ Process {
139
+ id: installedProc
140
+ running: false
141
+ stdout: StdioCollector {
142
+ waitForEnd: true
143
+ onStreamFinished: {
144
+ try {
145
+ var arr = JSON.parse(String(text).trim())
146
+ if (Array.isArray(arr)) {
147
+ root.installedPlugins = arr
148
+ root.applyInstalledFilter()
149
+ }
150
+ } catch (e) {
151
+ console.warn("Failed to parse installed plugins: " + e)
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ Process {
158
+ id: marketProc
159
+ running: false
160
+ stdout: StdioCollector {
161
+ waitForEnd: true
162
+ onStreamFinished: {
163
+ try {
164
+ var data = JSON.parse(String(text).trim())
165
+ var arr = Array.isArray(data) ? data : (data && data.plugins ? data.plugins : [])
166
+ if (Array.isArray(arr)) {
167
+ root.marketplacePlugins = arr
168
+ root.applyMarketFilter()
169
+ }
170
+ } catch (e) {
171
+ console.warn("Failed to parse marketplace plugins: " + e)
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ Process {
178
+ id: shortcutsProc
179
+ running: false
180
+ stdout: StdioCollector {
181
+ waitForEnd: true
182
+ onStreamFinished: {
183
+ try {
184
+ var arr = JSON.parse(String(text).trim())
185
+ var map = {}
186
+ if (Array.isArray(arr)) {
187
+ for (var i = 0; i < arr.length; i++) {
188
+ if (arr[i] && arr[i].id) {
189
+ map[arr[i].id] = arr[i].keys || ""
190
+ }
191
+ }
192
+ }
193
+ root.shortcutsMap = map
194
+ } catch (e) {
195
+ console.warn("Failed to parse shortcuts: " + e)
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ Process {
202
+ id: barProc
203
+ running: false
204
+ stdout: StdioCollector {
205
+ waitForEnd: true
206
+ onStreamFinished: {
207
+ try {
208
+ var data = JSON.parse(String(text).trim())
209
+ if (data && data.inBar) {
210
+ root.barLayoutData = data
211
+ root.applyInstalledFilter()
212
+ }
213
+ } catch (e) {
214
+ console.warn("Failed to parse bar layout: " + e)
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // ---------------------------------------------------------------- Filter Logic
221
+ function isPluginInBar(id) {
222
+ if (!id || !root.barLayoutData || !root.barLayoutData.inBar) return false
223
+ return root.barLayoutData.inBar.indexOf(id) !== -1
224
+ }
225
+
226
+ function getPluginIcon(id, name) {
227
+ var map = {
228
+ "omarchy.menu": 0xF0463, // md-rocket
229
+ "omarchy.active-window": 0xF05B1, // md-window_open
230
+ "omarchy.workspaces": 0xF0570, // md-view_grid
231
+ "omarchy.bar": 0xF056E, // md-view_dashboard
232
+ "omarchy.agents": 0xF06A9, // md-robot
233
+ "omarchy.audio": 0xF057E, // md-volume_high
234
+ "omarchy.bluetooth": 0xF00AF, // md-bluetooth
235
+ "omarchy.network": 0xF05A9, // md-wifi
236
+ "omarchy.clock": 0xF0150, // md-clock_outline
237
+ "omarchy.battery": 0xF0079, // md-battery
238
+ "omarchy.power": 0xF0425, // md-power
239
+ "omarchy.sysmon": 0xF029A, // md-gauge
240
+ "omarchy.weather": 0xF0599, // md-weather_sunny
241
+ "omarchy.media": 0xF075A, // md-music
242
+ "omarchy.notifications": 0xF009A, // md-bell
243
+ "omarchy.clipboard": 0xF014D, // md-clipboard_text
244
+ "omarchy.emojis": 0xF01F2, // md-emoticon_outline
245
+ "omarchy.indicators": 0xF0335, // md-lightbulb
246
+ "omarchy.keyboard-layout": 0xF097B, // md-keyboard_outline
247
+ "omarchy.lock": 0xF033E, // md-lock
248
+ "omarchy.nightlight": 0xF0594, // md-weather_night
249
+ "omarchy.microphone": 0xF036C, // md-microphone
250
+ "omarchy.tray": 0xF1294, // md-tray
251
+ "omarchy.background": 0xF02E9, // md-image
252
+ "omarchy.idle": 0xF051F, // md-timer_sand
253
+ "omarchy.polkit": 0xF0BC5, // md-shield_key_outline
254
+ "omarchy.reminders": 0xF13AB, // md-timer
255
+ "omarchy.spacer": 0xF084E, // md-arrow_expand_horizontal
256
+ "omarchy.speedtest": 0xF04C5, // md-speedometer
257
+ "omarchy.system-update": 0xF06B0, // md-update
258
+ "omarchy.tailscale": 0xF0582, // md-vpn
259
+ "omarchy.wifiqr": 0xF0432, // md-qrcode
260
+ "deomarchy.docker-status": 0xF0868, // md-docker
261
+ "deomarchy.wallpaper-manager": 0xF0E09, // md-wallpaper
262
+ "deomarchy.power-profile": 0xF0241, // md-flash
263
+ "deomarchy.system-overview": 0xF154D, // md-chart_box
264
+ "deomarchy.monitor-manager": 0xF0379, // md-monitor
265
+ "deomarchy.plugin-manager": 0xF0327, // md-puzzle
266
+ "deomarchy.plugin-library": 0xF0327, // md-puzzle
267
+ "nosignal.motion-wallpaper": 0xF022F, // md-film
268
+ "omaplug": 0xF06A5 // md-power_plug
269
+ }
270
+ if (map[id] !== undefined) return String.fromCodePoint(map[id])
271
+ var clean = (name || id || "").toLowerCase()
272
+ if (clean.indexOf("docker") !== -1) return String.fromCodePoint(0xF0868) // md-docker
273
+ if (clean.indexOf("game") !== -1) return String.fromCodePoint(0xF0296) // md-gamepad
274
+ if (clean.indexOf("music") !== -1 || clean.indexOf("audio") !== -1) return String.fromCodePoint(0xF075A) // md-music
275
+ if (clean.indexOf("wall") !== -1) return String.fromCodePoint(0xF02E9) // md-image
276
+ if (clean.indexOf("monitor") !== -1 || clean.indexOf("cpu") !== -1) return String.fromCodePoint(0xF029A) // md-gauge
277
+ if (clean.indexOf("calc") !== -1) return String.fromCodePoint(0xF00EC) // md-calculator
278
+ if (clean.indexOf("ai") !== -1 || clean.indexOf("gpt") !== -1) return String.fromCodePoint(0xF06A9) // md-robot
279
+ return String.fromCodePoint(0xF0068) // md-auto_fix
280
+ }
281
+
282
+ function getPluginDescription(p) {
283
+ if (p.description && p.description.length > 0) return p.description
284
+ var map = {
285
+ "omarchy.active-window": "Displays the title and icon of the current focused window",
286
+ "omarchy.agents": "Quick switcher and status for background AI coding assistants",
287
+ "omarchy.audio": "Audio volume slider, output sink switcher, and per-app stream mixer",
288
+ "omarchy.background": "High-performance wallpaper backdrop rendering service",
289
+ "omarchy.bar": "Core desktop top bar container and layout supervisor",
290
+ "omarchy.battery": "Battery level indicator, charging status, and power threshold alerts",
291
+ "omarchy.bluetooth": "Bluetooth device scanner, pairing manager, and quick connect",
292
+ "omarchy.clipboard": "Searchable clipboard history popup overlay",
293
+ "omarchy.clock": "Date, time, world clocks, and integrated monthly calendar panel",
294
+ "omarchy.dev-gallery": "Developer gallery showcase for Quickshell UI components",
295
+ "omarchy.disk-speedtest": "Real-time disk read/write benchmark utility",
296
+ "omarchy.dropbox": "Dropbox file sync state and folder shortcut",
297
+ "omarchy.emojis": "Searchable emoji picker with instant clipboard copy",
298
+ "omarchy.idle": "Wayland idle manager and automatic screen locking service",
299
+ "omarchy.image-picker": "Desktop screenshot and image selector overlay",
300
+ "omarchy.indicators": "Quick-glance system status indicators (caps lock, mute, recording)",
301
+ "omarchy.keyboard-layout": "Keyboard layout indicator and XKB switcher",
302
+ "omarchy.lock": "Wayland lock screen overlay with password authentication",
303
+ "omarchy.media": "MPRIS media player controls (play/pause, track, seek)",
304
+ "omarchy.menu": "Application launcher and system command launcher",
305
+ "omarchy.microphone": "Microphone volume control and mute toggle",
306
+ "omarchy.monitor": "Display arrangement, brightness, and resolution manager",
307
+ "nosignal.motion-wallpaper": "Dynamic animated video and shader wallpaper background",
308
+ "omarchy.network": "Wi-Fi network scanner, signal strength, and Ethernet status",
309
+ "omarchy.nightlight": "Blue light reduction gamma adjustment service (Hyprsunset)",
310
+ "omarchy.notifications": "Desktop notification center and daemon",
311
+ "omarchy.osd": "On-screen display popup for volume and brightness adjustments",
312
+ "omarchy.polkit": "Polkit authentication agent for administrative privilege prompts",
313
+ "omarchy.power": "Power menu with shutdown, reboot, suspend, and logout options",
314
+ "omarchy.reminders": "Sticky notes and system reminder overlay",
315
+ "omarchy.spacer": "Flexible spacer element for custom bar layout separation",
316
+ "omarchy.speedtest": "Network internet speed test benchmark panel",
317
+ "omarchy.sysmon": "Live CPU, RAM, disk, network, and temperature monitor",
318
+ "omarchy.system-update": "Arch Linux pacman & AUR package update notification widget",
319
+ "omarchy.tailscale": "Tailscale VPN status toggle and exit node switcher",
320
+ "omarchy.tray": "System tray host for background app indicators",
321
+ "omarchy.weather": "Local weather forecast and temperature indicator",
322
+ "omarchy.wifiqr": "Generates a Wi-Fi QR code for quick mobile connection",
323
+ "omarchy.workspaces": "Visual Hyprland workspace indicator with active window dots",
324
+ "deomarchy.docker-status": "Docker container monitor with one-click lazydocker integration",
325
+ "deomarchy.power-profile": "Power profile switcher (Performance / Balanced / Power-saver)",
326
+ "deomarchy.wallpaper-manager": "Theme-synchronized wallpaper selector panel",
327
+ "deomarchy.system-overview": "Comprehensive system resource overview dashboard"
328
+ }
329
+ return map[p.id] || ("ID: " + p.id)
330
+ }
331
+
332
+ function applyInstalledFilter() {
333
+ var list = root.installedPlugins || []
334
+ var q = root.installedSearch.toLowerCase().trim()
335
+ var filter = root.installedFilter
336
+
337
+ var res = []
338
+ for (var i = 0; i < list.length; i++) {
339
+ var p = list[i]
340
+ if (filter === "active" && !p.enabled) continue
341
+ if (filter === "inactive" && p.enabled) continue
342
+ if (filter === "bar" && !isPluginInBar(p.id)) continue
343
+ if (filter === "builtin" && !p.firstParty) continue
344
+ if (filter === "user" && p.firstParty) continue
345
+
346
+ if (q.length > 0) {
347
+ var name = (p.name || "").toLowerCase()
348
+ var id = (p.id || "").toLowerCase()
349
+ var desc = getPluginDescription(p).toLowerCase()
350
+ var kinds = (p.kinds || []).join(" ").toLowerCase()
351
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && desc.indexOf(q) === -1 && kinds.indexOf(q) === -1) continue
352
+ }
353
+ res.push(p)
354
+ }
355
+ root.filteredInstalled = res
356
+ }
357
+
358
+ function applyMarketFilter() {
359
+ var list = root.marketplacePlugins || []
360
+ var q = root.marketSearch.toLowerCase().trim()
361
+ var cat = root.marketCategory
362
+
363
+ var res = []
364
+ for (var i = 0; i < list.length; i++) {
365
+ var p = list[i]
366
+ if (cat !== "All" && (p.category || "").toLowerCase() !== cat.toLowerCase()) continue
367
+
368
+ if (q.length > 0) {
369
+ var name = (p.name || "").toLowerCase()
370
+ var id = (p.id || "").toLowerCase()
371
+ var desc = (p.description || "").toLowerCase()
372
+ var author = (p.author || "").toLowerCase()
373
+ var tags = (p.tags || []).join(" ").toLowerCase()
374
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && desc.indexOf(q) === -1 && author.indexOf(q) === -1 && tags.indexOf(q) === -1) continue
375
+ }
376
+ res.push(p)
377
+ }
378
+
379
+ if (root.marketSort === "stars") {
380
+ res.sort(function(a, b) { return (b.stars || 0) - (a.stars || 0) })
381
+ } else if (root.marketSort === "name") {
382
+ res.sort(function(a, b) { return (a.name || a.id).localeCompare(b.name || b.id) })
383
+ } else if (root.marketSort === "date") {
384
+ res.sort(function(a, b) { return String(b.addedAt || "").localeCompare(String(a.addedAt || "")) })
385
+ }
386
+
387
+ root.filteredMarketplace = res
388
+ }
389
+
390
+ onInstalledSearchChanged: applyInstalledFilter()
391
+ onInstalledFilterChanged: applyInstalledFilter()
392
+ onMarketSearchChanged: applyMarketFilter()
393
+ onMarketCategoryChanged: applyMarketFilter()
394
+ onMarketSortChanged: applyMarketFilter()
395
+
396
+ // ---------------------------------------------------------------- Actions
397
+ function isInstalled(id) {
398
+ if (!id) return false
399
+ var cleanId = id.replace(/^[a-z0-9_-]+\./, "")
400
+ for (var i = 0; i < root.installedPlugins.length; i++) {
401
+ var instId = root.installedPlugins[i].id
402
+ if (instId === id || instId === cleanId || instId.endsWith("." + id) || id.endsWith("." + instId)) {
403
+ return true
404
+ }
405
+ }
406
+ return false
407
+ }
408
+
409
+ function showToast(msg, type) {
410
+ root.statusMessage = msg
411
+ root.statusType = type || "info"
412
+ toastTimer.restart()
413
+ }
414
+
415
+ Timer {
416
+ id: toastTimer
417
+ interval: 3500
418
+ onTriggered: root.statusMessage = ""
419
+ }
420
+
421
+ function togglePlugin(plugin) {
422
+ if (!plugin || !plugin.id) return
423
+ var targetState = !plugin.enabled
424
+ plugin.enabled = targetState
425
+ applyInstalledFilter()
426
+
427
+ actionProc.command = ["bash", "-c", "omarchy plugins " + (targetState ? "enable" : "disable") + " '" + plugin.id + "'"]
428
+ actionProc.targetAction = "toggle"
429
+ actionProc.targetName = plugin.name || plugin.id
430
+ actionProc.targetState = targetState
431
+ actionProc.running = true
432
+ }
433
+
434
+ function toggleBarPlacement(plugin) {
435
+ if (!plugin || !plugin.id) return
436
+ if (isPluginInBar(plugin.id)) {
437
+ actionProc.command = ["bash", "-c", "omarchy plugins bar remove '" + plugin.id + "'"]
438
+ actionProc.targetAction = "bar_remove"
439
+ actionProc.targetName = plugin.name || plugin.id
440
+ actionProc.running = true
441
+ } else {
442
+ root.barTargetPlugin = plugin
443
+ root.barSectionDialogOpen = true
444
+ }
445
+ }
446
+
447
+ function addPluginToBar(plugin, section) {
448
+ root.barSectionDialogOpen = false
449
+ root.barTargetPlugin = null
450
+ if (!plugin || !plugin.id) return
451
+
452
+ showToast("Adding " + (plugin.name || plugin.id) + " to bar (" + section + ")...", "info")
453
+ actionProc.command = ["bash", "-c", "omarchy plugins bar add '" + plugin.id + "' " + section]
454
+ actionProc.targetAction = "bar_add"
455
+ actionProc.targetName = plugin.name || plugin.id
456
+ actionProc.running = true
457
+ }
458
+
459
+ function openShortcutDialog(plugin) {
460
+ root.shortcutTargetPlugin = plugin
461
+ root.shortcutInput = root.shortcutsMap[plugin.id] || ""
462
+ root.shortcutDialogOpen = true
463
+ }
464
+
465
+ function saveShortcut() {
466
+ var p = root.shortcutTargetPlugin
467
+ var keys = root.shortcutInput.trim()
468
+ root.shortcutDialogOpen = false
469
+ root.shortcutTargetPlugin = null
470
+ if (!p) return
471
+
472
+ if (keys === "") {
473
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut remove '" + p.id + "'"]
474
+ actionProc.targetAction = "shortcut_remove"
475
+ actionProc.targetName = p.name || p.id
476
+ actionProc.running = true
477
+ } else {
478
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut set '" + p.id + "' '" + keys + "'"]
479
+ actionProc.targetAction = "shortcut_set"
480
+ actionProc.targetName = p.name || p.id
481
+ actionProc.running = true
482
+ }
483
+ }
484
+
485
+ function clearShortcut() {
486
+ root.shortcutInput = ""
487
+ saveShortcut()
488
+ }
489
+
490
+ function installPlugin(p) {
491
+ if (!p || root.installingId !== "") return
492
+ root.installingId = p.id
493
+ showToast("Installing " + (p.name || p.id) + "...", "info")
494
+
495
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + p.id + "' --enable"]
496
+ actionProc.targetAction = "install"
497
+ actionProc.targetName = p.name || p.id
498
+ actionProc.running = true
499
+ }
500
+
501
+ function installFromUrl(url) {
502
+ if (!url || url.trim() === "") return
503
+ url = url.trim()
504
+ root.urlDialogOpen = false
505
+ root.urlInput = ""
506
+ showToast("Installing: " + url + "...", "info")
507
+
508
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + url + "' --enable"]
509
+ actionProc.targetAction = "install"
510
+ actionProc.targetName = url
511
+ actionProc.running = true
512
+ }
513
+
514
+ function confirmUninstall(p) {
515
+ root.pendingUninstallPlugin = p
516
+ root.confirmUninstallOpen = true
517
+ }
518
+
519
+ function executeUninstall() {
520
+ var p = root.pendingUninstallPlugin
521
+ root.confirmUninstallOpen = false
522
+ root.pendingUninstallPlugin = null
523
+ if (!p) return
524
+
525
+ showToast("Removing " + (p.name || p.id) + "...", "info")
526
+ actionProc.command = ["bash", "-c", "omarchy plugins remove '" + p.id + "'"]
527
+ actionProc.targetAction = "remove"
528
+ actionProc.targetName = p.name || p.id
529
+ actionProc.running = true
530
+ }
531
+
532
+ function updateAllPlugins() {
533
+ root.updatingAll = true
534
+ showToast("Checking and updating all plugins...", "info")
535
+ actionProc.command = ["bash", "-c", "omarchy plugins update"]
536
+ actionProc.targetAction = "update_all"
537
+ actionProc.targetName = "all plugins"
538
+ actionProc.running = true
539
+ }
540
+
541
+ function restartShell() {
542
+ root.restartDialogOpen = false
543
+ showToast("Restarting Quickshell...", "info")
544
+ actionProc.command = ["bash", "-c", "omarchy-restart-shell"]
545
+ actionProc.targetAction = "restart"
546
+ actionProc.targetName = "shell"
547
+ actionProc.running = true
548
+ }
549
+
550
+ Process {
551
+ id: actionProc
552
+ running: false
553
+ property string targetAction: ""
554
+ property string targetName: ""
555
+ property bool targetState: false
556
+
557
+ onExited: function(code) {
558
+ root.installingId = ""
559
+ root.updatingAll = false
560
+ if (code === 0) {
561
+ var ok = root.nf(root.ic.checkCircle)
562
+ if (targetAction === "install") {
563
+ root.showToast(ok + " Installed " + targetName + "!", "success")
564
+ } else if (targetAction === "remove") {
565
+ root.showToast(ok + " Removed " + targetName + ".", "success")
566
+ } else if (targetAction === "toggle") {
567
+ root.showToast(ok + " " + (targetState ? "Enabled" : "Disabled") + " " + targetName, "success")
568
+ } else if (targetAction === "bar_add") {
569
+ root.showToast(ok + " Pinned " + targetName + " to top bar!", "success")
570
+ } else if (targetAction === "bar_remove") {
571
+ root.showToast(ok + " Unpinned " + targetName + " from bar.", "success")
572
+ } else if (targetAction === "shortcut_set") {
573
+ root.showToast(ok + " Shortcut saved for " + targetName + "!", "success")
574
+ } else if (targetAction === "shortcut_remove") {
575
+ root.showToast(ok + " Shortcut cleared for " + targetName, "success")
576
+ } else if (targetAction === "update_all") {
577
+ root.showToast(ok + " All plugins up to date!", "success")
578
+ } else if (targetAction === "restart") {
579
+ root.showToast(ok + " Shell restarted successfully!", "success")
580
+ }
581
+ } else {
582
+ root.showToast(root.nf(root.ic.closeCircle) + " Operation failed for " + targetName, "error")
583
+ }
584
+ root.loadInstalled()
585
+ root.loadShortcuts()
586
+ root.loadBarLayout()
587
+ }
588
+ }
589
+
590
+ // ---------------------------------------------------------------- Main Floating Window
591
+ Window {
592
+ id: win
593
+ title: "Omarchy Plugin Manager"
594
+ visible: true
595
+ width: 980
596
+ height: 740
597
+ minimumWidth: 840
598
+ minimumHeight: 600
599
+ color: root.bgBase
600
+
601
+ Rectangle {
602
+ anchors.fill: parent
603
+ color: root.bgBase
604
+
605
+ ColumnLayout {
606
+ anchors.fill: parent
607
+ anchors.margins: 20
608
+ spacing: 16
609
+
610
+ // ========================================================== Top Navigation Header
611
+ RowLayout {
612
+ Layout.fillWidth: true
613
+ spacing: 16
614
+
615
+ // Left Brand
616
+ RowLayout {
617
+ spacing: 12
618
+ Rectangle {
619
+ Layout.preferredWidth: 42
620
+ Layout.preferredHeight: 42
621
+ radius: 12
622
+ color: root.accentGlow
623
+ border.color: Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.4)
624
+ border.width: 1
625
+ Text {
626
+ anchors.centerIn: parent
627
+ text: root.nf(root.ic.puzzle)
628
+ color: root.accent
629
+ font.family: root.nfFontFamily
630
+ font.pixelSize: 22
631
+ }
632
+ }
633
+
634
+ ColumnLayout {
635
+ spacing: 2
636
+ Text {
637
+ text: "Plugin Manager"
638
+ color: root.fgMain
639
+ font.pixelSize: 18
640
+ font.bold: true
641
+ }
642
+ Text {
643
+ text: "Omarchy Desktop & Extensions"
644
+ color: root.fgMuted
645
+ font.pixelSize: 11
646
+ }
647
+ }
648
+ }
649
+
650
+ Item { Layout.fillWidth: true }
651
+
652
+ // Center Segmented Tab Bar
653
+ Rectangle {
654
+ Layout.preferredWidth: 320
655
+ Layout.preferredHeight: 40
656
+ radius: 20
657
+ color: root.bgSurface
658
+ border.color: root.borderSubtle
659
+ border.width: 1
660
+
661
+ RowLayout {
662
+ anchors.fill: parent
663
+ anchors.margins: 4
664
+ spacing: 4
665
+
666
+ Rectangle {
667
+ Layout.fillWidth: true
668
+ Layout.fillHeight: true
669
+ radius: 16
670
+ color: root.currentTab === "installed" ? root.accent : "transparent"
671
+
672
+ RowLayout {
673
+ anchors.centerIn: parent
674
+ spacing: 6
675
+ Text {
676
+ text: root.nf(root.ic.package)
677
+ color: root.currentTab === "installed" ? "#11121d" : root.fgMuted
678
+ font.family: root.nfFontFamily
679
+ font.pixelSize: 13
680
+ }
681
+ Text {
682
+ text: "Installed (" + root.installedPlugins.length + ")"
683
+ color: root.currentTab === "installed" ? "#11121d" : root.fgMuted
684
+ font.pixelSize: 12
685
+ font.bold: root.currentTab === "installed"
686
+ }
687
+ }
688
+
689
+ MouseArea {
690
+ anchors.fill: parent
691
+ cursorShape: Qt.PointingHandCursor
692
+ onClicked: root.currentTab = "installed"
693
+ }
694
+ }
695
+
696
+ Rectangle {
697
+ Layout.fillWidth: true
698
+ Layout.fillHeight: true
699
+ radius: 16
700
+ color: root.currentTab === "marketplace" ? root.accent : "transparent"
701
+
702
+ RowLayout {
703
+ anchors.centerIn: parent
704
+ spacing: 6
705
+ Text {
706
+ text: root.nf(root.ic.globe)
707
+ color: root.currentTab === "marketplace" ? "#11121d" : root.fgMuted
708
+ font.family: root.nfFontFamily
709
+ font.pixelSize: 13
710
+ }
711
+ Text {
712
+ text: "Marketplace (" + (root.marketplacePlugins.length || "1000+") + ")"
713
+ color: root.currentTab === "marketplace" ? "#11121d" : root.fgMuted
714
+ font.pixelSize: 12
715
+ font.bold: root.currentTab === "marketplace"
716
+ }
717
+ }
718
+
719
+ MouseArea {
720
+ anchors.fill: parent
721
+ cursorShape: Qt.PointingHandCursor
722
+ onClicked: {
723
+ root.currentTab = "marketplace"
724
+ if (root.marketplacePlugins.length === 0) root.loadMarketplace()
725
+ }
726
+ }
727
+ }
728
+ }
729
+ }
730
+
731
+ Item { Layout.fillWidth: true }
732
+
733
+ // Right Actions
734
+ RowLayout {
735
+ spacing: 8
736
+
737
+ // Git URL
738
+ Rectangle {
739
+ Layout.preferredHeight: 36
740
+ Layout.preferredWidth: btnGitText.implicitWidth + 28
741
+ radius: 10
742
+ color: root.bgSurface
743
+ border.color: root.borderSubtle
744
+ border.width: 1
745
+
746
+ RowLayout {
747
+ anchors.centerIn: parent
748
+ spacing: 6
749
+ Text {
750
+ text: root.nf(root.ic.plus)
751
+ color: root.accent
752
+ font.family: root.nfFontFamily
753
+ font.pixelSize: 13
754
+ }
755
+ Text {
756
+ id: btnGitText
757
+ text: "Git URL"
758
+ color: root.fgMain
759
+ font.pixelSize: 12
760
+ font.bold: true
761
+ }
762
+ }
763
+
764
+ MouseArea {
765
+ anchors.fill: parent
766
+ cursorShape: Qt.PointingHandCursor
767
+ onClicked: root.urlDialogOpen = true
768
+ }
769
+ }
770
+
771
+ // Update All
772
+ Rectangle {
773
+ Layout.preferredHeight: 36
774
+ Layout.preferredWidth: btnUpText.implicitWidth + 28
775
+ radius: 10
776
+ color: root.bgSurface
777
+ border.color: root.borderSubtle
778
+ border.width: 1
779
+
780
+ RowLayout {
781
+ anchors.centerIn: parent
782
+ spacing: 6
783
+ Text {
784
+ text: root.nf(root.ic.update)
785
+ color: root.greenSuccess
786
+ font.family: root.nfFontFamily
787
+ font.pixelSize: 13
788
+ }
789
+ Text {
790
+ id: btnUpText
791
+ text: "Update"
792
+ color: root.fgMain
793
+ font.pixelSize: 12
794
+ font.bold: true
795
+ }
796
+ }
797
+
798
+ MouseArea {
799
+ anchors.fill: parent
800
+ cursorShape: Qt.PointingHandCursor
801
+ onClicked: root.updateAllPlugins()
802
+ }
803
+ }
804
+
805
+ // Restart Shell
806
+ Rectangle {
807
+ Layout.preferredHeight: 36
808
+ Layout.preferredWidth: 38
809
+ radius: 10
810
+ color: root.bgSurface
811
+ border.color: root.borderSubtle
812
+ border.width: 1
813
+
814
+ Text {
815
+ anchors.centerIn: parent
816
+ text: root.nf(root.ic.bolt)
817
+ color: root.fgMain
818
+ font.family: root.nfFontFamily
819
+ font.pixelSize: 15
820
+ }
821
+
822
+ MouseArea {
823
+ anchors.fill: parent
824
+ cursorShape: Qt.PointingHandCursor
825
+ onClicked: root.restartDialogOpen = true
826
+ }
827
+ }
828
+ }
829
+ }
830
+
831
+ // ========================================================== Toast Banner
832
+ Rectangle {
833
+ Layout.fillWidth: true
834
+ Layout.preferredHeight: root.statusMessage !== "" ? 36 : 0
835
+ visible: root.statusMessage !== ""
836
+ radius: 10
837
+ color: root.statusType === "success" ? root.greenGlow : (root.statusType === "error" ? root.redGlow : root.accentGlow)
838
+ border.color: root.statusType === "success" ? root.greenSuccess : (root.statusType === "error" ? root.redDanger : root.accent)
839
+ border.width: 1
840
+ clip: true
841
+
842
+ Text {
843
+ anchors.centerIn: parent
844
+ text: root.statusMessage
845
+ color: root.fgMain
846
+ font.family: root.nfFontFamily
847
+ font.pixelSize: 12
848
+ font.bold: true
849
+ }
850
+ }
851
+
852
+ // ========================================================== Views
853
+ StackLayout {
854
+ Layout.fillWidth: true
855
+ Layout.fillHeight: true
856
+ currentIndex: root.currentTab === "installed" ? 0 : 1
857
+
858
+ // -------------------------------------------------------- TAB 1: INSTALLED
859
+ Item {
860
+ ColumnLayout {
861
+ anchors.fill: parent
862
+ spacing: 12
863
+
864
+ // Filter & Search Toolbar
865
+ RowLayout {
866
+ Layout.fillWidth: true
867
+ spacing: 12
868
+
869
+ // Search Box
870
+ Rectangle {
871
+ Layout.fillWidth: true
872
+ Layout.preferredHeight: 40
873
+ radius: 12
874
+ color: root.bgSurface
875
+ border.color: root.borderSubtle
876
+ border.width: 1
877
+
878
+ RowLayout {
879
+ anchors.fill: parent
880
+ anchors.leftMargin: 12
881
+ anchors.rightMargin: 12
882
+ spacing: 10
883
+
884
+ Text {
885
+ text: root.nf(root.ic.magnify)
886
+ color: root.fgDim
887
+ font.family: root.nfFontFamily
888
+ font.pixelSize: 14
889
+ }
890
+
891
+ TextInput {
892
+ id: installedSearchInput
893
+ Layout.fillWidth: true
894
+ text: root.installedSearch
895
+ color: root.fgMain
896
+ font.pixelSize: 13
897
+ selectByMouse: true
898
+ onTextChanged: root.installedSearch = text
899
+
900
+ Text {
901
+ text: "Search plugins by name, ID, or description..."
902
+ color: root.fgDim
903
+ font.pixelSize: 13
904
+ visible: !installedSearchInput.text
905
+ }
906
+ }
907
+
908
+ Text {
909
+ visible: installedSearchInput.text.length > 0
910
+ text: root.nf(root.ic.close)
911
+ color: root.fgDim
912
+ font.family: root.nfFontFamily
913
+ font.pixelSize: 13
914
+ MouseArea {
915
+ anchors.fill: parent
916
+ cursorShape: Qt.PointingHandCursor
917
+ onClicked: root.installedSearch = ""
918
+ }
919
+ }
920
+ }
921
+ }
922
+
923
+ // Filter Chips
924
+ RowLayout {
925
+ spacing: 6
926
+
927
+ Repeater {
928
+ model: [
929
+ { id: "all", label: "All (" + root.installedPlugins.length + ")" },
930
+ { id: "active", label: "Active" },
931
+ { id: "inactive", label: "Inactive" },
932
+ { id: "bar", label: "In Bar" },
933
+ { id: "builtin", label: "Built-in" },
934
+ { id: "user", label: "User" }
935
+ ]
936
+
937
+ delegate: Rectangle {
938
+ required property var modelData
939
+ Layout.preferredHeight: 34
940
+ Layout.preferredWidth: instChipText.implicitWidth + 22
941
+ radius: 17
942
+ color: root.installedFilter === modelData.id ? root.accentGlow : root.bgSurface
943
+ border.color: root.installedFilter === modelData.id ? root.accent : root.borderSubtle
944
+ border.width: 1
945
+
946
+ Text {
947
+ id: instChipText
948
+ anchors.centerIn: parent
949
+ text: modelData.label
950
+ color: root.installedFilter === modelData.id ? root.accent : root.fgMuted
951
+ font.pixelSize: 12
952
+ font.bold: root.installedFilter === modelData.id
953
+ }
954
+
955
+ MouseArea {
956
+ anchors.fill: parent
957
+ cursorShape: Qt.PointingHandCursor
958
+ onClicked: root.installedFilter = modelData.id
959
+ }
960
+ }
961
+ }
962
+ }
963
+ }
964
+
965
+ // List of Installed Plugins
966
+ ListView {
967
+ Layout.fillWidth: true
968
+ Layout.fillHeight: true
969
+ clip: true
970
+ spacing: 8
971
+ model: root.filteredInstalled
972
+
973
+ delegate: Rectangle {
974
+ required property var modelData
975
+ property bool inBar: root.isPluginInBar(modelData.id)
976
+ property string shortcut: root.shortcutsMap[modelData.id] || ""
977
+ property bool isBarCompatible: {
978
+ var kinds = modelData.kinds || []
979
+ return kinds.indexOf("bar-widget") !== -1 || kinds.indexOf("bar") !== -1 || kinds.indexOf("panel") !== -1
980
+ }
981
+
982
+ width: parent ? parent.width : 0
983
+ height: 74
984
+ radius: 12
985
+ color: modelData.enabled ? root.bgCard : root.bgSurface
986
+ border.color: modelData.enabled ? root.borderActive : root.borderSubtle
987
+ border.width: 1
988
+
989
+ RowLayout {
990
+ anchors.fill: parent
991
+ anchors.margins: 12
992
+ spacing: 14
993
+
994
+ // Dynamic Icon Badge
995
+ Rectangle {
996
+ Layout.preferredWidth: 46
997
+ Layout.preferredHeight: 46
998
+ radius: 12
999
+ color: modelData.enabled ? root.accentGlow : root.bgElevated
1000
+ border.color: modelData.enabled ? Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.3) : root.borderSubtle
1001
+ border.width: 1
1002
+
1003
+ Text {
1004
+ anchors.centerIn: parent
1005
+ text: root.getPluginIcon(modelData.id, modelData.name)
1006
+ color: modelData.enabled ? root.accent : root.fgDim
1007
+ font.family: root.nfFontFamily
1008
+ font.pixelSize: 22
1009
+ }
1010
+ }
1011
+
1012
+ // Metadata
1013
+ ColumnLayout {
1014
+ Layout.fillWidth: true
1015
+ spacing: 3
1016
+
1017
+ RowLayout {
1018
+ spacing: 8
1019
+ Text {
1020
+ text: modelData.name || modelData.id
1021
+ color: root.fgMain
1022
+ font.pixelSize: 14
1023
+ font.bold: true
1024
+ }
1025
+
1026
+ // Kind pills
1027
+ Repeater {
1028
+ model: modelData.kinds || []
1029
+ delegate: Rectangle {
1030
+ required property var modelData
1031
+ Layout.preferredHeight: 20
1032
+ Layout.preferredWidth: kindText.implicitWidth + 12
1033
+ radius: 6
1034
+ color: root.purpleGlow
1035
+ border.color: Qt.rgba(root.purpleTag.r, root.purpleTag.g, root.purpleTag.b, 0.3)
1036
+ border.width: 1
1037
+
1038
+ Text {
1039
+ id: kindText
1040
+ anchors.centerIn: parent
1041
+ text: modelData === "bar-widget" ? "Widget" : (modelData === "service" ? "Service" : (modelData === "panel" ? "Panel" : modelData))
1042
+ color: root.purpleTag
1043
+ font.pixelSize: 10
1044
+ font.bold: true
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ // Source pill
1050
+ Rectangle {
1051
+ Layout.preferredHeight: 20
1052
+ Layout.preferredWidth: srcText.implicitWidth + 12
1053
+ radius: 6
1054
+ color: modelData.firstParty ? Qt.rgba(255, 255, 255, 0.05) : root.greenGlow
1055
+ border.color: modelData.firstParty ? root.borderSubtle : Qt.rgba(root.greenSuccess.r, root.greenSuccess.g, root.greenSuccess.b, 0.3)
1056
+ border.width: 1
1057
+
1058
+ Text {
1059
+ id: srcText
1060
+ anchors.centerIn: parent
1061
+ text: modelData.firstParty ? "Core" : "User"
1062
+ color: modelData.firstParty ? root.fgDim : root.greenSuccess
1063
+ font.pixelSize: 10
1064
+ font.bold: true
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ Text {
1070
+ Layout.fillWidth: true
1071
+ text: root.getPluginDescription(modelData)
1072
+ color: root.fgMuted
1073
+ font.pixelSize: 11
1074
+ elide: Text.ElideRight
1075
+ }
1076
+ }
1077
+
1078
+ // Interactive Controls Row
1079
+ RowLayout {
1080
+ spacing: 8
1081
+
1082
+ // Bar Pin Button
1083
+ Rectangle {
1084
+ visible: isBarCompatible
1085
+ Layout.preferredHeight: 32
1086
+ Layout.preferredWidth: barBtnText.implicitWidth + 24
1087
+ radius: 8
1088
+ color: inBar ? root.greenGlow : root.bgElevated
1089
+ border.color: inBar ? root.greenSuccess : root.borderSubtle
1090
+ border.width: 1
1091
+
1092
+ RowLayout {
1093
+ anchors.centerIn: parent
1094
+ spacing: 6
1095
+ Text {
1096
+ text: inBar ? root.nf(root.ic.pin) : root.nf(root.ic.plus)
1097
+ color: inBar ? root.greenSuccess : root.fgMuted
1098
+ font.family: root.nfFontFamily
1099
+ font.pixelSize: 12
1100
+ }
1101
+ Text {
1102
+ id: barBtnText
1103
+ text: inBar ? "In Top Bar" : "Pin to Bar"
1104
+ color: inBar ? root.greenSuccess : root.fgMuted
1105
+ font.pixelSize: 11
1106
+ font.bold: inBar
1107
+ }
1108
+ }
1109
+
1110
+ MouseArea {
1111
+ anchors.fill: parent
1112
+ cursorShape: Qt.PointingHandCursor
1113
+ onClicked: root.toggleBarPlacement(modelData)
1114
+ }
1115
+ }
1116
+
1117
+ // Shortcut Keycap Button
1118
+ Rectangle {
1119
+ Layout.preferredHeight: 32
1120
+ Layout.preferredWidth: scBtnText.implicitWidth + 24
1121
+ radius: 8
1122
+ color: shortcut !== "" ? root.amberGlow : root.bgElevated
1123
+ border.color: shortcut !== "" ? root.amberWarn : root.borderSubtle
1124
+ border.width: 1
1125
+
1126
+ RowLayout {
1127
+ anchors.centerIn: parent
1128
+ spacing: 6
1129
+ Text {
1130
+ text: root.nf(root.ic.keyboard)
1131
+ color: shortcut !== "" ? root.amberWarn : root.fgDim
1132
+ font.family: root.nfFontFamily
1133
+ font.pixelSize: 13
1134
+ }
1135
+ Text {
1136
+ id: scBtnText
1137
+ text: shortcut !== "" ? shortcut : "Shortcut"
1138
+ color: shortcut !== "" ? root.amberWarn : root.fgMuted
1139
+ font.pixelSize: 11
1140
+ font.bold: shortcut !== ""
1141
+ }
1142
+ }
1143
+
1144
+ MouseArea {
1145
+ anchors.fill: parent
1146
+ cursorShape: Qt.PointingHandCursor
1147
+ onClicked: root.openShortcutDialog(modelData)
1148
+ }
1149
+ }
1150
+
1151
+ // Delete User Plugin
1152
+ Rectangle {
1153
+ visible: !modelData.firstParty
1154
+ Layout.preferredWidth: 32
1155
+ Layout.preferredHeight: 32
1156
+ radius: 8
1157
+ color: root.redGlow
1158
+ border.color: Qt.rgba(root.redDanger.r, root.redDanger.g, root.redDanger.b, 0.4)
1159
+ border.width: 1
1160
+
1161
+ Text {
1162
+ anchors.centerIn: parent
1163
+ text: root.nf(root.ic.trash)
1164
+ color: root.redDanger
1165
+ font.family: root.nfFontFamily
1166
+ font.pixelSize: 14
1167
+ }
1168
+
1169
+ MouseArea {
1170
+ anchors.fill: parent
1171
+ cursorShape: Qt.PointingHandCursor
1172
+ onClicked: root.confirmUninstall(modelData)
1173
+ }
1174
+ }
1175
+
1176
+ // Active Toggle Switch
1177
+ Rectangle {
1178
+ Layout.preferredWidth: 48
1179
+ Layout.preferredHeight: 28
1180
+ radius: 14
1181
+ color: modelData.enabled ? root.accent : root.bgElevated
1182
+ border.color: modelData.enabled ? root.accent : root.borderSubtle
1183
+ border.width: 1
1184
+
1185
+ Rectangle {
1186
+ width: 22
1187
+ height: 22
1188
+ radius: 11
1189
+ color: "#ffffff"
1190
+ anchors.verticalCenter: parent.verticalCenter
1191
+ x: modelData.enabled ? 23 : 3
1192
+ Behavior on x { NumberAnimation { duration: 120 } }
1193
+ }
1194
+
1195
+ MouseArea {
1196
+ anchors.fill: parent
1197
+ cursorShape: Qt.PointingHandCursor
1198
+ onClicked: root.togglePlugin(modelData)
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ // -------------------------------------------------------- TAB 2: MARKETPLACE
1209
+ Item {
1210
+ ColumnLayout {
1211
+ anchors.fill: parent
1212
+ spacing: 12
1213
+
1214
+ // Filter & Search Toolbar
1215
+ RowLayout {
1216
+ Layout.fillWidth: true
1217
+ spacing: 12
1218
+
1219
+ // Search Box
1220
+ Rectangle {
1221
+ Layout.fillWidth: true
1222
+ Layout.preferredHeight: 40
1223
+ radius: 12
1224
+ color: root.bgSurface
1225
+ border.color: root.borderSubtle
1226
+ border.width: 1
1227
+
1228
+ RowLayout {
1229
+ anchors.fill: parent
1230
+ anchors.leftMargin: 12
1231
+ anchors.rightMargin: 12
1232
+ spacing: 10
1233
+
1234
+ Text {
1235
+ text: root.nf(root.ic.globe)
1236
+ color: root.fgDim
1237
+ font.family: root.nfFontFamily
1238
+ font.pixelSize: 14
1239
+ }
1240
+
1241
+ TextInput {
1242
+ id: marketSearchInput
1243
+ Layout.fillWidth: true
1244
+ text: root.marketSearch
1245
+ color: root.fgMain
1246
+ font.pixelSize: 13
1247
+ selectByMouse: true
1248
+ onTextChanged: root.marketSearch = text
1249
+
1250
+ Text {
1251
+ text: "Search 1,000+ community extensions from OmarchyPlugins.com..."
1252
+ color: root.fgDim
1253
+ font.pixelSize: 13
1254
+ visible: !marketSearchInput.text
1255
+ }
1256
+ }
1257
+
1258
+ Text {
1259
+ visible: marketSearchInput.text.length > 0
1260
+ text: root.nf(root.ic.close)
1261
+ color: root.fgDim
1262
+ font.family: root.nfFontFamily
1263
+ font.pixelSize: 13
1264
+ MouseArea {
1265
+ anchors.fill: parent
1266
+ cursorShape: Qt.PointingHandCursor
1267
+ onClicked: root.marketSearch = ""
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+
1273
+ // Category Chips ScrollView
1274
+ ScrollView {
1275
+ Layout.preferredWidth: 460
1276
+ Layout.preferredHeight: 40
1277
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
1278
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
1279
+
1280
+ RowLayout {
1281
+ spacing: 6
1282
+ Repeater {
1283
+ model: ["All", "Widgets", "Desktop", "System", "Productivity", "Developer Tools", "Appearance", "Utilities"]
1284
+ delegate: Rectangle {
1285
+ required property var modelData
1286
+ Layout.preferredHeight: 34
1287
+ Layout.preferredWidth: mCatText.implicitWidth + 20
1288
+ radius: 17
1289
+ color: root.marketCategory === modelData ? root.accentGlow : root.bgSurface
1290
+ border.color: root.marketCategory === modelData ? root.accent : root.borderSubtle
1291
+ border.width: 1
1292
+
1293
+ Text {
1294
+ id: mCatText
1295
+ anchors.centerIn: parent
1296
+ text: modelData
1297
+ color: root.marketCategory === modelData ? root.accent : root.fgMuted
1298
+ font.pixelSize: 12
1299
+ font.bold: root.marketCategory === modelData
1300
+ }
1301
+
1302
+ MouseArea {
1303
+ anchors.fill: parent
1304
+ cursorShape: Qt.PointingHandCursor
1305
+ onClicked: root.marketCategory = modelData
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+
1313
+ // Marketplace Cards List
1314
+ ListView {
1315
+ Layout.fillWidth: true
1316
+ Layout.fillHeight: true
1317
+ clip: true
1318
+ spacing: 8
1319
+ model: root.filteredMarketplace
1320
+
1321
+ delegate: Rectangle {
1322
+ required property var modelData
1323
+ property bool installed: root.isInstalled(modelData.id)
1324
+
1325
+ width: parent ? parent.width : 0
1326
+ height: 78
1327
+ radius: 12
1328
+ color: installed ? root.bgCard : root.bgSurface
1329
+ border.color: installed ? Qt.rgba(root.greenSuccess.r, root.greenSuccess.g, root.greenSuccess.b, 0.3) : root.borderSubtle
1330
+ border.width: 1
1331
+
1332
+ RowLayout {
1333
+ anchors.fill: parent
1334
+ anchors.margins: 12
1335
+ spacing: 14
1336
+
1337
+ // Initial Badge
1338
+ Rectangle {
1339
+ Layout.preferredWidth: 46
1340
+ Layout.preferredHeight: 46
1341
+ radius: 12
1342
+ color: root.bgElevated
1343
+ border.color: root.borderSubtle
1344
+ border.width: 1
1345
+
1346
+ Text {
1347
+ anchors.centerIn: parent
1348
+ text: modelData.initials || root.getPluginIcon(modelData.id, modelData.name)
1349
+ color: root.accent
1350
+ font.family: root.nfFontFamily
1351
+ font.pixelSize: modelData.initials ? 16 : 22
1352
+ font.bold: !!modelData.initials
1353
+ }
1354
+ }
1355
+
1356
+ // Metadata
1357
+ ColumnLayout {
1358
+ Layout.fillWidth: true
1359
+ spacing: 3
1360
+
1361
+ RowLayout {
1362
+ spacing: 8
1363
+ Text {
1364
+ text: modelData.name || modelData.id
1365
+ color: root.fgMain
1366
+ font.pixelSize: 14
1367
+ font.bold: true
1368
+ }
1369
+
1370
+ Text {
1371
+ visible: (modelData.stars || 0) > 0
1372
+ text: root.nf(root.ic.star) + " " + modelData.stars
1373
+ color: "#e0af68"
1374
+ font.family: root.nfFontFamily
1375
+ font.pixelSize: 11
1376
+ font.bold: true
1377
+ }
1378
+
1379
+ Rectangle {
1380
+ visible: modelData.category !== undefined && modelData.category !== ""
1381
+ Layout.preferredHeight: 20
1382
+ Layout.preferredWidth: catPill.implicitWidth + 12
1383
+ radius: 6
1384
+ color: root.purpleGlow
1385
+ border.color: Qt.rgba(root.purpleTag.r, root.purpleTag.g, root.purpleTag.b, 0.3)
1386
+ border.width: 1
1387
+
1388
+ Text {
1389
+ id: catPill
1390
+ anchors.centerIn: parent
1391
+ text: modelData.category || "Misc"
1392
+ color: root.purpleTag
1393
+ font.pixelSize: 10
1394
+ }
1395
+ }
1396
+
1397
+ Text {
1398
+ visible: modelData.author !== undefined && modelData.author !== ""
1399
+ text: "by " + modelData.author
1400
+ color: root.fgDim
1401
+ font.pixelSize: 11
1402
+ }
1403
+ }
1404
+
1405
+ Text {
1406
+ Layout.fillWidth: true
1407
+ text: modelData.description || ""
1408
+ color: root.fgMuted
1409
+ font.pixelSize: 11
1410
+ elide: Text.ElideRight
1411
+ }
1412
+ }
1413
+
1414
+ // Actions
1415
+ RowLayout {
1416
+ spacing: 8
1417
+
1418
+ // Repo Link
1419
+ Rectangle {
1420
+ visible: modelData.repo !== undefined && modelData.repo !== ""
1421
+ Layout.preferredWidth: 34
1422
+ Layout.preferredHeight: 34
1423
+ radius: 8
1424
+ color: root.bgElevated
1425
+ border.color: root.borderSubtle
1426
+ border.width: 1
1427
+
1428
+ Text {
1429
+ anchors.centerIn: parent
1430
+ text: root.nf(root.ic.externalLink)
1431
+ color: root.fgMain
1432
+ font.family: root.nfFontFamily
1433
+ font.pixelSize: 14
1434
+ }
1435
+
1436
+ MouseArea {
1437
+ anchors.fill: parent
1438
+ cursorShape: Qt.PointingHandCursor
1439
+ onClicked: {
1440
+ if (modelData.repo) {
1441
+ Qt.openUrlExternally(modelData.repo)
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+
1447
+ // Install / Installed Status Button
1448
+ Rectangle {
1449
+ Layout.preferredHeight: 34
1450
+ Layout.preferredWidth: installed ? 100 : 84
1451
+ radius: 8
1452
+ color: installed ? root.greenGlow : (root.installingId === modelData.id ? root.bgElevated : root.accent)
1453
+ border.color: installed ? root.greenSuccess : "transparent"
1454
+ border.width: installed ? 1 : 0
1455
+
1456
+ Text {
1457
+ anchors.centerIn: parent
1458
+ text: installed ? root.nf(root.ic.check) + " Installed" : (root.installingId === modelData.id ? "Installing..." : "Install")
1459
+ color: installed ? root.greenSuccess : (root.installingId === modelData.id ? root.fgDim : "#11121d")
1460
+ font.family: root.nfFontFamily
1461
+ font.pixelSize: 11
1462
+ font.bold: true
1463
+ }
1464
+
1465
+ MouseArea {
1466
+ anchors.fill: parent
1467
+ cursorShape: Qt.PointingHandCursor
1468
+ enabled: !installed && root.installingId === ""
1469
+ onClicked: root.installPlugin(modelData)
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ // ========================================================== MODALS
1482
+ // 1. Keyboard Shortcut Dialog Modal
1483
+ Rectangle {
1484
+ anchors.fill: parent
1485
+ color: Qt.rgba(0, 0, 0, 0.8)
1486
+ visible: root.shortcutDialogOpen
1487
+
1488
+ MouseArea { anchors.fill: parent; onClicked: root.shortcutDialogOpen = false }
1489
+
1490
+ Rectangle {
1491
+ anchors.centerIn: parent
1492
+ width: 480
1493
+ height: 290
1494
+ radius: 16
1495
+ color: root.bgSurface
1496
+ border.color: root.borderActive
1497
+ border.width: 1
1498
+
1499
+ MouseArea { anchors.fill: parent }
1500
+
1501
+ ColumnLayout {
1502
+ anchors.fill: parent
1503
+ anchors.margins: 22
1504
+ spacing: 14
1505
+
1506
+ RowLayout {
1507
+ spacing: 10
1508
+ Text {
1509
+ text: root.nf(root.ic.keyboard)
1510
+ color: root.amberWarn
1511
+ font.family: root.nfFontFamily
1512
+ font.pixelSize: 19
1513
+ }
1514
+ Text {
1515
+ text: "Set Keyboard Shortcut"
1516
+ color: root.fgMain
1517
+ font.pixelSize: 16
1518
+ font.bold: true
1519
+ }
1520
+ }
1521
+
1522
+ Text {
1523
+ text: "Configure a global Hyprland keybind for '" + (root.shortcutTargetPlugin ? (root.shortcutTargetPlugin.name || root.shortcutTargetPlugin.id) : "") + "':"
1524
+ color: root.fgMuted
1525
+ font.pixelSize: 12
1526
+ }
1527
+
1528
+ // Key Input Box
1529
+ Rectangle {
1530
+ Layout.fillWidth: true
1531
+ Layout.preferredHeight: 42
1532
+ radius: 10
1533
+ color: root.bgBase
1534
+ border.color: root.amberWarn
1535
+ border.width: 1
1536
+
1537
+ RowLayout {
1538
+ anchors.fill: parent
1539
+ anchors.margins: 10
1540
+ spacing: 10
1541
+
1542
+ TextInput {
1543
+ id: scInputField
1544
+ Layout.fillWidth: true
1545
+ text: root.shortcutInput
1546
+ color: root.fgMain
1547
+ font.pixelSize: 13
1548
+ font.bold: true
1549
+ selectByMouse: true
1550
+ onTextChanged: root.shortcutInput = text
1551
+
1552
+ Text {
1553
+ text: "e.g. SUPER + ALT + D"
1554
+ color: root.fgDim
1555
+ font.pixelSize: 13
1556
+ visible: !scInputField.text
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ // Quick Modifiers row
1563
+ RowLayout {
1564
+ spacing: 6
1565
+ Text { text: "Add:"; color: root.fgDim; font.pixelSize: 11 }
1566
+ Repeater {
1567
+ model: ["SUPER + ", "ALT + ", "CTRL + ", "SHIFT + "]
1568
+ delegate: Rectangle {
1569
+ required property var modelData
1570
+ Layout.preferredHeight: 26
1571
+ Layout.preferredWidth: qModText.implicitWidth + 14
1572
+ radius: 6
1573
+ color: root.bgElevated
1574
+ border.color: root.borderSubtle
1575
+ border.width: 1
1576
+
1577
+ Text {
1578
+ id: qModText
1579
+ anchors.centerIn: parent
1580
+ text: modelData
1581
+ color: root.fgMain
1582
+ font.pixelSize: 11
1583
+ }
1584
+
1585
+ MouseArea {
1586
+ anchors.fill: parent
1587
+ cursorShape: Qt.PointingHandCursor
1588
+ onClicked: {
1589
+ if (root.shortcutInput.indexOf(modelData) === -1) {
1590
+ root.shortcutInput = modelData + root.shortcutInput
1591
+ }
1592
+ }
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ Item { Layout.fillHeight: true }
1599
+
1600
+ RowLayout {
1601
+ Layout.fillWidth: true
1602
+ Button {
1603
+ text: "Clear Shortcut"
1604
+ onClicked: root.clearShortcut()
1605
+ }
1606
+ Item { Layout.fillWidth: true }
1607
+ Button {
1608
+ text: "Cancel"
1609
+ onClicked: root.shortcutDialogOpen = false
1610
+ }
1611
+ Button {
1612
+ text: "Save & Apply"
1613
+ highlighted: true
1614
+ onClicked: root.saveShortcut()
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ }
1620
+
1621
+ // 2. Bar Section Selector Modal
1622
+ Rectangle {
1623
+ anchors.fill: parent
1624
+ color: Qt.rgba(0, 0, 0, 0.8)
1625
+ visible: root.barSectionDialogOpen
1626
+
1627
+ MouseArea { anchors.fill: parent; onClicked: root.barSectionDialogOpen = false }
1628
+
1629
+ Rectangle {
1630
+ anchors.centerIn: parent
1631
+ width: 440
1632
+ height: 230
1633
+ radius: 16
1634
+ color: root.bgSurface
1635
+ border.color: root.borderActive
1636
+ border.width: 1
1637
+
1638
+ MouseArea { anchors.fill: parent }
1639
+
1640
+ ColumnLayout {
1641
+ anchors.fill: parent
1642
+ anchors.margins: 22
1643
+ spacing: 14
1644
+
1645
+ RowLayout {
1646
+ spacing: 8
1647
+ Text {
1648
+ text: root.nf(root.ic.pin)
1649
+ color: root.accent
1650
+ font.family: root.nfFontFamily
1651
+ font.pixelSize: 17
1652
+ }
1653
+ Text {
1654
+ text: "Pin to Omarchy Top Bar"
1655
+ color: root.fgMain
1656
+ font.pixelSize: 16
1657
+ font.bold: true
1658
+ }
1659
+ }
1660
+
1661
+ Text {
1662
+ text: "Choose which section of the top bar to place '" + (root.barTargetPlugin ? (root.barTargetPlugin.name || root.barTargetPlugin.id) : "") + "':"
1663
+ color: root.fgMuted
1664
+ font.pixelSize: 12
1665
+ }
1666
+
1667
+ RowLayout {
1668
+ Layout.fillWidth: true
1669
+ spacing: 10
1670
+
1671
+ Button {
1672
+ Layout.fillWidth: true
1673
+ text: root.nf(root.ic.arrowLeft) + " Left"
1674
+ font.family: root.nfFontFamily
1675
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "left")
1676
+ }
1677
+ Button {
1678
+ Layout.fillWidth: true
1679
+ text: "Center"
1680
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "center")
1681
+ }
1682
+ Button {
1683
+ Layout.fillWidth: true
1684
+ text: "Right " + root.nf(root.ic.arrowRight)
1685
+ font.family: root.nfFontFamily
1686
+ highlighted: true
1687
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "right")
1688
+ }
1689
+ }
1690
+
1691
+ Item { Layout.fillHeight: true }
1692
+
1693
+ RowLayout {
1694
+ Layout.fillWidth: true
1695
+ Item { Layout.fillWidth: true }
1696
+ Button {
1697
+ text: "Cancel"
1698
+ onClicked: root.barSectionDialogOpen = false
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+
1705
+ // 3. Git URL Dialog Modal
1706
+ Rectangle {
1707
+ anchors.fill: parent
1708
+ color: Qt.rgba(0, 0, 0, 0.8)
1709
+ visible: root.urlDialogOpen
1710
+
1711
+ MouseArea { anchors.fill: parent; onClicked: root.urlDialogOpen = false }
1712
+
1713
+ Rectangle {
1714
+ anchors.centerIn: parent
1715
+ width: 480
1716
+ height: 210
1717
+ radius: 16
1718
+ color: root.bgSurface
1719
+ border.color: root.borderActive
1720
+ border.width: 1
1721
+
1722
+ MouseArea { anchors.fill: parent }
1723
+
1724
+ ColumnLayout {
1725
+ anchors.fill: parent
1726
+ anchors.margins: 22
1727
+ spacing: 14
1728
+
1729
+ Text {
1730
+ text: "Install from Git Repository"
1731
+ color: root.fgMain
1732
+ font.pixelSize: 16
1733
+ font.bold: true
1734
+ }
1735
+
1736
+ Rectangle {
1737
+ Layout.fillWidth: true
1738
+ Layout.preferredHeight: 40
1739
+ radius: 10
1740
+ color: root.bgBase
1741
+ border.color: root.accent
1742
+ border.width: 1
1743
+
1744
+ TextInput {
1745
+ anchors.fill: parent
1746
+ anchors.margins: 10
1747
+ text: root.urlInput
1748
+ color: root.fgMain
1749
+ font.pixelSize: 13
1750
+ selectByMouse: true
1751
+ onTextChanged: root.urlInput = text
1752
+ Text {
1753
+ text: "https://github.com/author/plugin.git"
1754
+ color: root.fgDim
1755
+ font.pixelSize: 13
1756
+ visible: !parent.text
1757
+ }
1758
+ }
1759
+ }
1760
+
1761
+ RowLayout {
1762
+ Layout.fillWidth: true
1763
+ Item { Layout.fillWidth: true }
1764
+ Button {
1765
+ text: "Cancel"
1766
+ onClicked: root.urlDialogOpen = false
1767
+ }
1768
+ Button {
1769
+ text: "Install & Enable"
1770
+ highlighted: true
1771
+ onClicked: root.installFromUrl(root.urlInput)
1772
+ }
1773
+ }
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ // 4. Confirm Uninstall Modal
1779
+ Rectangle {
1780
+ anchors.fill: parent
1781
+ color: Qt.rgba(0, 0, 0, 0.8)
1782
+ visible: root.confirmUninstallOpen
1783
+
1784
+ MouseArea { anchors.fill: parent; onClicked: root.confirmUninstallOpen = false }
1785
+
1786
+ Rectangle {
1787
+ anchors.centerIn: parent
1788
+ width: 420
1789
+ height: 180
1790
+ radius: 16
1791
+ color: root.bgSurface
1792
+ border.color: root.redDanger
1793
+ border.width: 1
1794
+
1795
+ MouseArea { anchors.fill: parent }
1796
+
1797
+ ColumnLayout {
1798
+ anchors.fill: parent
1799
+ anchors.margins: 22
1800
+ spacing: 12
1801
+
1802
+ Text {
1803
+ text: "Uninstall Plugin"
1804
+ color: root.redDanger
1805
+ font.pixelSize: 16
1806
+ font.bold: true
1807
+ }
1808
+
1809
+ Text {
1810
+ text: "Are you sure you want to completely remove '" + (root.pendingUninstallPlugin ? (root.pendingUninstallPlugin.name || root.pendingUninstallPlugin.id) : "") + "'?"
1811
+ color: root.fgMain
1812
+ font.pixelSize: 12
1813
+ }
1814
+
1815
+ RowLayout {
1816
+ Layout.fillWidth: true
1817
+ Item { Layout.fillWidth: true }
1818
+ Button {
1819
+ text: "Cancel"
1820
+ onClicked: root.confirmUninstallOpen = false
1821
+ }
1822
+ Button {
1823
+ text: "Remove Plugin"
1824
+ highlighted: true
1825
+ onClicked: root.executeUninstall()
1826
+ }
1827
+ }
1828
+ }
1829
+ }
1830
+ }
1831
+
1832
+ // 5. Restart Shell Modal
1833
+ Rectangle {
1834
+ anchors.fill: parent
1835
+ color: Qt.rgba(0, 0, 0, 0.8)
1836
+ visible: root.restartDialogOpen
1837
+
1838
+ MouseArea { anchors.fill: parent; onClicked: root.restartDialogOpen = false }
1839
+
1840
+ Rectangle {
1841
+ anchors.centerIn: parent
1842
+ width: 420
1843
+ height: 180
1844
+ radius: 16
1845
+ color: root.bgSurface
1846
+ border.color: root.borderActive
1847
+ border.width: 1
1848
+
1849
+ MouseArea { anchors.fill: parent }
1850
+
1851
+ ColumnLayout {
1852
+ anchors.fill: parent
1853
+ anchors.margins: 22
1854
+ spacing: 12
1855
+
1856
+ RowLayout {
1857
+ spacing: 8
1858
+ Text {
1859
+ text: root.nf(root.ic.bolt)
1860
+ color: root.amberWarn
1861
+ font.family: root.nfFontFamily
1862
+ font.pixelSize: 17
1863
+ }
1864
+ Text {
1865
+ text: "Restart Quickshell"
1866
+ color: root.fgMain
1867
+ font.pixelSize: 16
1868
+ font.bold: true
1869
+ }
1870
+ }
1871
+
1872
+ Text {
1873
+ text: "Restart the Omarchy desktop shell to reload all widgets and panels?"
1874
+ color: root.fgMuted
1875
+ font.pixelSize: 12
1876
+ }
1877
+
1878
+ RowLayout {
1879
+ Layout.fillWidth: true
1880
+ Item { Layout.fillWidth: true }
1881
+ Button {
1882
+ text: "Cancel"
1883
+ onClicked: root.restartDialogOpen = false
1884
+ }
1885
+ Button {
1886
+ text: "Restart Shell"
1887
+ highlighted: true
1888
+ onClicked: root.restartShell()
1889
+ }
1890
+ }
1891
+ }
1892
+ }
1893
+ }
1894
+ }
1895
+ }
1896
+}
0
1897
▼registry.json
+23-1
3
3
"version": "1.0.0",
4
4
"plugins": [
5
5
{
6
- "id": "deomarchy.sysmon",
6
+ "id": "deomarchy.plugin-manager",
7
+ "name": "Plugin Manager",
8
+ "description": "Manage installed plugins and discover 1,000+ community extensions from OmarchyPlugins.com",
9
+ "author": "de-omarchy",
10
+ "category": "System",
11
+ "tags": ["management", "marketplace", "panel", "bar-widget"],
12
+ "kind": "panel",
13
+ "status": "stable",
14
+ "installCommand": "cp -r shell/plugins/plugin-manager ~/.config/omarchy/plugins/"
15
+ },
16
+ {
17
+ "id": "omaplug",
18
+ "name": "Omaplug",
19
+ "description": "Standalone Omarchy plugin manager: enable/disable, update, install, and remove plugins",
20
+ "author": "Fross",
21
+ "category": "System",
22
+ "tags": ["management", "bar-widget"],
23
+ "kind": "bar-widget",
24
+ "status": "stable",
25
+ "installCommand": "git clone https://github.com/fross100/omaplug.git ~/.config/omarchy/plugins/omaplug"
26
+ },
27
+ {
28
+ "id": "omarchy.sysmon",
7
29
"name": "System Monitor",
8
30
"description": "CPU, RAM, disk, GPU temp, network in bar",
9
31
"author": "de-omarchy",
10
32
▼shell/plugins/plugin-library/Panel.qml
+1158-141
1
1
import QtQuick
2
+import QtQuick.Controls
3
+import QtQuick.Layouts
2
4
import Quickshell
3
5
import Quickshell.Io
4
6
import qs.Commons
6
8
7
9
Panel {
8
10
id: root
9
- moduleName: "deomarchy.plugin-library"
11
+ moduleName: "deomarchy.plugin-manager"
12
+ ipcTarget: "deomarchy.plugin-manager"
10
13
11
- property var plugins: []
12
- property int selectedIndex: -1
13
- property string statusText: ""
14
- property bool installing: false
14
+ implicitWidth: 700
15
+ implicitHeight: 720
15
16
16
- implicitWidth: 520
17
- implicitHeight: 600
17
+ property var anchorItem: null
18
+ property var hostWidget: null
19
+ readonly property var barIdentity: hostWidget || root
18
20
19
- Component.onCompleted: loadRegistry()
21
+ // ---------------------------------------------------------------- State
22
+ property string currentTab: "installed" // "installed" | "marketplace"
23
+ property var installedPlugins: []
24
+ property var marketplacePlugins: []
25
+ property var filteredInstalled: []
26
+ property var filteredMarketplace: []
27
+ property var shortcutsMap: ({})
28
+ property var barLayoutData: ({ inBar: [] })
20
29
21
- function loadRegistry() {
22
- var path = Quickshell.env("HOME") + "/.config/omarchy/plugins/registry.json"
23
- var fallback = (Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy") + "/registry.json"
24
- registryProc.command = ["bash", "-c", "cat " + path + " 2>/dev/null || cat " + fallback + " 2>/dev/null || echo '{\"plugins\":[]}'"]
25
- registryProc.running = true
30
+ property string installedSearch: ""
31
+ property string installedFilter: "all" // "all" | "active" | "inactive" | "bar" | "builtin" | "user"
32
+
33
+ property string marketSearch: ""
34
+ property string marketCategory: "All"
35
+ property string marketSort: "stars" // "stars" | "name" | "date"
36
+
37
+ property string installingId: ""
38
+ property string statusMessage: ""
39
+ property string statusType: "info"
40
+ property bool updatingAll: false
41
+
42
+ property bool urlDialogOpen: false
43
+ property string urlInput: ""
44
+
45
+ property bool confirmUninstallOpen: false
46
+ property var pendingUninstallPlugin: null
47
+
48
+ property bool shortcutDialogOpen: false
49
+ property var shortcutTargetPlugin: null
50
+ property string shortcutInput: ""
51
+
52
+ property bool barSectionDialogOpen: false
53
+ property var barTargetPlugin: null
54
+
55
+ readonly property var registry: root.bar && root.bar.shell
56
+ ? root.bar.shell.pluginRegistry
57
+ : (root.bar ? root.bar.pluginRegistry : null)
58
+
59
+ Component.onCompleted: {
60
+ loadInstalled()
61
+ loadMarketplace()
62
+ loadShortcuts()
63
+ loadBarLayout()
64
+ }
65
+
66
+ onOpenedChanged: {
67
+ if (opened) {
68
+ loadInstalled()
69
+ loadMarketplace()
70
+ loadShortcuts()
71
+ loadBarLayout()
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------- Loading
76
+ function loadInstalled() {
77
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
78
+ installedProc.command = ["bash", "-c", "omarchy-shell shell listPlugins 2>/dev/null || " + om + "/bin/omarchy-plugins list --json 2>/dev/null || omarchy plugins list --json 2>/dev/null || echo '[]'"]
79
+ installedProc.running = true
80
+ }
81
+
82
+ function loadMarketplace() {
83
+ var home = Quickshell.env("HOME")
84
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
85
+ marketProc.command = ["bash", "-c", "cat " + home + "/.cache/omarchy/marketplace_catalog.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || " + om + "/bin/omarchy-plugins marketplace --json 2>/dev/null || cat " + om + "/registry.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || echo '[]'"]
86
+ marketProc.running = true
87
+ }
88
+
89
+ function loadShortcuts() {
90
+ shortcutsProc.command = ["bash", "-c", "omarchy plugins shortcut list --json 2>/dev/null || echo '[]'"]
91
+ shortcutsProc.running = true
92
+ }
93
+
94
+ function loadBarLayout() {
95
+ barProc.command = ["bash", "-c", "omarchy plugins bar list 2>/dev/null || echo '{\"inBar\":[]}'"]
96
+ barProc.running = true
26
97
}
27
98
28
99
Process {
29
- id: registryProc
100
+ id: installedProc
30
101
running: false
31
- stdout: SplitParser {
32
- onRead: function(line) {
102
+ stdout: StdioCollector {
103
+ waitForEnd: true
104
+ onStreamFinished: {
33
105
try {
34
- var data = JSON.parse(String(line).trim())
35
- if (data && data.plugins) root.plugins = data.plugins
36
- } catch(e) {}
106
+ var arr = JSON.parse(String(text).trim())
107
+ if (Array.isArray(arr)) {
108
+ root.installedPlugins = arr
109
+ root.applyInstalledFilter()
110
+ }
111
+ } catch (e) {
112
+ console.warn("Failed to parse installed plugins: " + e)
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ Process {
119
+ id: marketProc
120
+ running: false
121
+ stdout: StdioCollector {
122
+ waitForEnd: true
123
+ onStreamFinished: {
124
+ try {
125
+ var data = JSON.parse(String(text).trim())
126
+ var arr = Array.isArray(data) ? data : (data && data.plugins ? data.plugins : [])
127
+ if (Array.isArray(arr)) {
128
+ root.marketplacePlugins = arr
129
+ root.applyMarketFilter()
130
+ }
131
+ } catch (e) {
132
+ console.warn("Failed to parse marketplace plugins: " + e)
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ Process {
139
+ id: shortcutsProc
140
+ running: false
141
+ stdout: StdioCollector {
142
+ waitForEnd: true
143
+ onStreamFinished: {
144
+ try {
145
+ var arr = JSON.parse(String(text).trim())
146
+ var map = {}
147
+ if (Array.isArray(arr)) {
148
+ for (var i = 0; i < arr.length; i++) {
149
+ if (arr[i] && arr[i].id) {
150
+ map[arr[i].id] = arr[i].keys || ""
151
+ }
152
+ }
153
+ }
154
+ root.shortcutsMap = map
155
+ } catch (e) {
156
+ console.warn("Failed to parse shortcuts: " + e)
157
+ }
37
158
}
38
159
}
39
160
}
40
161
41
162
Process {
42
- id: installProc
163
+ id: barProc
43
164
running: false
44
- onRunningChanged: {
45
- if (!running) {
46
- root.installing = false
47
- root.statusText = root.installExitCode === 0 ? "Installed!" : "Install failed"
48
- timer.restart()
165
+ stdout: StdioCollector {
166
+ waitForEnd: true
167
+ onStreamFinished: {
168
+ try {
169
+ var data = JSON.parse(String(text).trim())
170
+ if (data && data.inBar) {
171
+ root.barLayoutData = data
172
+ root.applyInstalledFilter()
173
+ }
174
+ } catch (e) {
175
+ console.warn("Failed to parse bar layout: " + e)
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // ---------------------------------------------------------------- Filtering
182
+ function isPluginInBar(id) {
183
+ if (!id || !root.barLayoutData || !root.barLayoutData.inBar) return false
184
+ return root.barLayoutData.inBar.indexOf(id) !== -1
185
+ }
186
+
187
+ function applyInstalledFilter() {
188
+ var list = root.installedPlugins || []
189
+ var q = root.installedSearch.toLowerCase().trim()
190
+ var filter = root.installedFilter
191
+
192
+ var res = []
193
+ for (var i = 0; i < list.length; i++) {
194
+ var p = list[i]
195
+ if (filter === "active" && !p.enabled) continue
196
+ if (filter === "inactive" && p.enabled) continue
197
+ if (filter === "bar" && !isPluginInBar(p.id)) continue
198
+ if (filter === "builtin" && !p.firstParty) continue
199
+ if (filter === "user" && p.firstParty) continue
200
+
201
+ if (q.length > 0) {
202
+ var name = (p.name || "").toLowerCase()
203
+ var id = (p.id || "").toLowerCase()
204
+ var kinds = (p.kinds || []).join(" ").toLowerCase()
205
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && kinds.indexOf(q) === -1) continue
206
+ }
207
+ res.push(p)
208
+ }
209
+ root.filteredInstalled = res
210
+ }
211
+
212
+ function applyMarketFilter() {
213
+ var list = root.marketplacePlugins || []
214
+ var q = root.marketSearch.toLowerCase().trim()
215
+ var cat = root.marketCategory
216
+
217
+ var res = []
218
+ for (var i = 0; i < list.length; i++) {
219
+ var p = list[i]
220
+ if (cat !== "All" && (p.category || "").toLowerCase() !== cat.toLowerCase()) continue
221
+
222
+ if (q.length > 0) {
223
+ var name = (p.name || "").toLowerCase()
224
+ var id = (p.id || "").toLowerCase()
225
+ var desc = (p.description || "").toLowerCase()
226
+ var author = (p.author || "").toLowerCase()
227
+ var tags = (p.tags || []).join(" ").toLowerCase()
228
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && desc.indexOf(q) === -1 && author.indexOf(q) === -1 && tags.indexOf(q) === -1) continue
229
+ }
230
+ res.push(p)
231
+ }
232
+
233
+ if (root.marketSort === "stars") {
234
+ res.sort(function(a, b) { return (b.stars || 0) - (a.stars || 0) })
235
+ } else if (root.marketSort === "name") {
236
+ res.sort(function(a, b) { return (a.name || a.id).localeCompare(b.name || b.id) })
237
+ } else if (root.marketSort === "date") {
238
+ res.sort(function(a, b) { return String(b.addedAt || "").localeCompare(String(a.addedAt || "")) })
239
+ }
240
+
241
+ root.filteredMarketplace = res
242
+ }
243
+
244
+ onInstalledSearchChanged: applyInstalledFilter()
245
+ onInstalledFilterChanged: applyInstalledFilter()
246
+ onMarketSearchChanged: applyMarketFilter()
247
+ onMarketCategoryChanged: applyMarketFilter()
248
+ onMarketSortChanged: applyMarketFilter()
249
+
250
+ // ---------------------------------------------------------------- Helpers
251
+ function isInstalled(id) {
252
+ if (!id) return false
253
+ var cleanId = id.replace(/^[a-z0-9_-]+\./, "")
254
+ for (var i = 0; i < root.installedPlugins.length; i++) {
255
+ var instId = root.installedPlugins[i].id
256
+ if (instId === id || instId === cleanId || instId.endsWith("." + id) || id.endsWith("." + instId)) {
257
+ return true
49
258
}
50
259
}
51
- property int installExitCode: 0
52
- onExited: function(code) { installExitCode = code }
260
+ return false
261
+ }
262
+
263
+ function showToast(msg, type) {
264
+ root.statusMessage = msg
265
+ root.statusType = type || "info"
266
+ toastTimer.restart()
53
267
}
54
268
55
269
Timer {
56
- id: timer
57
- interval: 3000
58
- onTriggered: root.statusText = ""
270
+ id: toastTimer
271
+ interval: 3500
272
+ onTriggered: root.statusMessage = ""
273
+ }
274
+
275
+ function togglePlugin(plugin) {
276
+ if (!plugin || !plugin.id) return
277
+ var targetState = !plugin.enabled
278
+ plugin.enabled = targetState
279
+ applyInstalledFilter()
280
+
281
+ if (root.registry && typeof root.registry.setEnabled === "function") {
282
+ root.registry.setEnabled(plugin.id, targetState)
283
+ }
284
+
285
+ actionProc.command = ["bash", "-c", "omarchy plugins " + (targetState ? "enable" : "disable") + " '" + plugin.id + "'"]
286
+ actionProc.targetAction = "toggle"
287
+ actionProc.targetName = plugin.name || plugin.id
288
+ actionProc.targetState = targetState
289
+ actionProc.running = true
290
+ }
291
+
292
+ function toggleBarPlacement(plugin) {
293
+ if (!plugin || !plugin.id) return
294
+ if (isPluginInBar(plugin.id)) {
295
+ actionProc.command = ["bash", "-c", "omarchy plugins bar remove '" + plugin.id + "'"]
296
+ actionProc.targetAction = "bar_remove"
297
+ actionProc.targetName = plugin.name || plugin.id
298
+ actionProc.running = true
299
+ } else {
300
+ root.barTargetPlugin = plugin
301
+ root.barSectionDialogOpen = true
302
+ }
59
303
}
60
304
61
- function installPlugin(plugin) {
62
- if (root.installing) return
63
- root.installing = true
64
- root.statusText = "Installing " + plugin.name + "..."
65
- var pluginDir = Quickshell.env("HOME") + "/.config/omarchy/plugins"
66
- installProc.command = ["bash", "-c", "mkdir -p " + pluginDir + " && " + plugin.installCommand]
67
- installProc.running = true
305
+ function addPluginToBar(plugin, section) {
306
+ root.barSectionDialogOpen = false
307
+ root.barTargetPlugin = null
308
+ if (!plugin || !plugin.id) return
309
+
310
+ showToast("Adding " + (plugin.name || plugin.id) + " to bar (" + section + ")...", "info")
311
+ actionProc.command = ["bash", "-c", "omarchy plugins bar add '" + plugin.id + "' " + section]
312
+ actionProc.targetAction = "bar_add"
313
+ actionProc.targetName = plugin.name || plugin.id
314
+ actionProc.running = true
315
+ }
316
+
317
+ function openShortcutDialog(plugin) {
318
+ root.shortcutTargetPlugin = plugin
319
+ root.shortcutInput = root.shortcutsMap[plugin.id] || ""
320
+ root.shortcutDialogOpen = true
321
+ }
322
+
323
+ function saveShortcut() {
324
+ var p = root.shortcutTargetPlugin
325
+ var keys = root.shortcutInput.trim()
326
+ root.shortcutDialogOpen = false
327
+ root.shortcutTargetPlugin = null
328
+ if (!p) return
329
+
330
+ if (keys === "") {
331
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut remove '" + p.id + "'"]
332
+ actionProc.targetAction = "shortcut_remove"
333
+ actionProc.targetName = p.name || p.id
334
+ actionProc.running = true
335
+ } else {
336
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut set '" + p.id + "' '" + keys + "'"]
337
+ actionProc.targetAction = "shortcut_set"
338
+ actionProc.targetName = p.name || p.id
339
+ actionProc.running = true
340
+ }
341
+ }
342
+
343
+ function clearShortcut() {
344
+ root.shortcutInput = ""
345
+ saveShortcut()
346
+ }
347
+
348
+ function installPlugin(p) {
349
+ if (!p || root.installingId !== "") return
350
+ root.installingId = p.id
351
+ showToast("Installing " + (p.name || p.id) + "...", "info")
352
+
353
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + p.id + "' --enable"]
354
+ actionProc.targetAction = "install"
355
+ actionProc.targetName = p.name || p.id
356
+ actionProc.running = true
357
+ }
358
+
359
+ function installFromUrl(url) {
360
+ if (!url || url.trim() === "") return
361
+ url = url.trim()
362
+ root.urlDialogOpen = false
363
+ root.urlInput = ""
364
+ showToast("Installing: " + url + "...", "info")
365
+
366
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + url + "' --enable"]
367
+ actionProc.targetAction = "install"
368
+ actionProc.targetName = url
369
+ actionProc.running = true
370
+ }
371
+
372
+ function confirmUninstall(p) {
373
+ root.pendingUninstallPlugin = p
374
+ root.confirmUninstallOpen = true
375
+ }
376
+
377
+ function executeUninstall() {
378
+ var p = root.pendingUninstallPlugin
379
+ root.confirmUninstallOpen = false
380
+ root.pendingUninstallPlugin = null
381
+ if (!p) return
382
+
383
+ showToast("Removing " + (p.name || p.id) + "...", "info")
384
+ actionProc.command = ["bash", "-c", "omarchy plugins remove '" + p.id + "'"]
385
+ actionProc.targetAction = "remove"
386
+ actionProc.targetName = p.name || p.id
387
+ actionProc.running = true
388
+ }
389
+
390
+ Process {
391
+ id: actionProc
392
+ running: false
393
+ property string targetAction: ""
394
+ property string targetName: ""
395
+ property bool targetState: false
396
+
397
+ onExited: function(code) {
398
+ root.installingId = ""
399
+ root.updatingAll = false
400
+ if (code === 0) {
401
+ if (targetAction === "install") {
402
+ root.showToast("✓ Installed " + targetName + "!", "success")
403
+ } else if (targetAction === "remove") {
404
+ root.showToast("✓ Removed " + targetName + ".", "success")
405
+ } else if (targetAction === "toggle") {
406
+ root.showToast("✓ " + (targetState ? "Enabled" : "Disabled") + " " + targetName, "success")
407
+ } else if (targetAction === "bar_add") {
408
+ root.showToast("✓ Added " + targetName + " to top bar!", "success")
409
+ } else if (targetAction === "bar_remove") {
410
+ root.showToast("✓ Removed " + targetName + " from bar.", "success")
411
+ } else if (targetAction === "shortcut_set") {
412
+ root.showToast("✓ Shortcut saved for " + targetName + "!", "success")
413
+ } else if (targetAction === "shortcut_remove") {
414
+ root.showToast("✓ Shortcut cleared for " + targetName, "success")
415
+ }
416
+ } else {
417
+ root.showToast("✕ Action failed for " + targetName, "error")
418
+ }
419
+ root.loadInstalled()
420
+ root.loadShortcuts()
421
+ root.loadBarLayout()
422
+ }
68
423
}
69
424
70
- Column {
425
+ function getInitials(name) {
426
+ if (!name) return "?"
427
+ var parts = name.split(/[\s.-]+/)
428
+ if (parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0) {
429
+ return (parts[0][0] + parts[1][0]).toUpperCase()
430
+ }
431
+ return name.substring(0, Math.min(2, name.length)).toUpperCase()
432
+ }
433
+
434
+ // ---------------------------------------------------------------- Content Layout
435
+ ColumnLayout {
71
436
anchors.fill: parent
72
- anchors.margins: 16
437
+ anchors.margins: 14
73
438
spacing: 12
74
439
75
- Row {
76
- width: parent.width
77
- spacing: 12
440
+ // Header
441
+ RowLayout {
442
+ Layout.fillWidth: true
443
+ spacing: 10
78
444
79
445
Text {
80
- text: "Plugin Library"
446
+ text: "🧩 Plugin Manager"
81
447
color: Color.foreground
82
448
font.family: root.bar ? root.bar.fontFamily : Style.font.family
83
- font.pixelSize: 20
449
+ font.pixelSize: 18
84
450
font.bold: true
85
- anchors.verticalCenter: parent.verticalCenter
86
451
}
87
452
88
- Item { width: 1; height: 1 }
453
+ Item { Layout.fillWidth: true }
454
+
455
+ // Tab switcher
456
+ Rectangle {
457
+ Layout.preferredWidth: 260
458
+ Layout.preferredHeight: 32
459
+ radius: 16
460
+ color: Qt.rgba(0, 0, 0, 0.25)
461
+ border.color: Qt.rgba(255, 255, 255, 0.08)
462
+ border.width: 1
463
+
464
+ RowLayout {
465
+ anchors.fill: parent
466
+ spacing: 0
467
+
468
+ Rectangle {
469
+ Layout.fillWidth: true
470
+ Layout.fillHeight: true
471
+ radius: 14
472
+ color: root.currentTab === "installed" ? Color.accent : "transparent"
473
+
474
+ Text {
475
+ anchors.centerIn: parent
476
+ text: "📦 Installed (" + root.installedPlugins.length + ")"
477
+ color: root.currentTab === "installed" ? Color.background : Color.foreground
478
+ font.pixelSize: 11
479
+ font.bold: root.currentTab === "installed"
480
+ }
481
+
482
+ MouseArea {
483
+ anchors.fill: parent
484
+ cursorShape: Qt.PointingHandCursor
485
+ onClicked: root.currentTab = "installed"
486
+ }
487
+ }
488
+
489
+ Rectangle {
490
+ Layout.fillWidth: true
491
+ Layout.fillHeight: true
492
+ radius: 14
493
+ color: root.currentTab === "marketplace" ? Color.accent : "transparent"
494
+
495
+ Text {
496
+ anchors.centerIn: parent
497
+ text: "🌐 Marketplace"
498
+ color: root.currentTab === "marketplace" ? Color.background : Color.foreground
499
+ font.pixelSize: 11
500
+ font.bold: root.currentTab === "marketplace"
501
+ }
89
502
503
+ MouseArea {
504
+ anchors.fill: parent
505
+ cursorShape: Qt.PointingHandCursor
506
+ onClicked: {
507
+ root.currentTab = "marketplace"
508
+ if (root.marketplacePlugins.length === 0) root.loadMarketplace()
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ // Add Git URL Button
90
516
Rectangle {
91
- width: statusText.length > 0 ? statusLabel.implicitWidth + 20 : 0
92
- height: 24
93
- radius: 12
94
- color: installing ? Qt.rgba(0.2, 0.5, 0.9, 0.3) : Qt.rgba(0.2, 0.8, 0.3, 0.3)
95
- visible: statusText.length > 0
96
- anchors.verticalCenter: parent.verticalCenter
517
+ Layout.preferredHeight: 30
518
+ Layout.preferredWidth: 32
519
+ radius: 6
520
+ color: Qt.rgba(255, 255, 255, 0.08)
521
+ border.color: Qt.rgba(255, 255, 255, 0.12)
522
+ border.width: 1
97
523
98
524
Text {
99
- id: statusLabel
100
525
anchors.centerIn: parent
101
- text: root.statusText
526
+ text: "+"
102
527
color: Color.foreground
103
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
104
- font.pixelSize: 11
528
+ font.pixelSize: 16
529
+ font.bold: true
530
+ }
531
+
532
+ MouseArea {
533
+ anchors.fill: parent
534
+ cursorShape: Qt.PointingHandCursor
535
+ onClicked: root.urlDialogOpen = true
105
536
}
106
537
}
107
538
}
108
539
109
- Text {
110
- text: plugins.length + " plugins available"
111
- color: Color.subtext
112
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
113
- font.pixelSize: 12
540
+ // Status Banner Toast
541
+ Rectangle {
542
+ Layout.fillWidth: true
543
+ Layout.preferredHeight: root.statusMessage !== "" ? 30 : 0
544
+ visible: root.statusMessage !== ""
545
+ radius: 6
546
+ color: root.statusType === "success" ? Qt.rgba(0.2, 0.8, 0.3, 0.2) : Qt.rgba(0.3, 0.6, 0.9, 0.2)
547
+ border.color: root.statusType === "success" ? "#a6e3a1" : "#89b4fa"
548
+ border.width: 1
549
+ clip: true
550
+
551
+ Text {
552
+ anchors.centerIn: parent
553
+ text: root.statusMessage
554
+ color: Color.foreground
555
+ font.pixelSize: 11
556
+ font.bold: true
557
+ }
114
558
}
115
559
116
- ListView {
117
- id: listView
118
- width: parent.width
119
- height: parent.height - 80
120
- clip: true
121
- spacing: 8
122
- model: root.plugins
123
-
124
- delegate: Rectangle {
125
- required property var modelData
126
- required property int index
127
- width: listView.width
128
- height: 90
129
- radius: 8
130
- color: index === root.selected ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.15) : Qt.rgba(1, 1, 1, 0.05)
131
- border.color: index === root.selected ? Color.accent : Qt.rgba(1, 1, 1, 0.1)
132
- border.width: index === root.selected ? 1 : 0
133
-
134
- Column {
560
+ // Stack Views
561
+ StackLayout {
562
+ Layout.fillWidth: true
563
+ Layout.fillHeight: true
564
+ currentIndex: root.currentTab === "installed" ? 0 : 1
565
+
566
+ // TAB 1: INSTALLED
567
+ Item {
568
+ ColumnLayout {
135
569
anchors.fill: parent
136
- anchors.margins: 10
137
- spacing: 4
138
-
139
- Row {
140
- width: parent.width
141
- spacing: 8
142
-
143
- Rectangle {
144
- width: 36; height: 36; radius: 8
145
- color: Qt.rgba(0.4, 0.5, 0.9, 0.2)
146
- Text {
147
- anchors.centerIn: parent
148
- text: modelData.name ? modelData.name.charAt(0) : "?"
570
+ spacing: 8
571
+
572
+ // Search Bar
573
+ Rectangle {
574
+ Layout.fillWidth: true
575
+ Layout.preferredHeight: 32
576
+ radius: 6
577
+ color: Qt.rgba(0, 0, 0, 0.2)
578
+ border.color: Qt.rgba(255, 255, 255, 0.1)
579
+ border.width: 1
580
+
581
+ RowLayout {
582
+ anchors.fill: parent
583
+ anchors.margins: 6
584
+ spacing: 6
585
+ Text { text: "🔍"; font.pixelSize: 10; color: Color.subtext }
586
+ TextInput {
587
+ id: installedSearchField
588
+ Layout.fillWidth: true
589
+ text: root.installedSearch
149
590
color: Color.foreground
150
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
151
- font.pixelSize: 16
152
- font.bold: true
591
+ font.pixelSize: 11
592
+ onTextChanged: root.installedSearch = text
593
+ Text {
594
+ text: "Filter installed..."
595
+ color: Color.subtext
596
+ font.pixelSize: 11
597
+ visible: !installedSearchField.text
598
+ }
153
599
}
154
600
}
601
+ }
155
602
156
- Column {
157
- width: parent.width - 120
158
- spacing: 2
603
+ // Filter Chips
604
+ RowLayout {
605
+ spacing: 6
606
+ Repeater {
607
+ model: [
608
+ { id: "all", label: "All" },
609
+ { id: "active", label: "Active" },
610
+ { id: "inactive", label: "Inactive" },
611
+ { id: "bar", label: "📌 In Bar" },
612
+ { id: "user", label: "User" }
613
+ ]
614
+ delegate: Rectangle {
615
+ required property var modelData
616
+ Layout.preferredHeight: 24
617
+ Layout.preferredWidth: chipText.implicitWidth + 14
618
+ radius: 12
619
+ color: root.installedFilter === modelData.id ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.25) : Qt.rgba(255, 255, 255, 0.05)
620
+ border.color: root.installedFilter === modelData.id ? Color.accent : "transparent"
621
+ border.width: 1
159
622
160
- Text {
161
- text: modelData.name || "Unknown"
162
- color: Color.foreground
163
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
164
- font.pixelSize: 13
165
- font.bold: true
166
- elide: Text.ElideRight
623
+ Text {
624
+ id: chipText
625
+ anchors.centerIn: parent
626
+ text: modelData.label
627
+ color: Color.foreground
628
+ font.pixelSize: 10
629
+ }
630
+
631
+ MouseArea {
632
+ anchors.fill: parent
633
+ cursorShape: Qt.PointingHandCursor
634
+ onClicked: root.installedFilter = modelData.id
635
+ }
167
636
}
637
+ }
638
+ }
168
639
169
- Text {
170
- text: modelData.description || ""
171
- color: Color.subtext
172
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
173
- font.pixelSize: 11
174
- elide: Text.ElideRight
175
- width: parent.width
640
+ // List
641
+ ListView {
642
+ Layout.fillWidth: true
643
+ Layout.fillHeight: true
644
+ clip: true
645
+ spacing: 6
646
+ model: root.filteredInstalled
647
+
648
+ delegate: Rectangle {
649
+ required property var modelData
650
+ property bool inBar: root.isPluginInBar(modelData.id)
651
+ property string shortcut: root.shortcutsMap[modelData.id] || ""
652
+ property bool isBarCompatible: {
653
+ var kinds = modelData.kinds || []
654
+ return kinds.indexOf("bar-widget") !== -1 || kinds.indexOf("bar") !== -1 || kinds.indexOf("panel") !== -1
176
655
}
177
656
178
- Row {
179
- spacing: 6
180
- Text {
181
- text: modelData.author || ""
182
- color: Color.subtext
183
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
184
- font.pixelSize: 10
657
+ width: parent.width
658
+ height: 66
659
+ radius: 8
660
+ color: Qt.rgba(255, 255, 255, modelData.enabled ? 0.08 : 0.03)
661
+ border.color: Qt.rgba(255, 255, 255, 0.08)
662
+ border.width: 1
663
+
664
+ RowLayout {
665
+ anchors.fill: parent
666
+ anchors.margins: 8
667
+ spacing: 8
668
+
669
+ Rectangle {
670
+ Layout.preferredWidth: 36
671
+ Layout.preferredHeight: 36
672
+ radius: 8
673
+ color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.2)
674
+ Text {
675
+ anchors.centerIn: parent
676
+ text: root.getInitials(modelData.name || modelData.id)
677
+ color: Color.foreground
678
+ font.pixelSize: 12
679
+ font.bold: true
680
+ }
681
+ }
682
+
683
+ ColumnLayout {
684
+ Layout.fillWidth: true
685
+ spacing: 2
686
+ Text {
687
+ text: modelData.name || modelData.id
688
+ color: Color.foreground
689
+ font.pixelSize: 12
690
+ font.bold: true
691
+ }
692
+ Text {
693
+ text: modelData.id + (modelData.firstParty ? " (Built-in)" : " (User)")
694
+ color: Color.subtext
695
+ font.pixelSize: 10
696
+ }
697
+ }
698
+
699
+ // Bar placement toggle
700
+ Rectangle {
701
+ visible: isBarCompatible
702
+ Layout.preferredHeight: 26
703
+ Layout.preferredWidth: pBarText.implicitWidth + 12
704
+ radius: 4
705
+ color: inBar ? Qt.rgba(0.2, 0.8, 0.3, 0.18) : Qt.rgba(255, 255, 255, 0.06)
706
+ border.color: inBar ? "#a6e3a1" : Qt.rgba(255, 255, 255, 0.1)
707
+ border.width: 1
708
+
709
+ Text {
710
+ id: pBarText
711
+ anchors.centerIn: parent
712
+ text: inBar ? "📌 In Bar" : "➕ Bar"
713
+ color: inBar ? "#a6e3a1" : Color.subtext
714
+ font.pixelSize: 9
715
+ font.bold: inBar
716
+ }
717
+
718
+ MouseArea {
719
+ anchors.fill: parent
720
+ cursorShape: Qt.PointingHandCursor
721
+ onClicked: root.toggleBarPlacement(modelData)
722
+ }
723
+ }
724
+
725
+ // Shortcut button
726
+ Rectangle {
727
+ Layout.preferredHeight: 26
728
+ Layout.preferredWidth: pScText.implicitWidth + 12
729
+ radius: 4
730
+ color: shortcut !== "" ? Qt.rgba(1, 0.7, 0.2, 0.18) : Qt.rgba(255, 255, 255, 0.06)
731
+ border.color: shortcut !== "" ? "#ff9e64" : Qt.rgba(255, 255, 255, 0.1)
732
+ border.width: 1
733
+
734
+ Text {
735
+ id: pScText
736
+ anchors.centerIn: parent
737
+ text: shortcut !== "" ? "⌨ " + shortcut : "⌨ Key"
738
+ color: shortcut !== "" ? "#ff9e64" : Color.subtext
739
+ font.pixelSize: 9
740
+ font.bold: shortcut !== ""
741
+ }
742
+
743
+ MouseArea {
744
+ anchors.fill: parent
745
+ cursorShape: Qt.PointingHandCursor
746
+ onClicked: root.openShortcutDialog(modelData)
747
+ }
748
+ }
749
+
750
+ // Delete button for user plugins
751
+ Rectangle {
752
+ visible: !modelData.firstParty
753
+ Layout.preferredWidth: 26
754
+ Layout.preferredHeight: 26
755
+ radius: 4
756
+ color: Qt.rgba(1, 0, 0, 0.15)
757
+ Text { anchors.centerIn: parent; text: "🗑"; font.pixelSize: 10 }
758
+ MouseArea {
759
+ anchors.fill: parent
760
+ cursorShape: Qt.PointingHandCursor
761
+ onClicked: root.confirmUninstall(modelData)
762
+ }
763
+ }
764
+
765
+ // Switch
766
+ Rectangle {
767
+ Layout.preferredWidth: 44
768
+ Layout.preferredHeight: 24
769
+ radius: 12
770
+ color: modelData.enabled ? Color.accent : Qt.rgba(255, 255, 255, 0.15)
771
+ border.color: Qt.rgba(255, 255, 255, 0.1)
772
+ border.width: 1
773
+
774
+ Rectangle {
775
+ width: 18
776
+ height: 18
777
+ radius: 9
778
+ color: "#ffffff"
779
+ anchors.verticalCenter: parent.verticalCenter
780
+ x: modelData.enabled ? 22 : 4
781
+ Behavior on x { NumberAnimation { duration: 120 } }
782
+ }
783
+
784
+ MouseArea {
785
+ anchors.fill: parent
786
+ cursorShape: Qt.PointingHandCursor
787
+ onClicked: root.togglePlugin(modelData)
788
+ }
185
789
}
790
+ }
791
+ }
792
+ }
793
+ }
794
+ }
795
+
796
+ // TAB 2: MARKETPLACE
797
+ Item {
798
+ ColumnLayout {
799
+ anchors.fill: parent
800
+ spacing: 8
801
+
802
+ // Search Bar
803
+ Rectangle {
804
+ Layout.fillWidth: true
805
+ Layout.preferredHeight: 32
806
+ radius: 6
807
+ color: Qt.rgba(0, 0, 0, 0.2)
808
+ border.color: Qt.rgba(255, 255, 255, 0.1)
809
+ border.width: 1
810
+
811
+ RowLayout {
812
+ anchors.fill: parent
813
+ anchors.margins: 6
814
+ spacing: 6
815
+ Text { text: "🌐"; font.pixelSize: 10; color: Color.subtext }
816
+ TextInput {
817
+ id: marketSearchField
818
+ Layout.fillWidth: true
819
+ text: root.marketSearch
820
+ color: Color.foreground
821
+ font.pixelSize: 11
822
+ onTextChanged: root.marketSearch = text
186
823
Text {
187
- text: (modelData.tags || []).join(", ")
824
+ text: "Search 1,000+ plugins..."
188
825
color: Color.subtext
189
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
190
- font.pixelSize: 10
826
+ font.pixelSize: 11
827
+ visible: !marketSearchField.text
191
828
}
192
829
}
193
830
}
831
+ }
194
832
195
- Rectangle {
196
- width: 60; height: 28; radius: 6
197
- color: root.installing ? Qt.rgba(1,1,1,0.05) : Qt.rgba(0.3, 0.7, 0.3, 0.3)
198
- anchors.verticalCenter: parent.verticalCenter
833
+ // Category Chips
834
+ ScrollView {
835
+ Layout.fillWidth: true
836
+ Layout.preferredHeight: 28
837
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
838
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
199
839
200
- Text {
201
- anchors.centerIn: parent
202
- text: root.installing ? "..." : "Install"
203
- color: root.installing ? Color.subtext : Color.foreground
204
- font.family: root.bar ? root.bar.fontFamily : Style.font.family
205
- font.pixelSize: 11
206
- font.bold: true
840
+ RowLayout {
841
+ spacing: 6
842
+ Repeater {
843
+ model: ["All", "Appearance", "Desktop", "Productivity", "System", "Utilities", "Widgets"]
844
+ delegate: Rectangle {
845
+ required property var modelData
846
+ Layout.preferredHeight: 24
847
+ Layout.preferredWidth: mCatText.implicitWidth + 14
848
+ radius: 12
849
+ color: root.marketCategory === modelData ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.25) : Qt.rgba(255, 255, 255, 0.05)
850
+ border.color: root.marketCategory === modelData ? Color.accent : "transparent"
851
+ border.width: 1
852
+
853
+ Text {
854
+ id: mCatText
855
+ anchors.centerIn: parent
856
+ text: modelData
857
+ color: Color.foreground
858
+ font.pixelSize: 10
859
+ }
860
+
861
+ MouseArea {
862
+ anchors.fill: parent
863
+ cursorShape: Qt.PointingHandCursor
864
+ onClicked: root.marketCategory = modelData
865
+ }
866
+ }
207
867
}
868
+ }
869
+ }
870
+
871
+ // List
872
+ ListView {
873
+ Layout.fillWidth: true
874
+ Layout.fillHeight: true
875
+ clip: true
876
+ spacing: 6
877
+ model: root.filteredMarketplace
208
878
209
- MouseArea {
879
+ delegate: Rectangle {
880
+ required property var modelData
881
+ property bool installed: root.isInstalled(modelData.id)
882
+ width: parent.width
883
+ height: 74
884
+ radius: 8
885
+ color: Qt.rgba(255, 255, 255, 0.05)
886
+ border.color: Qt.rgba(255, 255, 255, 0.08)
887
+ border.width: 1
888
+
889
+ RowLayout {
210
890
anchors.fill: parent
211
- cursorShape: Qt.PointingHandCursor
212
- enabled: !root.installing
213
- onClicked: root.installPlugin(modelData)
891
+ anchors.margins: 10
892
+ spacing: 10
893
+
894
+ Rectangle {
895
+ Layout.preferredWidth: 38
896
+ Layout.preferredHeight: 38
897
+ radius: 8
898
+ color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.2)
899
+ Text {
900
+ anchors.centerIn: parent
901
+ text: modelData.initials || root.getInitials(modelData.name || modelData.id)
902
+ color: Color.foreground
903
+ font.pixelSize: 13
904
+ font.bold: true
905
+ }
906
+ }
907
+
908
+ ColumnLayout {
909
+ Layout.fillWidth: true
910
+ spacing: 2
911
+ RowLayout {
912
+ spacing: 6
913
+ Text {
914
+ text: modelData.name || modelData.id
915
+ color: Color.foreground
916
+ font.pixelSize: 12
917
+ font.bold: true
918
+ }
919
+ Text {
920
+ visible: (modelData.stars || 0) > 0
921
+ text: "★ " + modelData.stars
922
+ color: "#e0af68"
923
+ font.pixelSize: 10
924
+ }
925
+ }
926
+ Text {
927
+ Layout.fillWidth: true
928
+ text: modelData.description || ""
929
+ color: Color.subtext
930
+ font.pixelSize: 10
931
+ elide: Text.ElideRight
932
+ }
933
+ }
934
+
935
+ // Install button
936
+ Rectangle {
937
+ Layout.preferredHeight: 28
938
+ Layout.preferredWidth: installed ? 78 : 66
939
+ radius: 6
940
+ color: installed ? Qt.rgba(0.2, 0.8, 0.3, 0.18) : (root.installingId === modelData.id ? Qt.rgba(255, 255, 255, 0.08) : Color.accent)
941
+ border.color: installed ? "#a6e3a1" : "transparent"
942
+ border.width: installed ? 1 : 0
943
+
944
+ Text {
945
+ anchors.centerIn: parent
946
+ text: installed ? "✓ Installed" : (root.installingId === modelData.id ? "..." : "Install")
947
+ color: installed ? "#a6e3a1" : (root.installingId === modelData.id ? Color.subtext : Color.background)
948
+ font.pixelSize: 10
949
+ font.bold: true
950
+ }
951
+
952
+ MouseArea {
953
+ anchors.fill: parent
954
+ cursorShape: Qt.PointingHandCursor
955
+ enabled: !installed && root.installingId === ""
956
+ onClicked: root.installPlugin(modelData)
957
+ }
958
+ }
214
959
}
215
960
}
216
961
}
217
962
}
963
+ }
964
+ }
965
+ }
966
+
967
+ // Modals
968
+ // 1. Shortcut Dialog
969
+ Rectangle {
970
+ anchors.fill: parent
971
+ color: Qt.rgba(0, 0, 0, 0.75)
972
+ visible: root.shortcutDialogOpen
973
+
974
+ MouseArea { anchors.fill: parent; onClicked: root.shortcutDialogOpen = false }
975
+
976
+ Rectangle {
977
+ anchors.centerIn: parent
978
+ width: 440
979
+ height: 240
980
+ radius: 10
981
+ color: Color.popups ? Color.popups.background : "#1f2335"
982
+ border.color: "#ff9e64"
983
+ border.width: 1
984
+
985
+ MouseArea { anchors.fill: parent }
986
+
987
+ ColumnLayout {
988
+ anchors.fill: parent
989
+ anchors.margins: 16
990
+ spacing: 10
991
+
992
+ Text {
993
+ text: "⌨ Set Keyboard Shortcut"
994
+ color: Color.foreground
995
+ font.pixelSize: 14
996
+ font.bold: true
997
+ }
218
998
219
- MouseArea {
220
- anchors.fill: parent
221
- onClicked: root.selectedIndex = index
999
+ Text {
1000
+ text: "Set Hyprland keybind for '" + (root.shortcutTargetPlugin ? (root.shortcutTargetPlugin.name || root.shortcutTargetPlugin.id) : "") + "':"
1001
+ color: Color.subtext
1002
+ font.pixelSize: 11
1003
+ }
1004
+
1005
+ Rectangle {
1006
+ Layout.fillWidth: true
1007
+ Layout.preferredHeight: 34
1008
+ radius: 6
1009
+ color: Qt.rgba(0, 0, 0, 0.25)
1010
+ border.color: "#ff9e64"
1011
+ border.width: 1
1012
+
1013
+ TextInput {
1014
+ anchors.fill: parent
1015
+ anchors.margins: 6
1016
+ text: root.shortcutInput
1017
+ color: Color.foreground
1018
+ font.pixelSize: 12
1019
+ font.bold: true
1020
+ onTextChanged: root.shortcutInput = text
1021
+ Text {
1022
+ text: "e.g. SUPER + ALT + D"
1023
+ color: Color.subtext
1024
+ font.pixelSize: 11
1025
+ visible: !parent.text
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ RowLayout {
1031
+ Layout.fillWidth: true
1032
+ Button {
1033
+ text: "Clear"
1034
+ onClicked: root.clearShortcut()
1035
+ }
1036
+ Item { Layout.fillWidth: true }
1037
+ Button {
1038
+ text: "Cancel"
1039
+ onClicked: root.shortcutDialogOpen = false
1040
+ }
1041
+ Button {
1042
+ text: "Save & Apply"
1043
+ onClicked: root.saveShortcut()
1044
+ }
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ // 2. Bar Section Selector
1051
+ Rectangle {
1052
+ anchors.fill: parent
1053
+ color: Qt.rgba(0, 0, 0, 0.75)
1054
+ visible: root.barSectionDialogOpen
1055
+
1056
+ MouseArea { anchors.fill: parent; onClicked: root.barSectionDialogOpen = false }
1057
+
1058
+ Rectangle {
1059
+ anchors.centerIn: parent
1060
+ width: 380
1061
+ height: 180
1062
+ radius: 10
1063
+ color: Color.popups ? Color.popups.background : "#1f2335"
1064
+ border.color: "#a6e3a1"
1065
+ border.width: 1
1066
+
1067
+ MouseArea { anchors.fill: parent }
1068
+
1069
+ ColumnLayout {
1070
+ anchors.fill: parent
1071
+ anchors.margins: 16
1072
+ spacing: 12
1073
+
1074
+ Text {
1075
+ text: "📌 Add to Omarchy Top Bar"
1076
+ color: Color.foreground
1077
+ font.pixelSize: 14
1078
+ font.bold: true
1079
+ }
1080
+
1081
+ Text {
1082
+ text: "Select bar section:"
1083
+ color: Color.subtext
1084
+ font.pixelSize: 11
1085
+ }
1086
+
1087
+ RowLayout {
1088
+ Layout.fillWidth: true
1089
+ spacing: 8
1090
+
1091
+ Button {
1092
+ Layout.fillWidth: true
1093
+ text: "Left"
1094
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "left")
1095
+ }
1096
+ Button {
1097
+ Layout.fillWidth: true
1098
+ text: "Center"
1099
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "center")
1100
+ }
1101
+ Button {
1102
+ Layout.fillWidth: true
1103
+ text: "Right"
1104
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "right")
1105
+ }
1106
+ }
1107
+
1108
+ RowLayout {
1109
+ Layout.fillWidth: true
1110
+ Item { Layout.fillWidth: true }
1111
+ Button {
1112
+ text: "Cancel"
1113
+ onClicked: root.barSectionDialogOpen = false
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ // 3. Git URL Dialog Modal
1121
+ Rectangle {
1122
+ anchors.fill: parent
1123
+ color: Qt.rgba(0, 0, 0, 0.7)
1124
+ visible: root.urlDialogOpen
1125
+
1126
+ MouseArea { anchors.fill: parent; onClicked: root.urlDialogOpen = false }
1127
+
1128
+ Rectangle {
1129
+ anchors.centerIn: parent
1130
+ width: 420
1131
+ height: 180
1132
+ radius: 10
1133
+ color: Color.popups ? Color.popups.background : "#1f2335"
1134
+ border.color: Qt.rgba(255, 255, 255, 0.15)
1135
+ border.width: 1
1136
+
1137
+ MouseArea { anchors.fill: parent }
1138
+
1139
+ ColumnLayout {
1140
+ anchors.fill: parent
1141
+ anchors.margins: 16
1142
+ spacing: 10
1143
+
1144
+ Text {
1145
+ text: "Install from Git Repository"
1146
+ color: Color.foreground
1147
+ font.pixelSize: 14
1148
+ font.bold: true
1149
+ }
1150
+
1151
+ Rectangle {
1152
+ Layout.fillWidth: true
1153
+ Layout.preferredHeight: 34
1154
+ radius: 6
1155
+ color: Qt.rgba(0, 0, 0, 0.25)
1156
+ border.color: Color.accent
1157
+ border.width: 1
1158
+
1159
+ TextInput {
1160
+ anchors.fill: parent
1161
+ anchors.margins: 6
1162
+ text: root.urlInput
1163
+ color: Color.foreground
1164
+ font.pixelSize: 11
1165
+ onTextChanged: root.urlInput = text
1166
+ Text {
1167
+ text: "https://github.com/..."
1168
+ color: Color.subtext
1169
+ font.pixelSize: 11
1170
+ visible: !parent.text
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ RowLayout {
1176
+ Layout.fillWidth: true
1177
+ Item { Layout.fillWidth: true }
1178
+ Button {
1179
+ text: "Cancel"
1180
+ onClicked: root.urlDialogOpen = false
1181
+ }
1182
+ Button {
1183
+ text: "Install"
1184
+ onClicked: root.installFromUrl(root.urlInput)
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ // 4. Confirm Uninstall Dialog
1192
+ Rectangle {
1193
+ anchors.fill: parent
1194
+ color: Qt.rgba(0, 0, 0, 0.7)
1195
+ visible: root.confirmUninstallOpen
1196
+
1197
+ MouseArea { anchors.fill: parent; onClicked: root.confirmUninstallOpen = false }
1198
+
1199
+ Rectangle {
1200
+ anchors.centerIn: parent
1201
+ width: 380
1202
+ height: 150
1203
+ radius: 10
1204
+ color: Color.popups ? Color.popups.background : "#1f2335"
1205
+ border.color: Qt.rgba(255, 255, 255, 0.15)
1206
+ border.width: 1
1207
+
1208
+ MouseArea { anchors.fill: parent }
1209
+
1210
+ ColumnLayout {
1211
+ anchors.fill: parent
1212
+ anchors.margins: 16
1213
+ spacing: 10
1214
+
1215
+ Text {
1216
+ text: "Uninstall Plugin"
1217
+ color: "#f7768e"
1218
+ font.pixelSize: 14
1219
+ font.bold: true
1220
+ }
1221
+
1222
+ Text {
1223
+ text: "Remove '" + (root.pendingUninstallPlugin ? (root.pendingUninstallPlugin.name || root.pendingUninstallPlugin.id) : "") + "'?"
1224
+ color: Color.foreground
1225
+ font.pixelSize: 11
1226
+ }
1227
+
1228
+ RowLayout {
1229
+ Layout.fillWidth: true
1230
+ Item { Layout.fillWidth: true }
1231
+ Button {
1232
+ text: "Cancel"
1233
+ onClicked: root.confirmUninstallOpen = false
1234
+ }
1235
+ Button {
1236
+ text: "Remove"
1237
+ onClicked: root.executeUninstall()
1238
+ }
222
1239
}
223
1240
}
224
1241
}
225
1242
▼shell/plugins/plugin-manager/BarWidget.qml
+70-0
1
+import QtQuick
2
+import Quickshell
3
+import Quickshell.Io
4
+import qs.Commons
5
+import qs.Ui
6
+
7
+BarWidget {
8
+ id: root
9
+ moduleName: "deomarchy.plugin-manager"
10
+
11
+ readonly property bool opened: panelItem ? panelItem.opened === true : false
12
+
13
+ function open() { if (panelItem) panelItem.open() }
14
+ function close() { if (panelItem) panelItem.close() }
15
+ function togglePanel() { if (panelItem) panelItem.toggle() }
16
+
17
+ readonly property bool popoutSwitchClosing: panelItem ? panelItem.popoutSwitchClosing === true : false
18
+ function closeForPopoutSwitch() { if (panelItem) panelItem.closeForPopoutSwitch() }
19
+
20
+ property var panelItem: null
21
+
22
+ function injectPanel() {
23
+ var target = panelLoader.item
24
+ if (!target) return
25
+ panelItem = target
26
+ if ("bar" in target) target.bar = root.bar
27
+ if ("settings" in target) target.settings = root.settings
28
+ if ("anchorItem" in target) target.anchorItem = button
29
+ if ("hostWidget" in target) target.hostWidget = root
30
+ }
31
+
32
+ implicitWidth: button.implicitWidth
33
+ implicitHeight: button.implicitHeight
34
+
35
+ onBarChanged: injectPanel()
36
+ onSettingsChanged: injectPanel()
37
+
38
+ Loader {
39
+ id: panelLoader
40
+ active: true
41
+ source: Qt.resolvedUrl("Panel.qml")
42
+ visible: false
43
+ onLoaded: {
44
+ root.injectPanel()
45
+ Qt.callLater(root.injectPanel)
46
+ }
47
+ }
48
+
49
+ IpcHandler {
50
+ target: "deomarchy.plugin-manager"
51
+
52
+ function open(): void { root.open() }
53
+ function close(): void { root.close() }
54
+ function toggle(): void { root.togglePanel() }
55
+ }
56
+
57
+ BarIconButton {
58
+ id: button
59
+ anchors.fill: parent
60
+ bar: root.bar
61
+ // md-puzzle from JetBrainsMono Nerd Font — the family the bar already
62
+ // renders with — so this matches the other monochrome widget icons.
63
+ text: String.fromCodePoint(0xF0327)
64
+ tooltipText: root.opened ? "Close Plugin Manager" : "Plugin Manager"
65
+
66
+ onPressed: function(b) {
67
+ if (b === Qt.LeftButton) root.togglePanel()
68
+ }
69
+ }
70
+}
0
71
▼shell/plugins/plugin-manager/Panel.qml
+1323-0
1
+import QtQuick
2
+import QtQuick.Controls
3
+import QtQuick.Layouts
4
+import Quickshell
5
+import Quickshell.Io
6
+import qs.Commons
7
+import qs.Ui
8
+
9
+Panel {
10
+ id: root
11
+ moduleName: "deomarchy.plugin-manager"
12
+ ipcTarget: "deomarchy.plugin-manager"
13
+
14
+ implicitWidth: 700
15
+ implicitHeight: 720
16
+
17
+ property var anchorItem: null
18
+ property var hostWidget: null
19
+ readonly property var barIdentity: hostWidget || root
20
+
21
+ // ---------------------------------------------------------------- State
22
+ property string currentTab: "installed" // "installed" | "marketplace"
23
+ property var installedPlugins: []
24
+ property var marketplacePlugins: []
25
+ property var filteredInstalled: []
26
+ property var filteredMarketplace: []
27
+ property var shortcutsMap: ({})
28
+ property var barLayoutData: ({ inBar: [] })
29
+
30
+ property string installedSearch: ""
31
+ property string installedFilter: "all" // "all" | "active" | "inactive" | "bar" | "builtin" | "user"
32
+
33
+ property string marketSearch: ""
34
+ property string marketCategory: "All"
35
+ property string marketSort: "stars" // "stars" | "name" | "date"
36
+
37
+ property string installingId: ""
38
+ property string statusMessage: ""
39
+ property string statusType: "info"
40
+ property bool updatingAll: false
41
+
42
+ property bool urlDialogOpen: false
43
+ property string urlInput: ""
44
+
45
+ property bool confirmUninstallOpen: false
46
+ property var pendingUninstallPlugin: null
47
+
48
+ property bool shortcutDialogOpen: false
49
+ property var shortcutTargetPlugin: null
50
+ property string shortcutInput: ""
51
+
52
+ property bool barSectionDialogOpen: false
53
+ property var barTargetPlugin: null
54
+
55
+ readonly property var registry: root.bar && root.bar.shell
56
+ ? root.bar.shell.pluginRegistry
57
+ : (root.bar ? root.bar.pluginRegistry : null)
58
+
59
+ // ---------------------------------------------------------------- Iconography
60
+ //
61
+ // Nerd Font glyph codepoints (same family the bar renders with) instead of
62
+ // colored emoji. Most glyphs live beyond the BMP, so they are stored as
63
+ // ints and resolved with String.fromCodePoint.
64
+ readonly property string nfFontFamily: "JetBrainsMono Nerd Font"
65
+ function nf(code) { return String.fromCodePoint(code) }
66
+ readonly property var ic: ({
67
+ puzzle: 0xF0327, // md-puzzle
68
+ package: 0xF03D3, // md-package
69
+ globe: 0xF00AC, // fa-globe
70
+ magnify: 0xF0349, // md-magnify
71
+ close: 0xF0156, // md-close
72
+ check: 0xF012C, // md-check
73
+ closeCircle: 0xF0159, // md-close_circle
74
+ checkCircle: 0xF05E0, // md-check_circle
75
+ pin: 0xF0403, // md-pin
76
+ plus: 0xF0415, // md-plus
77
+ keyboard: 0xF097B, // md-keyboard_outline
78
+ trash: 0xF1F8, // fa-trash
79
+ star: 0xF04CE // md-star
80
+ })
81
+
82
+ // Per-plugin glyph, mirroring the Plugin Library window's map.
83
+ function getPluginIcon(id) {
84
+ var map = {
85
+ "omarchy.menu": 0xF0463, // md-rocket
86
+ "omarchy.active-window": 0xF05B1, // md-window_open
87
+ "omarchy.workspaces": 0xF0570, // md-view_grid
88
+ "omarchy.bar": 0xF056E, // md-view_dashboard
89
+ "omarchy.agents": 0xF06A9, // md-robot
90
+ "omarchy.audio": 0xF057E, // md-volume_high
91
+ "omarchy.bluetooth": 0xF00AF, // md-bluetooth
92
+ "omarchy.network": 0xF05A9, // md-wifi
93
+ "omarchy.clock": 0xF0150, // md-clock_outline
94
+ "omarchy.battery": 0xF0079, // md-battery
95
+ "omarchy.power": 0xF0425, // md-power
96
+ "omarchy.sysmon": 0xF029A, // md-gauge
97
+ "omarchy.weather": 0xF0599, // md-weather_sunny
98
+ "omarchy.media": 0xF075A, // md-music
99
+ "omarchy.notifications": 0xF009A, // md-bell
100
+ "omarchy.clipboard": 0xF014D, // md-clipboard_text
101
+ "omarchy.emojis": 0xF01F2, // md-emoticon_outline
102
+ "omarchy.indicators": 0xF0335, // md-lightbulb
103
+ "omarchy.keyboard-layout": 0xF097B, // md-keyboard_outline
104
+ "omarchy.lock": 0xF033E, // md-lock
105
+ "omarchy.nightlight": 0xF0594, // md-weather_night
106
+ "omarchy.microphone": 0xF036C, // md-microphone
107
+ "omarchy.tray": 0xF1294, // md-tray
108
+ "omarchy.background": 0xF02E9, // md-image
109
+ "omarchy.idle": 0xF051F, // md-timer_sand
110
+ "omarchy.polkit": 0xF0BC5, // md-shield_key_outline
111
+ "omarchy.reminders": 0xF13AB, // md-timer
112
+ "omarchy.spacer": 0xF084E, // md-arrow_expand_horizontal
113
+ "omarchy.speedtest": 0xF04C5, // md-speedometer
114
+ "omarchy.system-update": 0xF06B0, // md-update
115
+ "omarchy.tailscale": 0xF0582, // md-vpn
116
+ "omarchy.wifiqr": 0xF0432, // md-qrcode
117
+ "deomarchy.docker-status": 0xF0868, // md-docker
118
+ "deomarchy.wallpaper-manager": 0xF0E09, // md-wallpaper
119
+ "deomarchy.power-profile": 0xF0241, // md-flash
120
+ "deomarchy.system-overview": 0xF154D, // md-chart_box
121
+ "deomarchy.monitor-manager": 0xF0379, // md-monitor
122
+ "deomarchy.plugin-manager": 0xF0327, // md-puzzle
123
+ "deomarchy.plugin-library": 0xF0327, // md-puzzle
124
+ "nosignal.motion-wallpaper": 0xF022F, // md-film
125
+ "omaplug": 0xF06A5 // md-power_plug
126
+ }
127
+ if (map[id] !== undefined) return String.fromCodePoint(map[id])
128
+ return String.fromCodePoint(0xF0068) // md-auto_fix
129
+ }
130
+
131
+ Component.onCompleted: {
132
+ loadInstalled()
133
+ loadMarketplace()
134
+ loadShortcuts()
135
+ loadBarLayout()
136
+ }
137
+
138
+ onOpenedChanged: {
139
+ if (opened) {
140
+ loadInstalled()
141
+ loadMarketplace()
142
+ loadShortcuts()
143
+ loadBarLayout()
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------- Loading
148
+ function loadInstalled() {
149
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
150
+ installedProc.command = ["bash", "-c", "omarchy-shell shell listPlugins 2>/dev/null || " + om + "/bin/omarchy-plugins list --json 2>/dev/null || omarchy plugins list --json 2>/dev/null || echo '[]'"]
151
+ installedProc.running = true
152
+ }
153
+
154
+ function loadMarketplace() {
155
+ var home = Quickshell.env("HOME")
156
+ var om = Quickshell.env("OMARCHY_PATH") || "/usr/share/de-omarchy"
157
+ marketProc.command = ["bash", "-c", "cat " + home + "/.cache/omarchy/marketplace_catalog.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || " + om + "/bin/omarchy-plugins marketplace --json 2>/dev/null || cat " + om + "/registry.json 2>/dev/null | jq -c '.plugins // []' 2>/dev/null || echo '[]'"]
158
+ marketProc.running = true
159
+ }
160
+
161
+ function loadShortcuts() {
162
+ shortcutsProc.command = ["bash", "-c", "omarchy plugins shortcut list --json 2>/dev/null || echo '[]'"]
163
+ shortcutsProc.running = true
164
+ }
165
+
166
+ function loadBarLayout() {
167
+ barProc.command = ["bash", "-c", "omarchy plugins bar list 2>/dev/null || echo '{\"inBar\":[]}'"]
168
+ barProc.running = true
169
+ }
170
+
171
+ Process {
172
+ id: installedProc
173
+ running: false
174
+ stdout: StdioCollector {
175
+ waitForEnd: true
176
+ onStreamFinished: {
177
+ try {
178
+ var arr = JSON.parse(String(text).trim())
179
+ if (Array.isArray(arr)) {
180
+ root.installedPlugins = arr
181
+ root.applyInstalledFilter()
182
+ }
183
+ } catch (e) {
184
+ console.warn("Failed to parse installed plugins: " + e)
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ Process {
191
+ id: marketProc
192
+ running: false
193
+ stdout: StdioCollector {
194
+ waitForEnd: true
195
+ onStreamFinished: {
196
+ try {
197
+ var data = JSON.parse(String(text).trim())
198
+ var arr = Array.isArray(data) ? data : (data && data.plugins ? data.plugins : [])
199
+ if (Array.isArray(arr)) {
200
+ root.marketplacePlugins = arr
201
+ root.applyMarketFilter()
202
+ }
203
+ } catch (e) {
204
+ console.warn("Failed to parse marketplace plugins: " + e)
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ Process {
211
+ id: shortcutsProc
212
+ running: false
213
+ stdout: StdioCollector {
214
+ waitForEnd: true
215
+ onStreamFinished: {
216
+ try {
217
+ var arr = JSON.parse(String(text).trim())
218
+ var map = {}
219
+ if (Array.isArray(arr)) {
220
+ for (var i = 0; i < arr.length; i++) {
221
+ if (arr[i] && arr[i].id) {
222
+ map[arr[i].id] = arr[i].keys || ""
223
+ }
224
+ }
225
+ }
226
+ root.shortcutsMap = map
227
+ } catch (e) {
228
+ console.warn("Failed to parse shortcuts: " + e)
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ Process {
235
+ id: barProc
236
+ running: false
237
+ stdout: StdioCollector {
238
+ waitForEnd: true
239
+ onStreamFinished: {
240
+ try {
241
+ var data = JSON.parse(String(text).trim())
242
+ if (data && data.inBar) {
243
+ root.barLayoutData = data
244
+ root.applyInstalledFilter()
245
+ }
246
+ } catch (e) {
247
+ console.warn("Failed to parse bar layout: " + e)
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ // ---------------------------------------------------------------- Filtering
254
+ function isPluginInBar(id) {
255
+ if (!id || !root.barLayoutData || !root.barLayoutData.inBar) return false
256
+ return root.barLayoutData.inBar.indexOf(id) !== -1
257
+ }
258
+
259
+ function applyInstalledFilter() {
260
+ var list = root.installedPlugins || []
261
+ var q = root.installedSearch.toLowerCase().trim()
262
+ var filter = root.installedFilter
263
+
264
+ var res = []
265
+ for (var i = 0; i < list.length; i++) {
266
+ var p = list[i]
267
+ if (filter === "active" && !p.enabled) continue
268
+ if (filter === "inactive" && p.enabled) continue
269
+ if (filter === "bar" && !isPluginInBar(p.id)) continue
270
+ if (filter === "builtin" && !p.firstParty) continue
271
+ if (filter === "user" && p.firstParty) continue
272
+
273
+ if (q.length > 0) {
274
+ var name = (p.name || "").toLowerCase()
275
+ var id = (p.id || "").toLowerCase()
276
+ var kinds = (p.kinds || []).join(" ").toLowerCase()
277
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && kinds.indexOf(q) === -1) continue
278
+ }
279
+ res.push(p)
280
+ }
281
+ root.filteredInstalled = res
282
+ }
283
+
284
+ function applyMarketFilter() {
285
+ var list = root.marketplacePlugins || []
286
+ var q = root.marketSearch.toLowerCase().trim()
287
+ var cat = root.marketCategory
288
+
289
+ var res = []
290
+ for (var i = 0; i < list.length; i++) {
291
+ var p = list[i]
292
+ if (cat !== "All" && (p.category || "").toLowerCase() !== cat.toLowerCase()) continue
293
+
294
+ if (q.length > 0) {
295
+ var name = (p.name || "").toLowerCase()
296
+ var id = (p.id || "").toLowerCase()
297
+ var desc = (p.description || "").toLowerCase()
298
+ var author = (p.author || "").toLowerCase()
299
+ var tags = (p.tags || []).join(" ").toLowerCase()
300
+ if (name.indexOf(q) === -1 && id.indexOf(q) === -1 && desc.indexOf(q) === -1 && author.indexOf(q) === -1 && tags.indexOf(q) === -1) continue
301
+ }
302
+ res.push(p)
303
+ }
304
+
305
+ if (root.marketSort === "stars") {
306
+ res.sort(function(a, b) { return (b.stars || 0) - (a.stars || 0) })
307
+ } else if (root.marketSort === "name") {
308
+ res.sort(function(a, b) { return (a.name || a.id).localeCompare(b.name || b.id) })
309
+ } else if (root.marketSort === "date") {
310
+ res.sort(function(a, b) { return String(b.addedAt || "").localeCompare(String(a.addedAt || "")) })
311
+ }
312
+
313
+ root.filteredMarketplace = res
314
+ }
315
+
316
+ onInstalledSearchChanged: applyInstalledFilter()
317
+ onInstalledFilterChanged: applyInstalledFilter()
318
+ onMarketSearchChanged: applyMarketFilter()
319
+ onMarketCategoryChanged: applyMarketFilter()
320
+ onMarketSortChanged: applyMarketFilter()
321
+
322
+ // ---------------------------------------------------------------- Helpers
323
+ function isInstalled(id) {
324
+ if (!id) return false
325
+ var cleanId = id.replace(/^[a-z0-9_-]+\./, "")
326
+ for (var i = 0; i < root.installedPlugins.length; i++) {
327
+ var instId = root.installedPlugins[i].id
328
+ if (instId === id || instId === cleanId || instId.endsWith("." + id) || id.endsWith("." + instId)) {
329
+ return true
330
+ }
331
+ }
332
+ return false
333
+ }
334
+
335
+ function showToast(msg, type) {
336
+ root.statusMessage = msg
337
+ root.statusType = type || "info"
338
+ toastTimer.restart()
339
+ }
340
+
341
+ Timer {
342
+ id: toastTimer
343
+ interval: 3500
344
+ onTriggered: root.statusMessage = ""
345
+ }
346
+
347
+ function togglePlugin(plugin) {
348
+ if (!plugin || !plugin.id) return
349
+ var targetState = !plugin.enabled
350
+ plugin.enabled = targetState
351
+ applyInstalledFilter()
352
+
353
+ if (root.registry && typeof root.registry.setEnabled === "function") {
354
+ root.registry.setEnabled(plugin.id, targetState)
355
+ }
356
+
357
+ actionProc.command = ["bash", "-c", "omarchy plugins " + (targetState ? "enable" : "disable") + " '" + plugin.id + "'"]
358
+ actionProc.targetAction = "toggle"
359
+ actionProc.targetName = plugin.name || plugin.id
360
+ actionProc.targetState = targetState
361
+ actionProc.running = true
362
+ }
363
+
364
+ function toggleBarPlacement(plugin) {
365
+ if (!plugin || !plugin.id) return
366
+ if (isPluginInBar(plugin.id)) {
367
+ actionProc.command = ["bash", "-c", "omarchy plugins bar remove '" + plugin.id + "'"]
368
+ actionProc.targetAction = "bar_remove"
369
+ actionProc.targetName = plugin.name || plugin.id
370
+ actionProc.running = true
371
+ } else {
372
+ root.barTargetPlugin = plugin
373
+ root.barSectionDialogOpen = true
374
+ }
375
+ }
376
+
377
+ function addPluginToBar(plugin, section) {
378
+ root.barSectionDialogOpen = false
379
+ root.barTargetPlugin = null
380
+ if (!plugin || !plugin.id) return
381
+
382
+ showToast("Adding " + (plugin.name || plugin.id) + " to bar (" + section + ")...", "info")
383
+ actionProc.command = ["bash", "-c", "omarchy plugins bar add '" + plugin.id + "' " + section]
384
+ actionProc.targetAction = "bar_add"
385
+ actionProc.targetName = plugin.name || plugin.id
386
+ actionProc.running = true
387
+ }
388
+
389
+ function openShortcutDialog(plugin) {
390
+ root.shortcutTargetPlugin = plugin
391
+ root.shortcutInput = root.shortcutsMap[plugin.id] || ""
392
+ root.shortcutDialogOpen = true
393
+ }
394
+
395
+ function saveShortcut() {
396
+ var p = root.shortcutTargetPlugin
397
+ var keys = root.shortcutInput.trim()
398
+ root.shortcutDialogOpen = false
399
+ root.shortcutTargetPlugin = null
400
+ if (!p) return
401
+
402
+ if (keys === "") {
403
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut remove '" + p.id + "'"]
404
+ actionProc.targetAction = "shortcut_remove"
405
+ actionProc.targetName = p.name || p.id
406
+ actionProc.running = true
407
+ } else {
408
+ actionProc.command = ["bash", "-c", "omarchy plugins shortcut set '" + p.id + "' '" + keys + "'"]
409
+ actionProc.targetAction = "shortcut_set"
410
+ actionProc.targetName = p.name || p.id
411
+ actionProc.running = true
412
+ }
413
+ }
414
+
415
+ function clearShortcut() {
416
+ root.shortcutInput = ""
417
+ saveShortcut()
418
+ }
419
+
420
+ function installPlugin(p) {
421
+ if (!p || root.installingId !== "") return
422
+ root.installingId = p.id
423
+ showToast("Installing " + (p.name || p.id) + "...", "info")
424
+
425
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + p.id + "' --enable"]
426
+ actionProc.targetAction = "install"
427
+ actionProc.targetName = p.name || p.id
428
+ actionProc.running = true
429
+ }
430
+
431
+ function installFromUrl(url) {
432
+ if (!url || url.trim() === "") return
433
+ url = url.trim()
434
+ root.urlDialogOpen = false
435
+ root.urlInput = ""
436
+ showToast("Installing: " + url + "...", "info")
437
+
438
+ actionProc.command = ["bash", "-c", "omarchy plugins install '" + url + "' --enable"]
439
+ actionProc.targetAction = "install"
440
+ actionProc.targetName = url
441
+ actionProc.running = true
442
+ }
443
+
444
+ function confirmUninstall(p) {
445
+ root.pendingUninstallPlugin = p
446
+ root.confirmUninstallOpen = true
447
+ }
448
+
449
+ function executeUninstall() {
450
+ var p = root.pendingUninstallPlugin
451
+ root.confirmUninstallOpen = false
452
+ root.pendingUninstallPlugin = null
453
+ if (!p) return
454
+
455
+ showToast("Removing " + (p.name || p.id) + "...", "info")
456
+ actionProc.command = ["bash", "-c", "omarchy plugins remove '" + p.id + "'"]
457
+ actionProc.targetAction = "remove"
458
+ actionProc.targetName = p.name || p.id
459
+ actionProc.running = true
460
+ }
461
+
462
+ Process {
463
+ id: actionProc
464
+ running: false
465
+ property string targetAction: ""
466
+ property string targetName: ""
467
+ property bool targetState: false
468
+
469
+ onExited: function(code) {
470
+ root.installingId = ""
471
+ root.updatingAll = false
472
+ if (code === 0) {
473
+ var ok = root.nf(root.ic.checkCircle)
474
+ if (targetAction === "install") {
475
+ root.showToast(ok + " Installed " + targetName + "!", "success")
476
+ } else if (targetAction === "remove") {
477
+ root.showToast(ok + " Removed " + targetName + "!", "success")
478
+ } else if (targetAction === "toggle") {
479
+ root.showToast(ok + " " + (targetState ? "Enabled" : "Disabled") + " " + targetName, "success")
480
+ } else if (targetAction === "bar_add") {
481
+ root.showToast(ok + " Added " + targetName + " to top bar!", "success")
482
+ } else if (targetAction === "bar_remove") {
483
+ root.showToast(ok + " Removed " + targetName + " from bar.", "success")
484
+ } else if (targetAction === "shortcut_set") {
485
+ root.showToast(ok + " Shortcut saved for " + targetName + "!", "success")
486
+ } else if (targetAction === "shortcut_remove") {
487
+ root.showToast(ok + " Shortcut cleared for " + targetName, "success")
488
+ }
489
+ } else {
490
+ root.showToast(root.nf(root.ic.closeCircle) + " Action failed for " + targetName, "error")
491
+ }
492
+ root.loadInstalled()
493
+ root.loadShortcuts()
494
+ root.loadBarLayout()
495
+ }
496
+ }
497
+
498
+ function getInitials(name) {
499
+ if (!name) return "?"
500
+ var parts = name.split(/[\s.-]+/)
501
+ if (parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0) {
502
+ return (parts[0][0] + parts[1][0]).toUpperCase()
503
+ }
504
+ return name.substring(0, Math.min(2, name.length)).toUpperCase()
505
+ }
506
+
507
+ // ---------------------------------------------------------------- Content Layout
508
+ ColumnLayout {
509
+ anchors.fill: parent
510
+ anchors.margins: 14
511
+ spacing: 12
512
+
513
+ // Header
514
+ RowLayout {
515
+ Layout.fillWidth: true
516
+ spacing: 10
517
+
518
+ Text {
519
+ text: root.nf(root.ic.puzzle) + " Plugin Manager"
520
+ color: Color.foreground
521
+ font.family: root.nfFontFamily
522
+ font.pixelSize: 18
523
+ font.bold: true
524
+ }
525
+
526
+ Item { Layout.fillWidth: true }
527
+
528
+ // Tab switcher
529
+ Rectangle {
530
+ Layout.preferredWidth: 260
531
+ Layout.preferredHeight: 32
532
+ radius: 16
533
+ color: Qt.rgba(0, 0, 0, 0.25)
534
+ border.color: Qt.rgba(255, 255, 255, 0.08)
535
+ border.width: 1
536
+
537
+ RowLayout {
538
+ anchors.fill: parent
539
+ spacing: 0
540
+
541
+ Rectangle {
542
+ Layout.fillWidth: true
543
+ Layout.fillHeight: true
544
+ radius: 14
545
+ color: root.currentTab === "installed" ? Color.accent : "transparent"
546
+
547
+ Text {
548
+ anchors.centerIn: parent
549
+ text: root.nf(root.ic.package) + " Installed (" + root.installedPlugins.length + ")"
550
+ color: root.currentTab === "installed" ? Color.background : Color.foreground
551
+ font.family: root.nfFontFamily
552
+ font.pixelSize: 11
553
+ font.bold: root.currentTab === "installed"
554
+ }
555
+
556
+ MouseArea {
557
+ anchors.fill: parent
558
+ cursorShape: Qt.PointingHandCursor
559
+ onClicked: root.currentTab = "installed"
560
+ }
561
+ }
562
+
563
+ Rectangle {
564
+ Layout.fillWidth: true
565
+ Layout.fillHeight: true
566
+ radius: 14
567
+ color: root.currentTab === "marketplace" ? Color.accent : "transparent"
568
+
569
+ Text {
570
+ anchors.centerIn: parent
571
+ text: root.nf(root.ic.globe) + " Marketplace"
572
+ color: root.currentTab === "marketplace" ? Color.background : Color.foreground
573
+ font.family: root.nfFontFamily
574
+ font.pixelSize: 11
575
+ font.bold: root.currentTab === "marketplace"
576
+ }
577
+
578
+ MouseArea {
579
+ anchors.fill: parent
580
+ cursorShape: Qt.PointingHandCursor
581
+ onClicked: {
582
+ root.currentTab = "marketplace"
583
+ if (root.marketplacePlugins.length === 0) root.loadMarketplace()
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+
590
+ // Add Git URL Button
591
+ Rectangle {
592
+ Layout.preferredHeight: 30
593
+ Layout.preferredWidth: 32
594
+ radius: 6
595
+ color: Qt.rgba(255, 255, 255, 0.08)
596
+ border.color: Qt.rgba(255, 255, 255, 0.12)
597
+ border.width: 1
598
+
599
+ Text {
600
+ anchors.centerIn: parent
601
+ text: "+"
602
+ color: Color.foreground
603
+ font.pixelSize: 16
604
+ font.bold: true
605
+ }
606
+
607
+ MouseArea {
608
+ anchors.fill: parent
609
+ cursorShape: Qt.PointingHandCursor
610
+ onClicked: root.urlDialogOpen = true
611
+ }
612
+ }
613
+ }
614
+
615
+ // Status Banner Toast
616
+ Rectangle {
617
+ Layout.fillWidth: true
618
+ Layout.preferredHeight: root.statusMessage !== "" ? 30 : 0
619
+ visible: root.statusMessage !== ""
620
+ radius: 6
621
+ color: root.statusType === "success" ? Qt.rgba(0.2, 0.8, 0.3, 0.2) : Qt.rgba(0.3, 0.6, 0.9, 0.2)
622
+ border.color: root.statusType === "success" ? "#a6e3a1" : "#89b4fa"
623
+ border.width: 1
624
+ clip: true
625
+
626
+ Text {
627
+ anchors.centerIn: parent
628
+ text: root.statusMessage
629
+ color: Color.foreground
630
+ font.pixelSize: 11
631
+ font.bold: true
632
+ }
633
+ }
634
+
635
+ // Stack Views
636
+ StackLayout {
637
+ Layout.fillWidth: true
638
+ Layout.fillHeight: true
639
+ currentIndex: root.currentTab === "installed" ? 0 : 1
640
+
641
+ // TAB 1: INSTALLED
642
+ Item {
643
+ ColumnLayout {
644
+ anchors.fill: parent
645
+ spacing: 8
646
+
647
+ // Search Bar
648
+ Rectangle {
649
+ Layout.fillWidth: true
650
+ Layout.preferredHeight: 32
651
+ radius: 6
652
+ color: Qt.rgba(0, 0, 0, 0.2)
653
+ border.color: Qt.rgba(255, 255, 255, 0.1)
654
+ border.width: 1
655
+
656
+ RowLayout {
657
+ anchors.fill: parent
658
+ anchors.margins: 6
659
+ spacing: 6
660
+ Text { text: root.nf(root.ic.magnify); font.family: root.nfFontFamily; font.pixelSize: 11; color: Color.subtext }
661
+ TextInput {
662
+ id: installedSearchField
663
+ Layout.fillWidth: true
664
+ text: root.installedSearch
665
+ color: Color.foreground
666
+ font.pixelSize: 11
667
+ onTextChanged: root.installedSearch = text
668
+ Text {
669
+ text: "Filter installed..."
670
+ color: Color.subtext
671
+ font.pixelSize: 11
672
+ visible: !installedSearchField.text
673
+ }
674
+ }
675
+ }
676
+ }
677
+
678
+ // Filter Chips
679
+ RowLayout {
680
+ spacing: 6
681
+ Repeater {
682
+ model: [
683
+ { id: "all", label: "All" },
684
+ { id: "active", label: "Active" },
685
+ { id: "inactive", label: "Inactive" },
686
+ { id: "bar", label: "In Bar" },
687
+ { id: "user", label: "User" }
688
+ ]
689
+ delegate: Rectangle {
690
+ required property var modelData
691
+ Layout.preferredHeight: 24
692
+ Layout.preferredWidth: chipText.implicitWidth + 14
693
+ radius: 12
694
+ color: root.installedFilter === modelData.id ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.25) : Qt.rgba(255, 255, 255, 0.05)
695
+ border.color: root.installedFilter === modelData.id ? Color.accent : "transparent"
696
+ border.width: 1
697
+
698
+ Text {
699
+ id: chipText
700
+ anchors.centerIn: parent
701
+ text: modelData.label
702
+ color: Color.foreground
703
+ font.pixelSize: 10
704
+ }
705
+
706
+ MouseArea {
707
+ anchors.fill: parent
708
+ cursorShape: Qt.PointingHandCursor
709
+ onClicked: root.installedFilter = modelData.id
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ // List
716
+ ListView {
717
+ Layout.fillWidth: true
718
+ Layout.fillHeight: true
719
+ clip: true
720
+ spacing: 6
721
+ model: root.filteredInstalled
722
+
723
+ delegate: Rectangle {
724
+ required property var modelData
725
+ property bool inBar: root.isPluginInBar(modelData.id)
726
+ property string shortcut: root.shortcutsMap[modelData.id] || ""
727
+ property bool isBarCompatible: {
728
+ var kinds = modelData.kinds || []
729
+ return kinds.indexOf("bar-widget") !== -1 || kinds.indexOf("bar") !== -1 || kinds.indexOf("panel") !== -1
730
+ }
731
+
732
+ width: parent.width
733
+ height: 66
734
+ radius: 8
735
+ color: Qt.rgba(255, 255, 255, modelData.enabled ? 0.08 : 0.03)
736
+ border.color: Qt.rgba(255, 255, 255, 0.08)
737
+ border.width: 1
738
+
739
+ RowLayout {
740
+ anchors.fill: parent
741
+ anchors.margins: 8
742
+ spacing: 8
743
+
744
+ Rectangle {
745
+ Layout.preferredWidth: 36
746
+ Layout.preferredHeight: 36
747
+ radius: 8
748
+ color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.2)
749
+ Text {
750
+ anchors.centerIn: parent
751
+ text: root.getPluginIcon(modelData.id)
752
+ color: Color.accent
753
+ font.family: root.nfFontFamily
754
+ font.pixelSize: 18
755
+ }
756
+ }
757
+
758
+ ColumnLayout {
759
+ Layout.fillWidth: true
760
+ spacing: 2
761
+ Text {
762
+ text: modelData.name || modelData.id
763
+ color: Color.foreground
764
+ font.pixelSize: 12
765
+ font.bold: true
766
+ }
767
+ Text {
768
+ text: modelData.id + (modelData.firstParty ? " (Built-in)" : " (User)")
769
+ color: Color.subtext
770
+ font.pixelSize: 10
771
+ }
772
+ }
773
+
774
+ // Bar placement toggle
775
+ Rectangle {
776
+ visible: isBarCompatible
777
+ Layout.preferredHeight: 26
778
+ Layout.preferredWidth: pBarText.implicitWidth + 12
779
+ radius: 4
780
+ color: inBar ? Qt.rgba(0.2, 0.8, 0.3, 0.18) : Qt.rgba(255, 255, 255, 0.06)
781
+ border.color: inBar ? "#a6e3a1" : Qt.rgba(255, 255, 255, 0.1)
782
+ border.width: 1
783
+
784
+ Text {
785
+ id: pBarText
786
+ anchors.centerIn: parent
787
+ text: inBar ? root.nf(root.ic.pin) + " In Bar" : root.nf(root.ic.plus) + " Bar"
788
+ color: inBar ? "#a6e3a1" : Color.subtext
789
+ font.family: root.nfFontFamily
790
+ font.pixelSize: 9
791
+ font.bold: inBar
792
+ }
793
+
794
+ MouseArea {
795
+ anchors.fill: parent
796
+ cursorShape: Qt.PointingHandCursor
797
+ onClicked: root.toggleBarPlacement(modelData)
798
+ }
799
+ }
800
+
801
+ // Shortcut button
802
+ Rectangle {
803
+ Layout.preferredHeight: 26
804
+ Layout.preferredWidth: pScText.implicitWidth + 12
805
+ radius: 4
806
+ color: shortcut !== "" ? Qt.rgba(1, 0.7, 0.2, 0.18) : Qt.rgba(255, 255, 255, 0.06)
807
+ border.color: shortcut !== "" ? "#ff9e64" : Qt.rgba(255, 255, 255, 0.1)
808
+ border.width: 1
809
+
810
+ Text {
811
+ id: pScText
812
+ anchors.centerIn: parent
813
+ text: root.nf(root.ic.keyboard) + (shortcut !== "" ? " " + shortcut : " Key")
814
+ color: shortcut !== "" ? "#ff9e64" : Color.subtext
815
+ font.family: root.nfFontFamily
816
+ font.pixelSize: 9
817
+ font.bold: shortcut !== ""
818
+ }
819
+
820
+ MouseArea {
821
+ anchors.fill: parent
822
+ cursorShape: Qt.PointingHandCursor
823
+ onClicked: root.openShortcutDialog(modelData)
824
+ }
825
+ }
826
+
827
+ // Delete button for user plugins
828
+ Rectangle {
829
+ visible: !modelData.firstParty
830
+ Layout.preferredWidth: 26
831
+ Layout.preferredHeight: 26
832
+ radius: 4
833
+ color: Qt.rgba(1, 0, 0, 0.15)
834
+ Text { anchors.centerIn: parent; text: root.nf(root.ic.trash); font.family: root.nfFontFamily; font.pixelSize: 12; color: "#f7768e" }
835
+ MouseArea {
836
+ anchors.fill: parent
837
+ cursorShape: Qt.PointingHandCursor
838
+ onClicked: root.confirmUninstall(modelData)
839
+ }
840
+ }
841
+
842
+ // Switch
843
+ Rectangle {
844
+ Layout.preferredWidth: 44
845
+ Layout.preferredHeight: 24
846
+ radius: 12
847
+ color: modelData.enabled ? Color.accent : Qt.rgba(255, 255, 255, 0.15)
848
+ border.color: Qt.rgba(255, 255, 255, 0.1)
849
+ border.width: 1
850
+
851
+ Rectangle {
852
+ width: 18
853
+ height: 18
854
+ radius: 9
855
+ color: "#ffffff"
856
+ anchors.verticalCenter: parent.verticalCenter
857
+ x: modelData.enabled ? 22 : 4
858
+ Behavior on x { NumberAnimation { duration: 120 } }
859
+ }
860
+
861
+ MouseArea {
862
+ anchors.fill: parent
863
+ cursorShape: Qt.PointingHandCursor
864
+ onClicked: root.togglePlugin(modelData)
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ }
871
+ }
872
+
873
+ // TAB 2: MARKETPLACE
874
+ Item {
875
+ ColumnLayout {
876
+ anchors.fill: parent
877
+ spacing: 8
878
+
879
+ // Search Bar
880
+ Rectangle {
881
+ Layout.fillWidth: true
882
+ Layout.preferredHeight: 32
883
+ radius: 6
884
+ color: Qt.rgba(0, 0, 0, 0.2)
885
+ border.color: Qt.rgba(255, 255, 255, 0.1)
886
+ border.width: 1
887
+
888
+ RowLayout {
889
+ anchors.fill: parent
890
+ anchors.margins: 6
891
+ spacing: 6
892
+ Text { text: root.nf(root.ic.globe); font.family: root.nfFontFamily; font.pixelSize: 11; color: Color.subtext }
893
+ TextInput {
894
+ id: marketSearchField
895
+ Layout.fillWidth: true
896
+ text: root.marketSearch
897
+ color: Color.foreground
898
+ font.pixelSize: 11
899
+ onTextChanged: root.marketSearch = text
900
+ Text {
901
+ text: "Search 1,000+ plugins..."
902
+ color: Color.subtext
903
+ font.pixelSize: 11
904
+ visible: !marketSearchField.text
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ // Category Chips
911
+ ScrollView {
912
+ Layout.fillWidth: true
913
+ Layout.preferredHeight: 28
914
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
915
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
916
+
917
+ RowLayout {
918
+ spacing: 6
919
+ Repeater {
920
+ model: ["All", "Appearance", "Desktop", "Productivity", "System", "Utilities", "Widgets"]
921
+ delegate: Rectangle {
922
+ required property var modelData
923
+ Layout.preferredHeight: 24
924
+ Layout.preferredWidth: mCatText.implicitWidth + 14
925
+ radius: 12
926
+ color: root.marketCategory === modelData ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.25) : Qt.rgba(255, 255, 255, 0.05)
927
+ border.color: root.marketCategory === modelData ? Color.accent : "transparent"
928
+ border.width: 1
929
+
930
+ Text {
931
+ id: mCatText
932
+ anchors.centerIn: parent
933
+ text: modelData
934
+ color: Color.foreground
935
+ font.pixelSize: 10
936
+ }
937
+
938
+ MouseArea {
939
+ anchors.fill: parent
940
+ cursorShape: Qt.PointingHandCursor
941
+ onClicked: root.marketCategory = modelData
942
+ }
943
+ }
944
+ }
945
+ }
946
+ }
947
+
948
+ // List
949
+ ListView {
950
+ Layout.fillWidth: true
951
+ Layout.fillHeight: true
952
+ clip: true
953
+ spacing: 6
954
+ model: root.filteredMarketplace
955
+
956
+ delegate: Rectangle {
957
+ required property var modelData
958
+ property bool installed: root.isInstalled(modelData.id)
959
+ width: parent.width
960
+ height: 74
961
+ radius: 8
962
+ color: Qt.rgba(255, 255, 255, 0.05)
963
+ border.color: Qt.rgba(255, 255, 255, 0.08)
964
+ border.width: 1
965
+
966
+ RowLayout {
967
+ anchors.fill: parent
968
+ anchors.margins: 10
969
+ spacing: 10
970
+
971
+ Rectangle {
972
+ Layout.preferredWidth: 38
973
+ Layout.preferredHeight: 38
974
+ radius: 8
975
+ color: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.2)
976
+ Text {
977
+ anchors.centerIn: parent
978
+ text: modelData.initials || root.getInitials(modelData.name || modelData.id)
979
+ color: Color.foreground
980
+ font.pixelSize: 13
981
+ font.bold: true
982
+ }
983
+ }
984
+
985
+ ColumnLayout {
986
+ Layout.fillWidth: true
987
+ spacing: 2
988
+ RowLayout {
989
+ spacing: 6
990
+ Text {
991
+ text: modelData.name || modelData.id
992
+ color: Color.foreground
993
+ font.pixelSize: 12
994
+ font.bold: true
995
+ }
996
+ Text {
997
+ visible: (modelData.stars || 0) > 0
998
+ text: root.nf(root.ic.star) + " " + modelData.stars
999
+ color: "#e0af68"
1000
+ font.family: root.nfFontFamily
1001
+ font.pixelSize: 10
1002
+ }
1003
+ }
1004
+ Text {
1005
+ Layout.fillWidth: true
1006
+ text: modelData.description || ""
1007
+ color: Color.subtext
1008
+ font.pixelSize: 10
1009
+ elide: Text.ElideRight
1010
+ }
1011
+ }
1012
+
1013
+ // Install button
1014
+ Rectangle {
1015
+ Layout.preferredHeight: 28
1016
+ Layout.preferredWidth: installed ? 78 : 66
1017
+ radius: 6
1018
+ color: installed ? Qt.rgba(0.2, 0.8, 0.3, 0.18) : (root.installingId === modelData.id ? Qt.rgba(255, 255, 255, 0.08) : Color.accent)
1019
+ border.color: installed ? "#a6e3a1" : "transparent"
1020
+ border.width: installed ? 1 : 0
1021
+
1022
+ Text {
1023
+ anchors.centerIn: parent
1024
+ text: installed ? root.nf(root.ic.check) + " Installed" : (root.installingId === modelData.id ? "..." : "Install")
1025
+ color: installed ? "#a6e3a1" : (root.installingId === modelData.id ? Color.subtext : Color.background)
1026
+ font.family: root.nfFontFamily
1027
+ font.pixelSize: 10
1028
+ font.bold: true
1029
+ }
1030
+
1031
+ MouseArea {
1032
+ anchors.fill: parent
1033
+ cursorShape: Qt.PointingHandCursor
1034
+ enabled: !installed && root.installingId === ""
1035
+ onClicked: root.installPlugin(modelData)
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+ // Modals
1047
+ // 1. Shortcut Dialog
1048
+ Rectangle {
1049
+ anchors.fill: parent
1050
+ color: Qt.rgba(0, 0, 0, 0.75)
1051
+ visible: root.shortcutDialogOpen
1052
+
1053
+ MouseArea { anchors.fill: parent; onClicked: root.shortcutDialogOpen = false }
1054
+
1055
+ Rectangle {
1056
+ anchors.centerIn: parent
1057
+ width: 440
1058
+ height: 240
1059
+ radius: 10
1060
+ color: Color.popups ? Color.popups.background : "#1f2335"
1061
+ border.color: "#ff9e64"
1062
+ border.width: 1
1063
+
1064
+ MouseArea { anchors.fill: parent }
1065
+
1066
+ ColumnLayout {
1067
+ anchors.fill: parent
1068
+ anchors.margins: 16
1069
+ spacing: 10
1070
+
1071
+ Text {
1072
+ text: root.nf(root.ic.keyboard) + " Set Keyboard Shortcut"
1073
+ color: Color.foreground
1074
+ font.family: root.nfFontFamily
1075
+ font.pixelSize: 14
1076
+ font.bold: true
1077
+ }
1078
+
1079
+ Text {
1080
+ text: "Set Hyprland keybind for '" + (root.shortcutTargetPlugin ? (root.shortcutTargetPlugin.name || root.shortcutTargetPlugin.id) : "") + "':"
1081
+ color: Color.subtext
1082
+ font.pixelSize: 11
1083
+ }
1084
+
1085
+ Rectangle {
1086
+ Layout.fillWidth: true
1087
+ Layout.preferredHeight: 34
1088
+ radius: 6
1089
+ color: Qt.rgba(0, 0, 0, 0.25)
1090
+ border.color: "#ff9e64"
1091
+ border.width: 1
1092
+
1093
+ TextInput {
1094
+ anchors.fill: parent
1095
+ anchors.margins: 6
1096
+ text: root.shortcutInput
1097
+ color: Color.foreground
1098
+ font.pixelSize: 12
1099
+ font.bold: true
1100
+ onTextChanged: root.shortcutInput = text
1101
+ Text {
1102
+ text: "e.g. SUPER + ALT + D"
1103
+ color: Color.subtext
1104
+ font.pixelSize: 11
1105
+ visible: !parent.text
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ RowLayout {
1111
+ Layout.fillWidth: true
1112
+ Button {
1113
+ text: "Clear"
1114
+ onClicked: root.clearShortcut()
1115
+ }
1116
+ Item { Layout.fillWidth: true }
1117
+ Button {
1118
+ text: "Cancel"
1119
+ onClicked: root.shortcutDialogOpen = false
1120
+ }
1121
+ Button {
1122
+ text: "Save & Apply"
1123
+ onClicked: root.saveShortcut()
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ // 2. Bar Section Selector
1131
+ Rectangle {
1132
+ anchors.fill: parent
1133
+ color: Qt.rgba(0, 0, 0, 0.75)
1134
+ visible: root.barSectionDialogOpen
1135
+
1136
+ MouseArea { anchors.fill: parent; onClicked: root.barSectionDialogOpen = false }
1137
+
1138
+ Rectangle {
1139
+ anchors.centerIn: parent
1140
+ width: 380
1141
+ height: 180
1142
+ radius: 10
1143
+ color: Color.popups ? Color.popups.background : "#1f2335"
1144
+ border.color: "#a6e3a1"
1145
+ border.width: 1
1146
+
1147
+ MouseArea { anchors.fill: parent }
1148
+
1149
+ ColumnLayout {
1150
+ anchors.fill: parent
1151
+ anchors.margins: 16
1152
+ spacing: 12
1153
+
1154
+ Text {
1155
+ text: root.nf(root.ic.pin) + " Add to Omarchy Top Bar"
1156
+ color: Color.foreground
1157
+ font.pixelSize: 14
1158
+ font.bold: true
1159
+ }
1160
+
1161
+ Text {
1162
+ text: "Select bar section:"
1163
+ color: Color.subtext
1164
+ font.pixelSize: 11
1165
+ }
1166
+
1167
+ RowLayout {
1168
+ Layout.fillWidth: true
1169
+ spacing: 8
1170
+
1171
+ Button {
1172
+ Layout.fillWidth: true
1173
+ text: "Left"
1174
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "left")
1175
+ }
1176
+ Button {
1177
+ Layout.fillWidth: true
1178
+ text: "Center"
1179
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "center")
1180
+ }
1181
+ Button {
1182
+ Layout.fillWidth: true
1183
+ text: "Right"
1184
+ onClicked: root.addPluginToBar(root.barTargetPlugin, "right")
1185
+ }
1186
+ }
1187
+
1188
+ RowLayout {
1189
+ Layout.fillWidth: true
1190
+ Item { Layout.fillWidth: true }
1191
+ Button {
1192
+ text: "Cancel"
1193
+ onClicked: root.barSectionDialogOpen = false
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ // 3. Git URL Dialog Modal
1201
+ Rectangle {
1202
+ anchors.fill: parent
1203
+ color: Qt.rgba(0, 0, 0, 0.7)
1204
+ visible: root.urlDialogOpen
1205
+
1206
+ MouseArea { anchors.fill: parent; onClicked: root.urlDialogOpen = false }
1207
+
1208
+ Rectangle {
1209
+ anchors.centerIn: parent
1210
+ width: 420
1211
+ height: 180
1212
+ radius: 10
1213
+ color: Color.popups ? Color.popups.background : "#1f2335"
1214
+ border.color: Qt.rgba(255, 255, 255, 0.15)
1215
+ border.width: 1
1216
+
1217
+ MouseArea { anchors.fill: parent }
1218
+
1219
+ ColumnLayout {
1220
+ anchors.fill: parent
1221
+ anchors.margins: 16
1222
+ spacing: 10
1223
+
1224
+ Text {
1225
+ text: "Install from Git Repository"
1226
+ color: Color.foreground
1227
+ font.pixelSize: 14
1228
+ font.bold: true
1229
+ }
1230
+
1231
+ Rectangle {
1232
+ Layout.fillWidth: true
1233
+ Layout.preferredHeight: 34
1234
+ radius: 6
1235
+ color: Qt.rgba(0, 0, 0, 0.25)
1236
+ border.color: Color.accent
1237
+ border.width: 1
1238
+
1239
+ TextInput {
1240
+ anchors.fill: parent
1241
+ anchors.margins: 6
1242
+ text: root.urlInput
1243
+ color: Color.foreground
1244
+ font.pixelSize: 11
1245
+ onTextChanged: root.urlInput = text
1246
+ Text {
1247
+ text: "https://github.com/..."
1248
+ color: Color.subtext
1249
+ font.pixelSize: 11
1250
+ visible: !parent.text
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ RowLayout {
1256
+ Layout.fillWidth: true
1257
+ Item { Layout.fillWidth: true }
1258
+ Button {
1259
+ text: "Cancel"
1260
+ onClicked: root.urlDialogOpen = false
1261
+ }
1262
+ Button {
1263
+ text: "Install"
1264
+ onClicked: root.installFromUrl(root.urlInput)
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ // 4. Confirm Uninstall Dialog
1272
+ Rectangle {
1273
+ anchors.fill: parent
1274
+ color: Qt.rgba(0, 0, 0, 0.7)
1275
+ visible: root.confirmUninstallOpen
1276
+
1277
+ MouseArea { anchors.fill: parent; onClicked: root.confirmUninstallOpen = false }
1278
+
1279
+ Rectangle {
1280
+ anchors.centerIn: parent
1281
+ width: 380
1282
+ height: 150
1283
+ radius: 10
1284
+ color: Color.popups ? Color.popups.background : "#1f2335"
1285
+ border.color: Qt.rgba(255, 255, 255, 0.15)
1286
+ border.width: 1
1287
+
1288
+ MouseArea { anchors.fill: parent }
1289
+
1290
+ ColumnLayout {
1291
+ anchors.fill: parent
1292
+ anchors.margins: 16
1293
+ spacing: 10
1294
+
1295
+ Text {
1296
+ text: "Uninstall Plugin"
1297
+ color: "#f7768e"
1298
+ font.pixelSize: 14
1299
+ font.bold: true
1300
+ }
1301
+
1302
+ Text {
1303
+ text: "Remove '" + (root.pendingUninstallPlugin ? (root.pendingUninstallPlugin.name || root.pendingUninstallPlugin.id) : "") + "'?"
1304
+ color: Color.foreground
1305
+ font.pixelSize: 11
1306
+ }
1307
+
1308
+ RowLayout {
1309
+ Layout.fillWidth: true
1310
+ Item { Layout.fillWidth: true }
1311
+ Button {
1312
+ text: "Cancel"
1313
+ onClicked: root.confirmUninstallOpen = false
1314
+ }
1315
+ Button {
1316
+ text: "Remove"
1317
+ onClicked: root.executeUninstall()
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+}
0
1324
▼shell/plugins/plugin-manager/manifest.json
+23-0
1
+{
2
+ "schemaVersion": 1,
3
+ "id": "deomarchy.plugin-manager",
4
+ "name": "Plugin Manager",
5
+ "version": "1.0.0",
6
+ "author": "de-omarchy",
7
+ "description": "Manage installed plugins and discover 1,000+ community extensions from OmarchyPlugins.com",
8
+ "kinds": [
9
+ "bar-widget",
10
+ "panel"
11
+ ],
12
+ "entryPoints": {
13
+ "barWidget": "BarWidget.qml",
14
+ "panel": "Panel.qml"
15
+ },
16
+ "barWidget": {
17
+ "displayName": "Plugin Manager",
18
+ "description": "Enable/disable, update, install, and remove plugins",
19
+ "category": "System",
20
+ "allowMultiple": false,
21
+ "defaultSection": "right"
22
+ }
23
+}
0
24