diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index e220043f..c02313d8 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -14,13 +14,13 @@ // checked shell condition; append ✓ when it succeeds // Root Menu - "apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"keywords":"","action":"omarchy-shell shell summon omarchy.launcher"}, + "apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"action":"omarchy-shell shell summon omarchy.launcher"}, "learn": {"icon":"󰧑","label":"Learn","keywords":"manual docs help"}, - "trigger": {"icon":"󱓞","label":"Trigger","keywords":""}, - "style": {"icon":"","label":"Style","keywords":""}, - "setup": {"icon":"","label":"Setup","aliases":["settings"],"keywords":""}, - "install": {"icon":"󰉉","label":"Install","keywords":""}, - "remove": {"icon":"󰭌","label":"Remove","aliases":["uninstall"],"keywords":""}, + "trigger": {"icon":"󱓞","label":"Trigger"}, + "style": {"icon":"","label":"Style"}, + "setup": {"icon":"","label":"Setup","aliases":["settings"]}, + "install": {"icon":"󰉉","label":"Install"}, + "remove": {"icon":"󰭌","label":"Remove","aliases":["uninstall"]}, "update": {"icon":"","label":"Update","aliases":["restart","refresh"],"keywords":"upgrade channel firmware timezone"}, "about": {"icon":"","label":"About","keywords":"omarchy system information","action":"omarchy-launch-about"}, "system": {"icon":"","label":"System","aliases":["power-menu"],"keywords":"lock logout reboot shutdown suspend hibernate"}, @@ -47,7 +47,7 @@ "trigger.reminder": {"icon":"󰢌","label":"Reminder","aliases":["reminder"],"keywords":"timer notification remind"}, "trigger.capture": {"icon":"","label":"Capture","aliases":["capture","screenshot","screenrecord","screen-record","screenrecording"],"keywords":"color text extraction"}, "trigger.capture.screenshot": {"icon":"","label":"Screenshot","keywords":"picture","action":"omarchy-capture-screenshot"}, - "trigger.capture.screenrecord.stop": {"icon":"","label":"Stop Screenrecording","keywords":"recording video","when":"pgrep -f '^gpu-screen-recorder' >/dev/null","action":"omarchy-capture-screenrecording --stop-recording"}, + "trigger.capture.screenrecord.stop": {"icon":"","label":"Stop Screenrecording","keywords":"recording video","when":"pgrep -f '^gpu-screen-recorder'","action":"omarchy-capture-screenrecording --stop-recording"}, "trigger.capture.screenrecord": {"icon":"","label":"Screenrecord","keywords":"record screen video"}, "trigger.capture.text": {"icon":"󰴑","label":"Text","keywords":"ocr text extraction","action":"omarchy-capture-text"}, "trigger.capture.color": {"icon":"󰃉","label":"Color","keywords":"picker hyprpicker","action":"pkill hyprpicker || hyprpicker -a"}, @@ -59,15 +59,15 @@ "trigger.share": {"icon":"","label":"Share","aliases":["share"],"keywords":"clipboard file folder localsend"}, "trigger.toggle": {"icon":"󰔎","label":"Toggle","aliases":["toggle","toggles"],"keywords":"screensaver nightlight idle notifications bar quickshell"}, "trigger.hardware": {"icon":"","label":"Hardware","aliases":["hardware","hw"],"keywords":"monitor touchpad touchscreen gpu"}, - "trigger.hardware.laptop-display": {"icon":"󰛧","label":"Laptop Display","keywords":"monitor","action":"omarchy-hyprland-monitor-internal toggle"}, - "trigger.hardware.mirror-display": {"icon":"󰍹","label":"Mirror Display","keywords":"monitor","action":"omarchy-hyprland-monitor-internal-mirror toggle"}, - "trigger.hardware.hybrid-gpu": {"icon":"","label":"Hybrid GPU","keywords":"graphics","when":"omarchy-hw-hybrid-gpu >/dev/null","action":"omarchy-launch-floating-terminal-with-presentation omarchy-toggle-hybrid-gpu"}, - "trigger.hardware.touchpad": {"icon":"󰟸","label":"Touchpad","keywords":"trackpad","when":"omarchy-hw-touchpad >/dev/null","action":"omarchy-toggle-touchpad"}, - "trigger.hardware.touchpad-haptics": {"icon":"󰌌","label":"Touchpad Haptics","when":"omarchy-hw-dell-xps-haptic-touchpad >/dev/null && omarchy-cmd-present dell-xps-touchpad-haptics"}, - "trigger.hardware.touchpad-haptics.low": {"icon":"󰌌","label":"low","when":"omarchy-hw-dell-xps-haptic-touchpad >/dev/null && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"low\" ]]","action":"dell-xps-touchpad-haptics set low"}, - "trigger.hardware.touchpad-haptics.mid": {"icon":"󰌌","label":"mid","keywords":"medium","when":"omarchy-hw-dell-xps-haptic-touchpad >/dev/null && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"mid\" ]]","action":"dell-xps-touchpad-haptics set mid"}, - "trigger.hardware.touchpad-haptics.high": {"icon":"󰌌","label":"high","when":"omarchy-hw-dell-xps-haptic-touchpad >/dev/null && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"high\" ]]","action":"dell-xps-touchpad-haptics set high"}, - "trigger.hardware.touchscreen": {"icon":"󰆽","label":"Touchscreen","keywords":"tablet","when":"omarchy-hw-touchscreen >/dev/null","action":"omarchy-toggle-touchscreen"}, + "trigger.hardware.laptop-display": {"icon":"󰛧","label":"Laptop Display","keywords":"monitor","when":"omarchy-hw-laptop","action":"omarchy-hyprland-monitor-internal toggle"}, + "trigger.hardware.mirror-display": {"icon":"󰍹","label":"Mirror Display","keywords":"monitor","when":"omarchy-hw-laptop","action":"omarchy-hyprland-monitor-internal-mirror toggle"}, + "trigger.hardware.hybrid-gpu": {"icon":"","label":"Hybrid GPU","keywords":"graphics","when":"omarchy-hw-hybrid-gpu","action":"omarchy-launch-floating-terminal-with-presentation omarchy-toggle-hybrid-gpu"}, + "trigger.hardware.touchpad": {"icon":"󰟸","label":"Touchpad","keywords":"trackpad","when":"omarchy-hw-touchpad","action":"omarchy-toggle-touchpad"}, + "trigger.hardware.touchpad-haptics": {"icon":"󰌌","label":"Touchpad Haptics"}, + "trigger.hardware.touchpad-haptics.low": {"icon":"󰌌","label":"low","when":"omarchy-hw-dell-xps-haptic-touchpad && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"low\" ]]","action":"dell-xps-touchpad-haptics set low"}, + "trigger.hardware.touchpad-haptics.mid": {"icon":"󰌌","label":"mid","keywords":"medium","when":"omarchy-hw-dell-xps-haptic-touchpad && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"mid\" ]]","action":"dell-xps-touchpad-haptics set mid"}, + "trigger.hardware.touchpad-haptics.high": {"icon":"󰌌","label":"high","when":"omarchy-hw-dell-xps-haptic-touchpad && omarchy-cmd-present dell-xps-touchpad-haptics","checked":"[[ \"$(dell-xps-touchpad-haptics get)\" == \"high\" ]]","action":"dell-xps-touchpad-haptics set high"}, + "trigger.hardware.touchscreen": {"icon":"󰆽","label":"Touchscreen","keywords":"tablet","when":"omarchy-hw-touchscreen","action":"omarchy-toggle-touchscreen"}, "trigger.reminder.set": {"icon":"󰢌","label":"Set one","aliases":["reminder-set","remind"],"keywords":"timer","action":"omarchy-reminder -i"}, "trigger.reminder.show": {"icon":"󰢌","label":"Show all","keywords":"list","action":"omarchy-reminder show"}, "trigger.reminder.clear": {"icon":"󰢌","label":"Clear all","keywords":"delete","action":"omarchy-reminder clear"}, @@ -83,6 +83,7 @@ "trigger.toggle.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","action":"omarchy-hyprland-workspace-layout-toggle"}, "trigger.toggle.window-gaps": {"icon":"","label":"Window Gaps","action":"omarchy-hyprland-window-gaps-toggle"}, "trigger.toggle.one-window-ratio": {"icon":"","label":"1-Window Ratio","keywords":"square","action":"omarchy-hyprland-window-single-square-aspect-toggle"}, + // Style "style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"keywords":"colors","action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""}, "style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""}, @@ -236,7 +237,7 @@ "remove.browser": {"icon":"","label":"Browser","keywords":"uninstall"}, "remove.dictation": {"icon":"","label":"Dictation","keywords":"uninstall voice","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"}, "remove.gaming": {"icon":"","label":"Gaming","keywords":"uninstall steam retroarch minecraft"}, - "remove.service": {"icon":"","label":"Services","keywords":"uninstall dropbox tailscale vpn","when":"omarchy-pkg-present dropbox || omarchy-pkg-present tailscale"}, + "remove.service": {"icon":"","label":"Services","keywords":"uninstall dropbox tailscale vpn"}, "remove.windows": {"icon":"󰍲","label":"Windows","keywords":"uninstall vm","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"}, "remove.preinstalls": {"icon":"󰏓","label":"Preinstalls","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"}, "remove.security": {"icon":"","label":"Security","keywords":"fingerprint fido2 ssh sshd"}, diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 6f4bda7e..603aa4d5 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -347,13 +347,11 @@ Item { return MenuModel.childCount(root.items, root.itemOrder, id) } - // Items whose `when:` evaluated to false are hidden everywhere — nav, - // drilldown, and search. Items with no `when:` are always visible. + // Guarded items are hidden when their `when:` evaluates false. Static + // submenus are also hidden when none of their descendants are visible; + // provider-backed menus stay visible because their rows load on demand. function isVisible(entry) { - if (!entry) return false - if (!entry.when) return true - var result = root.whenResults[entry.id] - return result === undefined ? true : result + return MenuModel.isVisible(root.items, root.itemOrder, root.whenResults, entry) } // Label with the ✓ marker baked in when `checked:` evaluated truthy. @@ -752,8 +750,8 @@ Item { for (var i = 0; i < ids.length; i++) { var entry = root.items[ids[i]] if (!entry) continue - if (entry.when) script += "if " + entry.when + " >/dev/null 2>&1; then echo " + ids[i] + ":w:1; else echo " + ids[i] + ":w:0; fi\n" - if (entry.checked) script += "if " + entry.checked + " >/dev/null 2>&1; then echo " + ids[i] + ":c:1; else echo " + ids[i] + ":c:0; fi\n" + if (entry.when) script += "if { " + entry.when + "; } >/dev/null 2>&1; then echo " + ids[i] + ":w:1; else echo " + ids[i] + ":w:0; fi\n" + if (entry.checked) script += "if { " + entry.checked + "; } >/dev/null 2>&1; then echo " + ids[i] + ":c:1; else echo " + ids[i] + ":c:0; fi\n" } if (!script) { root.whenResults = ({}) diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index f3805108..251ee4bc 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -173,6 +173,25 @@ function childCount(items, itemOrder, id) { 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 + " ✓" @@ -282,6 +301,7 @@ if (typeof module !== "undefined") { parentPathFor: parentPathFor, isDescendantOf: isDescendantOf, childCount: childCount, + isVisible: isVisible, labelFor: labelFor, searchableToken: searchableToken, leafIdFor: leafIdFor, diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index cf86d6b2..fcd0f838 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -64,6 +64,20 @@ assert(menu.isDescendantOf(merged.items, 'style.theme', 'style'), 'menu detects assertEqual(menu.childCount(merged.items, merged.itemOrder, 'style'), 1, 'menu counts children') assertEqual(menu.labelFor({ id: 'style.theme', label: 'Theme', checked: 'cmd' }, { 'style.theme': true }), 'Theme ✓', 'menu appends checked marker') +const visibilityItems = { + hardware: menu.normalizeItem('hardware', { label: 'Hardware' }), + laptop: menu.normalizeItem('hardware.laptop', { label: 'Laptop', when: 'is-laptop', action: 'toggle-laptop' }), + nested: menu.normalizeItem('nested', { label: 'Nested' }), + branch: menu.normalizeItem('nested.branch', { label: 'Branch' }), + leaf: menu.normalizeItem('nested.branch.leaf', { label: 'Leaf', when: 'has-leaf', action: 'run-leaf' }), + dynamic: menu.normalizeItem('dynamic', { label: 'Dynamic', provider: 'items' }) +} +const visibilityOrder = Object.keys(visibilityItems) +assert(!menu.isVisible(visibilityItems, visibilityOrder, { 'hardware.laptop': false }, visibilityItems.hardware), 'menu hides a submenu with no visible children') +assert(menu.isVisible(visibilityItems, visibilityOrder, { 'hardware.laptop': true }, visibilityItems.hardware), 'menu shows a submenu with a visible child') +assert(!menu.isVisible(visibilityItems, visibilityOrder, { 'nested.branch.leaf': false }, visibilityItems.nested), 'menu hides recursively empty submenus') +assert(menu.isVisible(visibilityItems, visibilityOrder, {}, visibilityItems.dynamic), 'menu keeps provider-backed submenus visible') + const entry = merged.items['style.theme'] assert(menu.matchesQuery(entry, 'theme', true), 'menu matches labels and aliases') assert(menu.matchesQuery(entry, 'colors', true), 'menu matches aliases') @@ -122,6 +136,16 @@ assert( !defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'], 'menu removes the relocated toggles from Trigger > Toggle' ) +assertEqual( + defaultById['trigger.hardware.laptop-display'].when, + 'omarchy-hw-laptop', + 'menu only shows Laptop Display on laptops' +) +assertEqual( + defaultById['trigger.hardware.mirror-display'].when, + 'omarchy-hw-laptop', + 'menu only shows Mirror Display on laptops' +) assert( /font\.family: row\.iconFont\.length > 0 \? row\.iconFont : root\.fontFamily/.test(menuQml), 'menu rows support per-icon font families'