Static submenus and links now inherit visibility from their descendants, so hardware sections like Touchpad Haptics disappear on machines without the hardware instead of opening empty. Laptop Display and Mirror Display are guarded by omarchy-hw-laptop. The guard evaluator brace-wraps every when/checked condition before silencing it, so conditions in the jsonc no longer need their own >/dev/null plumbing, and parents no longer need to repeat their children's guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
316 lines
8.8 KiB
JavaScript
316 lines
8.8 KiB
JavaScript
function stripJsonc(raw) {
|
||
return String(raw || "")
|
||
.replace(/^\s*\/\/[^\n]*(\n|$)/gm, "")
|
||
.replace(/,(\s*[}\]])/g, "$1")
|
||
}
|
||
|
||
function normalizeAliases(value) {
|
||
if (Array.isArray(value)) return value.filter(function(v) { return v })
|
||
if (typeof value === "string" && value) return [value]
|
||
return []
|
||
}
|
||
|
||
function normalizeKeywords(id, aliases, raw) {
|
||
var parts = String(raw || "").split(/\s+/)
|
||
var seen = {}
|
||
var out = []
|
||
for (var i = 0; i < parts.length; i++) {
|
||
var p = parts[i]
|
||
if (!p || seen[p]) continue
|
||
seen[p] = true
|
||
out.push(p)
|
||
}
|
||
return out.join(" ")
|
||
}
|
||
|
||
function normalizeItem(id, raw) {
|
||
var value = raw || {}
|
||
var aliases = normalizeAliases(value.aliases)
|
||
var parent = value.parent
|
||
if (parent === undefined)
|
||
parent = id.indexOf(".") >= 0 ? id.split(".").slice(0, -1).join(".") : "root"
|
||
if (id === "root") parent = ""
|
||
|
||
var kind = value.action ? "action" : (value.target ? "link" : "menu")
|
||
|
||
return {
|
||
id: id,
|
||
parent: parent,
|
||
kind: kind,
|
||
icon: value.icon || "",
|
||
iconFont: value.iconFont || "",
|
||
label: value.label || id,
|
||
target: value.target || "",
|
||
keywords: normalizeKeywords(id, aliases, value.keywords),
|
||
description: value.description || "",
|
||
action: value.action || "",
|
||
provider: value.provider || "",
|
||
aliases: aliases,
|
||
when: value.when || "",
|
||
checked: value.checked || ""
|
||
}
|
||
}
|
||
|
||
function parseMenuJsonc(raw) {
|
||
var stripped = stripJsonc(raw)
|
||
if (!stripped.trim()) return []
|
||
|
||
var parsed
|
||
try {
|
||
parsed = JSON.parse(stripped)
|
||
} catch (e) {
|
||
return []
|
||
}
|
||
if (typeof parsed !== "object" || parsed === null) return []
|
||
|
||
var source = (parsed.items && typeof parsed.items === "object" && !Array.isArray(parsed.items))
|
||
? parsed.items
|
||
: parsed
|
||
var out = []
|
||
for (var id in source) {
|
||
var entry = source[id]
|
||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue
|
||
out.push(normalizeItem(id, entry))
|
||
}
|
||
return out
|
||
}
|
||
|
||
function mergeMenuSources(defaultItems, userItems) {
|
||
var nextItems = ({})
|
||
var nextOrder = []
|
||
var sources = [defaultItems || [], userItems || []]
|
||
|
||
for (var s = 0; s < sources.length; s++) {
|
||
var src = sources[s]
|
||
for (var i = 0; i < src.length; i++) {
|
||
var entry = src[i]
|
||
if (!entry || !entry.id) continue
|
||
if (!nextItems[entry.id]) nextOrder.push(entry.id)
|
||
var prior = nextItems[entry.id] || {}
|
||
var merged = {}
|
||
for (var k in prior) merged[k] = prior[k]
|
||
for (var k2 in entry) merged[k2] = entry[k2]
|
||
merged.id = entry.id
|
||
nextItems[entry.id] = merged
|
||
}
|
||
}
|
||
|
||
if (!nextItems.root) {
|
||
nextItems.root = { id: "root", parent: "", kind: "menu", icon: "", iconFont: "", label: "Go", target: "", keywords: "", description: "", aliases: [], when: "", checked: "", action: "", provider: "" }
|
||
nextOrder.unshift("root")
|
||
}
|
||
for (var k3 = 0; k3 < nextOrder.length; k3++) nextItems[nextOrder[k3]].order = k3
|
||
|
||
return {
|
||
items: nextItems,
|
||
itemOrder: nextOrder
|
||
}
|
||
}
|
||
|
||
function item(items, id) {
|
||
return items && items[id] ? items[id] : null
|
||
}
|
||
|
||
function slugify(value) {
|
||
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "item"
|
||
}
|
||
|
||
function depthFor(items, id) {
|
||
var depth = 0
|
||
var current = item(items, id)
|
||
var guard = 0
|
||
|
||
while (current && current.parent && current.parent !== "root" && guard < 32) {
|
||
depth += 1
|
||
current = item(items, current.parent)
|
||
guard += 1
|
||
}
|
||
|
||
return depth
|
||
}
|
||
|
||
function pathFor(items, id) {
|
||
var labels = []
|
||
var current = item(items, id)
|
||
var guard = 0
|
||
|
||
while (current && current.id !== "root" && guard < 32) {
|
||
labels.unshift(current.label)
|
||
current = item(items, current.parent)
|
||
guard += 1
|
||
}
|
||
|
||
return labels.join(" › ")
|
||
}
|
||
|
||
function parentPathFor(items, id) {
|
||
var entry = item(items, id)
|
||
if (!entry || !entry.parent || entry.parent === "root") return ""
|
||
return pathFor(items, entry.parent)
|
||
}
|
||
|
||
function isDescendantOf(items, id, ancestorId) {
|
||
if (ancestorId === "root") return id !== "root"
|
||
|
||
var current = item(items, id)
|
||
var guard = 0
|
||
while (current && current.parent && guard < 32) {
|
||
if (current.parent === ancestorId) return true
|
||
current = item(items, current.parent)
|
||
guard += 1
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
function childCount(items, itemOrder, id) {
|
||
var count = 0
|
||
var order = Array.isArray(itemOrder) ? itemOrder : []
|
||
for (var i = 0; i < order.length; i++) {
|
||
var entry = item(items, order[i])
|
||
if (entry && entry.parent === id) count += 1
|
||
}
|
||
return count
|
||
}
|
||
|
||
function isVisible(items, itemOrder, whenResults, entry, depth) {
|
||
if (!entry) return false
|
||
if (entry.when && whenResults && whenResults[entry.id] === false) return false
|
||
if (entry.kind !== "menu" && entry.kind !== "link") return true
|
||
if (entry.provider) return true
|
||
|
||
var guard = depth || 0
|
||
if (guard >= 32) return false
|
||
|
||
var target = entry.kind === "link" ? entry.target : entry.id
|
||
var order = Array.isArray(itemOrder) ? itemOrder : []
|
||
for (var i = 0; i < order.length; i++) {
|
||
var child = item(items, order[i])
|
||
if (child && child.parent === target && isVisible(items, itemOrder, whenResults, child, guard + 1)) return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
function labelFor(entry, checkedResults) {
|
||
if (!entry) return ""
|
||
if (entry.checked && checkedResults && checkedResults[entry.id]) return entry.label + " ✓"
|
||
return entry.label
|
||
}
|
||
|
||
function searchableToken(value) {
|
||
return String(value || "").replace(/[._-]+/g, " ")
|
||
}
|
||
|
||
function leafIdFor(id) {
|
||
var parts = String(id || "").split(".")
|
||
return parts.length > 0 ? parts[parts.length - 1] : id
|
||
}
|
||
|
||
function nameSearchText(entry) {
|
||
if (!entry) return ""
|
||
var aliases = []
|
||
var values = Array.isArray(entry.aliases) ? entry.aliases : []
|
||
for (var i = 0; i < values.length; i++) aliases.push(searchableToken(values[i]))
|
||
return [entry.label, searchableToken(leafIdFor(entry.id)), aliases.join(" ")].join(" ").toLowerCase()
|
||
}
|
||
|
||
function termInSearchWords(term, text) {
|
||
var words = String(text || "").toLowerCase().split(/\s+/)
|
||
for (var i = 0; i < words.length; i++) {
|
||
if (words[i] === term) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
function keywordTextMatches(query, text) {
|
||
var terms = String(query || "").toLowerCase().trim().split(/\s+/)
|
||
for (var i = 0; i < terms.length; i++) {
|
||
if (terms[i] && !termInSearchWords(terms[i], text)) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function matchesQuery(entry, query, visible) {
|
||
if (!entry || entry.id === "root") return false
|
||
if (!visible) return false
|
||
|
||
var nameText = nameSearchText(entry)
|
||
var keywordText = (entry.keywords + " " + entry.description).toLowerCase()
|
||
var terms = String(query || "").toLowerCase().trim().split(/\s+/)
|
||
|
||
for (var i = 0; i < terms.length; i++) {
|
||
if (!terms[i]) continue
|
||
if (nameText.indexOf(terms[i]) >= 0) continue
|
||
if (termInSearchWords(terms[i], keywordText)) continue
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
function searchScore(items, entry, query) {
|
||
var needle = String(query || "").toLowerCase().trim()
|
||
var label = entry.label.toLowerCase()
|
||
var nameText = nameSearchText(entry)
|
||
var keywordText = (entry.keywords + " " + entry.description).toLowerCase()
|
||
var score = 80
|
||
|
||
if (label === needle) score = entry.parent === "root" ? 2 : 0
|
||
else if (label.indexOf(needle) === 0) score = 10
|
||
else if (label.indexOf(needle) >= 0) score = 30
|
||
else if (nameText.indexOf(needle) >= 0) score = 40
|
||
else if (keywordTextMatches(needle, keywordText)) score = 60
|
||
|
||
if (entry.kind === "menu" || entry.kind === "link") score -= 2
|
||
|
||
return score * 1000 + depthFor(items, entry.id) * 25 + entry.order
|
||
}
|
||
|
||
function displayRow(items, itemOrder, checkedResults, entry, detail, score, section) {
|
||
var target = entry.kind === "link" ? entry.target : entry.id
|
||
return {
|
||
itemId: entry.id,
|
||
kind: entry.kind,
|
||
icon: entry.icon,
|
||
iconFont: entry.iconFont || "",
|
||
label: labelFor(entry, checkedResults),
|
||
target: target,
|
||
detail: detail || "",
|
||
path: pathFor(items, entry.id),
|
||
childCount: (entry.kind === "menu" || entry.kind === "link") ? childCount(items, itemOrder, target) : 0,
|
||
action: entry.action || "",
|
||
provider: entry.provider || "",
|
||
score: score || 0,
|
||
section: section || ""
|
||
}
|
||
}
|
||
|
||
if (typeof module !== "undefined") {
|
||
module.exports = {
|
||
stripJsonc: stripJsonc,
|
||
normalizeAliases: normalizeAliases,
|
||
normalizeKeywords: normalizeKeywords,
|
||
normalizeItem: normalizeItem,
|
||
parseMenuJsonc: parseMenuJsonc,
|
||
mergeMenuSources: mergeMenuSources,
|
||
item: item,
|
||
slugify: slugify,
|
||
depthFor: depthFor,
|
||
pathFor: pathFor,
|
||
parentPathFor: parentPathFor,
|
||
isDescendantOf: isDescendantOf,
|
||
childCount: childCount,
|
||
isVisible: isVisible,
|
||
labelFor: labelFor,
|
||
searchableToken: searchableToken,
|
||
leafIdFor: leafIdFor,
|
||
nameSearchText: nameSearchText,
|
||
termInSearchWords: termInSearchWords,
|
||
keywordTextMatches: keywordTextMatches,
|
||
matchesQuery: matchesQuery,
|
||
searchScore: searchScore,
|
||
displayRow: displayRow
|
||
}
|
||
}
|