Widen the Omarchy menu via a cloned plugin
Claude-Session: https://claude.ai/code/session_014ADhvRXuiTvrXSbConxUey
This commit is contained in:
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
-- Monitor 1 - C24 curved, DisplayPort via DP->HDMI adapter.
|
-- Monitor 1 - C24 curved, DisplayPort via DP->HDMI adapter.
|
||||||
-- NOTE: the DP2HDMI adapter caps this panel at 1920x1080@60 (no 75Hz mode).
|
-- NOTE: the DP2HDMI adapter caps this panel at 1920x1080@60 (no 75Hz mode).
|
||||||
|
-- Both externals stay at scale 1. Hyprland's fractional scaling works in
|
||||||
|
-- 1/120 steps and needs whole pixels on both axes, so 1.0 and 1.2 are
|
||||||
|
-- adjacent valid scales on a 1920x1080 panel - nothing sits between them.
|
||||||
|
-- Apparent size is tuned with `omarchy display text size` instead.
|
||||||
hl.monitor({ output = "DP-1", mode = "1920x1080@60", position = "0x0", scale = 1 })
|
hl.monitor({ output = "DP-1", mode = "1920x1080@60", position = "0x0", scale = 1 })
|
||||||
|
|
||||||
-- Monitor 2 - Sceptre F24, HDMI, native 75Hz.
|
-- Monitor 2 - Sceptre F24, HDMI, native 75Hz.
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ check_file "$SCRIPT_DIR/hypr/xdph.conf" "$HOME_DIR/.config/hypr/xdph.conf" "hypr
|
|||||||
check_file "$SCRIPT_DIR/omarchy/shell.json" "$HOME_DIR/.config/omarchy/shell.json" "omarchy/shell.json" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/shell.json" "$HOME_DIR/.config/omarchy/shell.json" "omarchy/shell.json" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/extensions/omarchy-menu.jsonc" "$HOME_DIR/.config/omarchy/extensions/omarchy-menu.jsonc" "omarchy/extensions/omarchy-menu.jsonc" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/extensions/omarchy-menu.jsonc" "$HOME_DIR/.config/omarchy/extensions/omarchy-menu.jsonc" "omarchy/extensions/omarchy-menu.jsonc" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/plugins/blob.workspaces/Workspaces.qml" "$HOME_DIR/.config/omarchy/plugins/blob.workspaces/Workspaces.qml" "omarchy/plugins/blob.workspaces" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/plugins/blob.workspaces/Workspaces.qml" "$HOME_DIR/.config/omarchy/plugins/blob.workspaces/Workspaces.qml" "omarchy/plugins/blob.workspaces" || check_status=1
|
||||||
|
check_file "$SCRIPT_DIR/omarchy/plugins/blob.menu/Menu.qml" "$HOME_DIR/.config/omarchy/plugins/blob.menu/Menu.qml" "omarchy/plugins/blob.menu" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/hooks/theme-set" "$HOME_DIR/.config/omarchy/hooks/theme-set" "omarchy/hooks/theme-set" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/hooks/theme-set" "$HOME_DIR/.config/omarchy/hooks/theme-set" "omarchy/hooks/theme-set" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/themed/zen.css.tpl" "$HOME_DIR/.config/omarchy/themed/zen.css.tpl" "omarchy/themed/zen.css.tpl" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/themed/zen.css.tpl" "$HOME_DIR/.config/omarchy/themed/zen.css.tpl" "omarchy/themed/zen.css.tpl" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts" || check_status=1
|
check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts" || check_status=1
|
||||||
|
|||||||
+14
-5
@@ -53,11 +53,20 @@ A command module with no `exec` key is a static icon; add `exec` and
|
|||||||
|
|
||||||
## Cloned plugins
|
## Cloned plugins
|
||||||
|
|
||||||
`plugins/blob.workspaces/` is a clone of `omarchy.workspaces`, made with
|
Both were made with `omarchy plugin clone`, which copies a built-in plugin,
|
||||||
`omarchy plugin clone`. The stock widget hardcodes workspaces 1-5 as always
|
disables the original, and points the bar at the copy - hence the `blob.` ids
|
||||||
visible and has no setting for it, so the clone changes that list to 1-9 to
|
in `shell.json`.
|
||||||
match the old waybar `persistent-workspaces`. Cloning also switches the bar
|
|
||||||
to the cloned id, which is why `shell.json` says `blob.workspaces`.
|
`plugins/blob.workspaces/` clones `omarchy.workspaces`. The stock widget
|
||||||
|
hardcodes workspaces 1-5 as always visible and has no setting for it, so the
|
||||||
|
clone changes that list to 1-9 to match the old waybar `persistent-workspaces`.
|
||||||
|
|
||||||
|
`plugins/blob.menu/` clones `omarchy.menu` to widen it. `cardWidth` in
|
||||||
|
`Menu.qml` is hardcoded at `Style.space(300)`; the clone raises it to 440, so
|
||||||
|
the apps menu (Super + Space) and the root menu (Super + Alt + Space) are both
|
||||||
|
wider. The two oversized menus (screen recording, font picker) keep their own
|
||||||
|
520 and are untouched. `omarchy-menu` still targets `omarchy.menu` on the CLI -
|
||||||
|
the manifest records `clonedFrom`, and the shell routes those calls here.
|
||||||
|
|
||||||
Re-clone after an Omarchy update if the upstream widget gains something worth
|
Re-clone after an Omarchy update if the upstream widget gains something worth
|
||||||
picking up: `omarchy plugin clone omarchy.workspaces`, then re-apply the
|
picking up: `omarchy plugin clone omarchy.workspaces`, then re-apply the
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
BarWidget {
|
||||||
|
id: root
|
||||||
|
moduleName: "omarchy.menu"
|
||||||
|
|
||||||
|
implicitWidth: button.implicitWidth
|
||||||
|
implicitHeight: button.implicitHeight
|
||||||
|
|
||||||
|
WidgetButton {
|
||||||
|
id: button
|
||||||
|
anchors.fill: parent
|
||||||
|
bar: root.bar
|
||||||
|
text: "\ue900"
|
||||||
|
fontFamily: "omarchy"
|
||||||
|
horizontalMargin: 7.5
|
||||||
|
onPressed: function(button) {
|
||||||
|
if (!root.bar) return
|
||||||
|
if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec")
|
||||||
|
else root.bar.run("omarchy-shell shell toggle omarchy.menu '{\"menu\":\"root\"}'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,510 @@
|
|||||||
|
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 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,
|
||||||
|
title: value.title || "",
|
||||||
|
target: value.target || "",
|
||||||
|
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", title: "", target: "", 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both merges below return fresh items/itemOrder objects for the caller to
|
||||||
|
// assign in one go. They must never write into the maps they are handed: those
|
||||||
|
// live in QML `var` properties, and an in-place write into such an object is
|
||||||
|
// occasionally dropped by the engine — the key lands with an undefined value.
|
||||||
|
// A lost write used to leave an id in itemOrder with no item behind it, and
|
||||||
|
// the next merge then kept that orphan and appended a second row for the same
|
||||||
|
// app, so the launcher listed it twice (and again on every later rescan).
|
||||||
|
|
||||||
|
// Swaps every app row for the current set. Rows keep the order they arrive in;
|
||||||
|
// ids already claimed (including duplicate desktop ids) are listed once.
|
||||||
|
function mergeAppRows(items, itemOrder, appRows) {
|
||||||
|
var source = items || ({})
|
||||||
|
var order = Array.isArray(itemOrder) ? itemOrder : []
|
||||||
|
var rows = Array.isArray(appRows) ? appRows : []
|
||||||
|
var nextItems = ({})
|
||||||
|
var nextOrder = []
|
||||||
|
|
||||||
|
for (var i = 0; i < order.length; i++) {
|
||||||
|
var id = order[i]
|
||||||
|
var existing = source[id]
|
||||||
|
// Orphans (an id with no item) are dropped rather than carried forward,
|
||||||
|
// so a single lost write cannot compound into a duplicate row.
|
||||||
|
if (!existing || existing.kind === "app") continue
|
||||||
|
nextItems[id] = existing
|
||||||
|
nextOrder.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < rows.length; j++) {
|
||||||
|
var row = rows[j]
|
||||||
|
if (!row || !row.id || nextItems[row.id]) continue
|
||||||
|
row.order = nextOrder.length
|
||||||
|
nextItems[row.id] = row
|
||||||
|
nextOrder.push(row.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items: nextItems, itemOrder: nextOrder }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swaps the rows one provider contributed, leaving every other item untouched.
|
||||||
|
// Rows carry the id of the submenu that produced them, so a provider that runs
|
||||||
|
// again drops its previous batch — a plugin that was just enabled disappears
|
||||||
|
// from the Enable list — without disturbing static children declared in JSONC.
|
||||||
|
function swapProviderRows(items, itemOrder, menuId, rows) {
|
||||||
|
var source = items || ({})
|
||||||
|
var order = Array.isArray(itemOrder) ? itemOrder : []
|
||||||
|
var incoming = Array.isArray(rows) ? rows : []
|
||||||
|
var nextItems = ({})
|
||||||
|
var nextOrder = []
|
||||||
|
|
||||||
|
for (var i = 0; i < order.length; i++) {
|
||||||
|
var id = order[i]
|
||||||
|
var existing = source[id]
|
||||||
|
if (!existing || existing.providerMenu === menuId) continue
|
||||||
|
nextItems[id] = existing
|
||||||
|
nextOrder.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < incoming.length; j++) {
|
||||||
|
var row = incoming[j]
|
||||||
|
if (!row || !row.id || nextItems[row.id]) continue
|
||||||
|
row.providerMenu = menuId
|
||||||
|
row.order = nextOrder.length
|
||||||
|
nextItems[row.id] = row
|
||||||
|
nextOrder.push(row.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items: nextItems, itemOrder: nextOrder }
|
||||||
|
}
|
||||||
|
|
||||||
|
function item(items, id) {
|
||||||
|
return items && items[id] ? items[id] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes may name a real id (`system`, `setup.power`) or an alias declared in
|
||||||
|
// JSONC (`power-menu`, `settings`). An exact id beats any alias, and app rows
|
||||||
|
// are never routable: their aliases carry .desktop Keywords and GenericName
|
||||||
|
// for search, so an installed application could otherwise shadow a menu route
|
||||||
|
// (htop ships `Keywords=system;...`). Unknown strings fall through as the
|
||||||
|
// literal input so misspellings still attempt to open that id.
|
||||||
|
function resolveRoute(items, itemOrder, input) {
|
||||||
|
var raw = String(input || "").toLowerCase().replace(/_/g, "-")
|
||||||
|
if (!raw || raw === "go" || raw === "menu") return "root"
|
||||||
|
if (item(items, raw)) return raw
|
||||||
|
var order = Array.isArray(itemOrder) ? itemOrder : []
|
||||||
|
for (var i = 0; i < order.length; i++) {
|
||||||
|
var entry = item(items, order[i])
|
||||||
|
if (!entry || entry.kind === "app" || !entry.aliases) continue
|
||||||
|
for (var j = 0; j < entry.aliases.length; j++) {
|
||||||
|
var alias = String(entry.aliases[j] || "").toLowerCase().replace(/_/g, "-")
|
||||||
|
if (alias === raw) return entry.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
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 descriptionTextMatches(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 descriptionText = String(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], descriptionText)) 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 descriptionText = String(entry.description || "").toLowerCase()
|
||||||
|
var score = 80
|
||||||
|
|
||||||
|
if (label === needle) score = entry.parent === "root" ? 2 : 0
|
||||||
|
// An installed app whose name contains the query as a whole word ("zen"
|
||||||
|
// for Zen Browser) beats exact-labeled menu entries like Install > Zen.
|
||||||
|
else if (entry.kind === "app" && label.split(/\s+/).indexOf(needle) >= 0) score = 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 (descriptionTextMatches(needle, descriptionText)) score = 60
|
||||||
|
|
||||||
|
if (entry.kind === "menu" || entry.kind === "link") score -= 2
|
||||||
|
// App rows sort after all menu items, so they lose the tiebreak below to an
|
||||||
|
// equal match. Outrank those, but stay inside the tier so better ones win.
|
||||||
|
if (entry.kind === "app") score -= 5
|
||||||
|
|
||||||
|
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 || "",
|
||||||
|
appIcon: entry.appIcon || "",
|
||||||
|
appId: entry.appId || "",
|
||||||
|
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 || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands a `checked:` expression reads a value out of. Every sibling row
|
||||||
|
// asks the same one -- Defaults > Browser has seven rows all comparing
|
||||||
|
// against `omarchy-default-browser` -- so the batch runs it once and the rows
|
||||||
|
// read the captured answer.
|
||||||
|
//
|
||||||
|
// The capture has to be eager. These are read inside `$(...)`, and a value
|
||||||
|
// cached while one expression runs lives in that subshell only, so a lazy
|
||||||
|
// memo never survives to the expression after it.
|
||||||
|
var GUARD_READERS = [
|
||||||
|
"omarchy-channel-current",
|
||||||
|
"omarchy-default-agent",
|
||||||
|
"omarchy-default-browser",
|
||||||
|
"omarchy-default-editor",
|
||||||
|
"omarchy-default-terminal",
|
||||||
|
"omarchy-dns"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Package and command presence account for most of what the guards ask, and
|
||||||
|
// asked one at a time they are almost all fork: the shipped menu spends over
|
||||||
|
// a second on them. Answer them inside the guard process instead. These
|
||||||
|
// shadow the real commands for the batch only, so they have to agree with
|
||||||
|
// them everywhere, including for no arguments at all (present is true of
|
||||||
|
// nothing, missing is not).
|
||||||
|
//
|
||||||
|
// `pacman -Q` resolves a name through what installed packages provide, not
|
||||||
|
// just what they are called -- with gvim installed it reports `vim` as
|
||||||
|
// present -- so the set has to carry provides too, or `install.editor.vim`
|
||||||
|
// comes back and offers to install what is already there. A version
|
||||||
|
// constraint (`bash>=1`) is not a name any set can answer, so it goes to
|
||||||
|
// pacman itself; no shipped guard writes one.
|
||||||
|
//
|
||||||
|
// `pacman -Qi` wraps a long list across continuation lines whenever COLUMNS
|
||||||
|
// is set in the environment, which a login shell may well have done, so the
|
||||||
|
// parser follows the indented lines rather than reading the first one and
|
||||||
|
// dropping half of what is installed.
|
||||||
|
function guardHelpers() {
|
||||||
|
return 'declare -A __omarchy_pkgs=()\n'
|
||||||
|
+ 'mapfile -t __omarchy_pkg_names < <({ pacman -Qq; LC_ALL=C pacman -Qi'
|
||||||
|
+ " | awk '/^[A-Za-z]/ { provides = ($0 ~ /^Provides/); sub(/^[^:]*: /, \"\") }"
|
||||||
|
+ ' provides && $0 != "None" { n = split($0, p, " ");'
|
||||||
|
+ ' for (i = 1; i <= n; i++) { sub(/[<>=].*/, "", p[i]); print p[i] } }\'; } 2>/dev/null)\n'
|
||||||
|
+ 'for __omarchy_pkg in "${__omarchy_pkg_names[@]}"; do __omarchy_pkgs[$__omarchy_pkg]=1; done\n'
|
||||||
|
+ '__omarchy_pkg_has() { [[ -n ${__omarchy_pkgs[$1]-} ]] && return 0; '
|
||||||
|
+ '[[ $1 == *[\\<\\>=]* ]] && { pacman -Q "$1" &>/dev/null; return; }; return 1; }\n'
|
||||||
|
+ 'omarchy-pkg-present() { local p; for p in "$@"; do __omarchy_pkg_has "$p" || return 1; done; return 0; }\n'
|
||||||
|
+ 'omarchy-pkg-missing() { local p; for p in "$@"; do __omarchy_pkg_has "$p" || return 0; done; return 1; }\n'
|
||||||
|
+ 'omarchy-cmd-present() { local c; for c in "$@"; do command -v "$c" &>/dev/null || return 1; done; return 0; }\n'
|
||||||
|
+ 'omarchy-cmd-missing() { local c; for c in "$@"; do command -v "$c" &>/dev/null || return 0; done; return 1; }\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute the captured answer into the expression rather than shadowing
|
||||||
|
// the reader with a function. `$(reader)` and the variable holding what it
|
||||||
|
// printed are interchangeable -- both strip trailing newlines, both split the
|
||||||
|
// same way unquoted -- while a function would also catch `command -v reader`,
|
||||||
|
// `VAR=x reader`, and every other form, and answer those wrong. Anything but
|
||||||
|
// the plain substitution is left alone to run the real command.
|
||||||
|
function guardPrelude(guards) {
|
||||||
|
var prelude = guardHelpers()
|
||||||
|
|
||||||
|
for (var i = 0; i < GUARD_READERS.length; i++) {
|
||||||
|
// The guards arrive already substituted, so what marks a reader as wanted
|
||||||
|
// is the slot standing in for it, not the call it replaced.
|
||||||
|
if (guards.indexOf(guardReaderSlot(i)) < 0) continue
|
||||||
|
// `|| :` so a reader that exits nonzero cannot take the batch down with
|
||||||
|
// it under a login shell that turned on errexit.
|
||||||
|
prelude += "__omarchy_read_" + i + "=$(" + GUARD_READERS[i] + " 2>/dev/null) || :\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return prelude
|
||||||
|
}
|
||||||
|
|
||||||
|
function guardReaderSlot(index) {
|
||||||
|
return "${__omarchy_read_" + index + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteGuardReaders(expression) {
|
||||||
|
for (var i = 0; i < GUARD_READERS.length; i++)
|
||||||
|
expression = expression.split("$(" + GUARD_READERS[i] + ")").join(guardReaderSlot(i))
|
||||||
|
|
||||||
|
return expression
|
||||||
|
}
|
||||||
|
|
||||||
|
function guardLine(id, tag, expression) {
|
||||||
|
return "if { " + substituteGuardReaders(expression) + "; } >/dev/null 2>&1; then echo "
|
||||||
|
+ id + ":" + tag + ":1; else echo " + id + ":" + tag + ":0; fi\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// One bash script for every `when:` and `checked:` in the menu, reporting
|
||||||
|
// `<id>:<w|c>:<0|1>` per line. Speed is the whole point: the menu opens on
|
||||||
|
// the last evaluation's answers, so however long this takes is how long a row
|
||||||
|
// can contradict the state it describes.
|
||||||
|
function guardScript(items) {
|
||||||
|
var guards = ""
|
||||||
|
var ids = Object.keys(items || {})
|
||||||
|
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
var entry = items[ids[i]]
|
||||||
|
if (!entry) continue
|
||||||
|
if (entry.when) guards += guardLine(ids[i], "w", entry.when)
|
||||||
|
if (entry.checked) guards += guardLine(ids[i], "c", entry.checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
return guards ? guardPrelude(guards) + guards : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = {
|
||||||
|
guardReaders: GUARD_READERS,
|
||||||
|
guardScript: guardScript,
|
||||||
|
stripJsonc: stripJsonc,
|
||||||
|
normalizeAliases: normalizeAliases,
|
||||||
|
normalizeItem: normalizeItem,
|
||||||
|
parseMenuJsonc: parseMenuJsonc,
|
||||||
|
mergeMenuSources: mergeMenuSources,
|
||||||
|
mergeAppRows: mergeAppRows,
|
||||||
|
swapProviderRows: swapProviderRows,
|
||||||
|
item: item,
|
||||||
|
resolveRoute: resolveRoute,
|
||||||
|
slugify: slugify,
|
||||||
|
depthFor: depthFor,
|
||||||
|
pathFor: pathFor,
|
||||||
|
parentPathFor: parentPathFor,
|
||||||
|
isDescendantOf: isDescendantOf,
|
||||||
|
childCount: childCount,
|
||||||
|
isVisible: isVisible,
|
||||||
|
labelFor: labelFor,
|
||||||
|
searchableToken: searchableToken,
|
||||||
|
leafIdFor: leafIdFor,
|
||||||
|
nameSearchText: nameSearchText,
|
||||||
|
termInSearchWords: termInSearchWords,
|
||||||
|
descriptionTextMatches: descriptionTextMatches,
|
||||||
|
matchesQuery: matchesQuery,
|
||||||
|
searchScore: searchScore,
|
||||||
|
displayRow: displayRow
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "blob.menu",
|
||||||
|
"name": "My Omarchy menu",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "Quickshell-powered Omarchy command menu",
|
||||||
|
"kinds": [
|
||||||
|
"menu",
|
||||||
|
"bar-widget"
|
||||||
|
],
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": {
|
||||||
|
"menu": "Menu.qml",
|
||||||
|
"barWidget": "BarWidget.qml"
|
||||||
|
},
|
||||||
|
"barWidget": {
|
||||||
|
"displayName": "My Omarchy menu",
|
||||||
|
"description": "Launches the Omarchy menu",
|
||||||
|
"category": "Compositor",
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
|
"omarchy": {
|
||||||
|
"clonedFrom": "omarchy.menu"
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -11,7 +11,7 @@
|
|||||||
"layout": {
|
"layout": {
|
||||||
"left": [
|
"left": [
|
||||||
{
|
{
|
||||||
"id": "omarchy.menu"
|
"id": "blob.menu"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "blob.workspaces"
|
"id": "blob.workspaces"
|
||||||
@@ -73,5 +73,11 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": []
|
"plugins": [],
|
||||||
|
"disabledPlugins": [
|
||||||
|
"omarchy.menu"
|
||||||
|
],
|
||||||
|
"cloneSourceRestores": [
|
||||||
|
"blob.menu"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user