Skip to content

Commit 5905d3b

Browse files
committed
feat(plugins): menu.parent — inject plugin entries into built-in submenus
- plugins.sh: loader reads menu.parent (enum) | menu.section (mutually exclusive); registry record becomes target\tentry\tfunc with p:/s: discriminator (single non-empty field avoids read collapsing empty tab fields); add _menu_selectable_count + _plugin_attach_builtin helpers - tweaks/apps/customize/security/cleanup menu.sh: append plugins targeting that submenu below the built-ins and dispatch by count; tweaks/cleanup array-ified - macrift.sh: main_menu groups only top-level (s:) records, skips p: ones - schemas/plugin.schema.json: add menu.parent enum + oneOf(section,parent) - vendor/wallpaper-links: migrate section:Customize → parent:customize (+ macrift_min 26.06), fixing the duplicate-Customize collision - PLUGINS.md: document section vs parent placement - tests/run.sh: bump harness MACRIFT_VERSION to 26.06; update registry-format asserts; add menu.parent loader + helper coverage
1 parent 52ef84b commit 5905d3b

12 files changed

Lines changed: 249 additions & 42 deletions

File tree

PLUGINS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,16 @@ but enables editor auto-completion in VS Code / Zed / IntelliJ.
7979
| `compat.macrift_min` | yes | Minimum macrift calver (`YY.MM`). |
8080
| `compat.macrift_api` | yes | Public-API major version (currently `1`). |
8181
| `compat.macos_min` | optional | Minimum macOS release (`13.0`, `14.0`, …). |
82-
| `menu.section` | yes | Where in the main menu the entry appears. Reuse an existing section or create a new one. |
82+
| `menu.section` | one of section/parent | Creates (or reuses) a **top-level** main-menu section the entry appears under. |
83+
| `menu.parent` | one of section/parent | Injects the entry **into** a built-in submenu instead: one of `tweaks`, `apps`, `customize`, `security`, `cleanup`. Requires `macrift_min ≥ 26.06`. |
8384
| `menu.entry` | yes | The label as shown. Append `` if it opens a submenu. |
8485
| `menu.function` | yes | Bash function defined in `menu.sh` — macrift calls this when the user selects the entry. |
86+
87+
Set **exactly one** of `menu.section` or `menu.parent`. Use `section` to add your
88+
own top-level entry to the main menu; use `parent` to land inside an existing
89+
built-in submenu (e.g. `parent: "customize"` puts your entry at the bottom of the
90+
**Customize** menu). Plugins using `parent` must set `compat.macrift_min` to `26.06`
91+
or later — older macrift builds don't understand it and will skip the plugin.
8592
| `lifecycle.on_install` | optional | Script run once when the plugin is installed. |
8693
| `lifecycle.on_remove` | optional | Script run once when the plugin is removed (before journal undo). |
8794

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
26.05.3
1+
26.06

apps/menu.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ apps_menu() {
4646
if ! xcode-select -p &>/dev/null; then
4747
items+=("---" "Xcode Command Line Tools")
4848
fi
49+
50+
# Plugins targeting menu.parent=apps append below the built-ins.
51+
local _nb; _nb=$(_menu_selectable_count items)
52+
local -a _pf=()
53+
_plugin_attach_builtin apps items _pf
4954
items+=("Back")
5055

5156
local choice
5257
choice=$(show_menu "Apps & Packages" "${items[@]}")
5358

59+
if (( choice > _nb )); then
60+
"${_pf[$((choice - _nb - 1))]}" || true
61+
continue
62+
fi
5463
case "$choice" in
5564
1) source "$MACRIFT_DIR/apps/brew.sh" && brew_menu ;;
5665
2) source "$MACRIFT_DIR/apps/appstore.sh" && appstore_menu ;;

cleanup/menu.sh

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,24 @@ cleanup_menu() {
1010
clear
1111

1212

13+
local -a items=(
14+
"Homebrew Cleanup"
15+
"Deep Clean (Mole)"
16+
)
17+
18+
# Plugins targeting menu.parent=cleanup append below the built-ins.
19+
local _nb; _nb=$(_menu_selectable_count items)
20+
local -a _pf=()
21+
_plugin_attach_builtin cleanup items _pf
22+
items+=("Back")
23+
1324
local choice
14-
choice=$(show_menu "Cleanup" \
15-
"Homebrew Cleanup" \
16-
"Deep Clean (Mole)" \
17-
"Back")
25+
choice=$(show_menu "Cleanup" "${items[@]}")
1826

27+
if (( choice > _nb )); then
28+
"${_pf[$((choice - _nb - 1))]}" || true
29+
continue
30+
fi
1931
case "$choice" in
2032
1) run_brew_cleanup ;;
2133
2) run_mole_cleanup || true ;; # guard set -e: interactive mole may exit non-zero

customize/menu.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,20 @@ customize_menu() {
3131
"Dock Layout ›"
3232
)
3333
$is_tahoe || items+=("Launchpad ›")
34+
35+
# Plugins targeting menu.parent=customize append below the built-ins.
36+
local _nb; _nb=$(_menu_selectable_count items)
37+
local -a _pf=()
38+
_plugin_attach_builtin customize items _pf
3439
items+=("Back")
3540

3641
local choice
3742
choice=$(show_menu "Customize" "${items[@]}")
3843

44+
if (( choice > _nb )); then
45+
"${_pf[$((choice - _nb - 1))]}" || true
46+
continue
47+
fi
3948
case "$choice" in
4049
1) source "$MACRIFT_DIR/customize/profile.sh" && profile_menu ;;
4150
2) source "$MACRIFT_DIR/customize/terminal.sh" && terminal_menu ;;

macrift.sh

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -210,27 +210,34 @@ main_menu() {
210210
local -a actions=(tweaks apps customize security cleanup)
211211

212212
if (( ${#MACRIFT_PLUGIN_REGISTRY[@]} > 0 )); then
213-
# Group plugin entries by section preserving first-seen order.
213+
# Group top-level plugin entries by section (first-seen order).
214+
# Records with a non-empty parent inject into a built-in submenu
215+
# instead (handled by the submenu functions), so skip them here.
214216
local -A _seen=()
215217
local -a _sections=()
216-
local rec _s _e _f
218+
local rec _target _s _e _f
217219
for rec in "${MACRIFT_PLUGIN_REGISTRY[@]}"; do
218-
IFS=$'\t' read -r _s _e _f <<<"$rec"
220+
IFS=$'\t' read -r _target _e _f <<<"$rec"
221+
[[ "$_target" == p:* ]] && continue # injected into a submenu, not root
222+
_s="${_target#s:}"
219223
if [[ -z "${_seen[$_s]+x}" ]]; then
220224
_sections+=("$_s")
221225
_seen[$_s]=1
222226
fi
223227
done
224-
items+=("---")
225-
for _s in "${_sections[@]}"; do
226-
items+=("## $_s")
227-
for rec in "${MACRIFT_PLUGIN_REGISTRY[@]}"; do
228-
IFS=$'\t' read -r section entry func <<<"$rec"
229-
[[ "$section" == "$_s" ]] || continue
230-
items+=("$entry")
231-
actions+=("plugin:$func")
228+
if (( ${#_sections[@]} > 0 )); then
229+
items+=("---")
230+
for _s in "${_sections[@]}"; do
231+
items+=("## $_s")
232+
for rec in "${MACRIFT_PLUGIN_REGISTRY[@]}"; do
233+
IFS=$'\t' read -r _target entry func <<<"$rec"
234+
[[ "$_target" == p:* ]] && continue
235+
[[ "${_target#s:}" == "$_s" ]] || continue
236+
items+=("$entry")
237+
actions+=("plugin:$func")
238+
done
232239
done
233-
done
240+
fi
234241
fi
235242

236243
items+=("---" "Manage Plugins ›" "$update_label" "Exit")

plugins.sh

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,13 @@ _plugin_compat_ok() {
100100
}
101101

102102
# Plugin registry — populated by _plugin_load_all at startup. Each entry is
103-
# tab-separated: section \t entry \t function
104-
# Read by macrift.sh's main_menu to inject plugin items.
103+
# tab-separated: target \t entry \t function
104+
# `target` carries a one-char discriminator prefix: `p:<slug>` injects INTO a
105+
# built-in submenu (tweaks/apps/customize/security/cleanup); `s:<name>` is a
106+
# top-level section. (A single non-empty field, rather than separate parent/
107+
# section columns, avoids `read` collapsing an empty tab-delimited field — tab
108+
# is IFS-whitespace.) Read by macrift.sh's main_menu (s:) and the submenu
109+
# functions via _plugin_attach_builtin (p:).
105110
MACRIFT_PLUGIN_REGISTRY=()
106111

107112
# Discover, compat-check, source, and register every plugin under
@@ -110,17 +115,31 @@ MACRIFT_PLUGIN_REGISTRY=()
110115
# Plugin failures emit log_warn but never abort the startup of macrift itself.
111116
_plugin_load_all() {
112117
MACRIFT_PLUGIN_REGISTRY=()
113-
local dir name section entry func
118+
local dir name parent section entry func target
114119
while IFS= read -r dir; do
115120
[[ -z "$dir" ]] && continue
116121
_plugin_compat_ok "$dir" || continue
117122

118123
# name is already validated by _plugin_compat_ok; safe to read.
119124
name=$(_plugin_field "$dir" .name)
120-
section=$(_plugin_field "$dir" .menu.section) || {
121-
log_warn "Plugin $name: missing menu.section — skipping"
125+
126+
# menu.parent (inject into a built-in submenu) and menu.section (new
127+
# top-level grouping) are optional but mutually exclusive — exactly one.
128+
parent=$(_plugin_field "$dir" .menu.parent || true)
129+
section=$(_plugin_field "$dir" .menu.section || true)
130+
if [[ -n "$parent" && -n "$section" ]]; then
131+
log_warn "Plugin $name: menu.parent and menu.section are mutually exclusive — skipping"
122132
continue
123-
}
133+
fi
134+
if [[ -n "$parent" ]]; then
135+
case "$parent" in
136+
tweaks|apps|customize|security|cleanup) ;;
137+
*) log_warn "Plugin $name: menu.parent '$parent' is not a built-in submenu — skipping"; continue ;;
138+
esac
139+
elif [[ -z "$section" ]]; then
140+
log_warn "Plugin $name: needs menu.parent or menu.section — skipping"
141+
continue
142+
fi
124143
entry=$(_plugin_field "$dir" .menu.entry) || {
125144
log_warn "Plugin $name: missing menu.entry — skipping"
126145
continue
@@ -144,10 +163,44 @@ _plugin_load_all() {
144163
continue
145164
fi
146165

147-
MACRIFT_PLUGIN_REGISTRY+=("$section"$'\t'"$entry"$'\t'"$func")
166+
if [[ -n "$parent" ]]; then target="p:$parent"; else target="s:$section"; fi
167+
MACRIFT_PLUGIN_REGISTRY+=("$target"$'\t'"$entry"$'\t'"$func")
148168
done < <(_plugin_discover)
149169
}
150170

171+
# Count selectable (non-`---`, non-`## `) entries in a menu items array (nameref).
172+
# Used by built-in submenus to know how many of their own rows precede any
173+
# injected plugin entries, so the plugin dispatch index is correct.
174+
_menu_selectable_count() {
175+
local -n _arr="$1"
176+
local n=0 it
177+
for it in "${_arr[@]}"; do
178+
[[ "$it" == "---" || "$it" == "## "* ]] && continue
179+
n=$((n + 1))
180+
done
181+
printf '%s' "$n"
182+
}
183+
184+
# Append plugin entries targeting built-in submenu <slug> to an items array
185+
# (nameref $2), recording their handler functions in a parallel funcs array
186+
# (nameref $3). Prepends one `---` divider when at least one entry is added.
187+
# No-op (funcs left empty) when no plugin targets <slug>.
188+
_plugin_attach_builtin() {
189+
local slug="$1"
190+
local -n _items="$2"
191+
local -n _funcs="$3"
192+
_funcs=()
193+
local rec target entry func added=0
194+
for rec in "${MACRIFT_PLUGIN_REGISTRY[@]}"; do
195+
IFS=$'\t' read -r target entry func <<<"$rec"
196+
[[ "$target" == "p:$slug" ]] || continue
197+
(( added == 0 )) && _items+=("---")
198+
_items+=("$entry")
199+
_funcs+=("$func")
200+
added=1
201+
done
202+
}
203+
151204
# Reproducibility lockfile — records the exact source / ref / commit / install
152205
# time for every plugin so `plugin restore` (future) can rehydrate on another
153206
# machine. Lives at $HOME/.macrift/plugins.lock.json.

schemas/plugin.schema.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,22 @@
6969

7070
"menu": {
7171
"type": "object",
72-
"required": ["section", "entry", "function"],
72+
"required": ["entry", "function"],
7373
"additionalProperties": false,
74+
"oneOf": [
75+
{ "required": ["section"] },
76+
{ "required": ["parent"] }
77+
],
7478
"properties": {
7579
"section": {
7680
"type": "string",
7781
"minLength": 1,
78-
"description": "Main-menu section the entry appears under. Reuse an existing section or create a new one."
82+
"description": "Top-level main-menu section the entry appears under. Reuse an existing section or create a new one. Mutually exclusive with 'parent'."
83+
},
84+
"parent": {
85+
"type": "string",
86+
"enum": ["tweaks", "apps", "customize", "security", "cleanup"],
87+
"description": "Inject the entry INTO a built-in submenu instead of creating a top-level section. Mutually exclusive with 'section'. Requires macrift ≥ 26.06."
7988
},
8089
"entry": {
8190
"type": "string",

security/menu.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ privacy_menu() {
1111

1212
local items=("Security Status" "Privacy Shortcuts ›" "Hostname" "DNS ›" "Update Control ›" "Unquarantine App")
1313
[[ -d "/Applications/Microsoft Defender Shim.app" ]] && items+=("Remove Microsoft Defender")
14+
15+
# Plugins targeting menu.parent=security append below the built-ins.
16+
local _nb; _nb=$(_menu_selectable_count items)
17+
local -a _pf=()
18+
_plugin_attach_builtin security items _pf
1419
items+=("Back")
1520

1621
local choice
1722
choice=$(show_menu "Privacy & Security" "${items[@]}")
1823

24+
if (( choice > _nb )); then
25+
"${_pf[$((choice - _nb - 1))]}" || true
26+
continue
27+
fi
1928
case "$choice" in
2029
1) show_security_status ;;
2130
2) privacy_shortcuts_menu ;;

0 commit comments

Comments
 (0)