Dim installed software in the Install menus instead of hiding it (#6955)

Install rows hid themselves with `when:"! <present>"`, so software you
already had vanished from the very list it was installed from. Add a
`disabled:` guard that keeps a row listed but dim, ✓-marked, unselectable
and out of search, and move every Install row onto it.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-15 15:31:07 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b724f76156
commit 4b93f8d84d
5 changed files with 255 additions and 91 deletions
+64 -60
View File
@@ -14,6 +14,10 @@
// title header text shown when the submenu is open; defaults to label // title header text shown when the submenu is open; defaults to label
// when shell condition; hide row when it fails // when shell condition; hide row when it fails
// checked shell condition; append ✓ when it succeeds // checked shell condition; append ✓ when it succeeds
// disabled shell condition; when it succeeds the row stays listed but
// goes dim, takes a ✓ and can no longer be selected. Install
// rows use it so software already on the machine reads as
// installed instead of vanishing from the list.
// Root Menu // Root Menu
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"provider":"apps"}, "apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"provider":"apps"},
@@ -201,71 +205,71 @@
"install.browser": {"icon":"","label":"Browser"}, "install.browser": {"icon":"","label":"Browser"},
"install.ai": {"icon":"󱚤","label":"AI"}, "install.ai": {"icon":"󱚤","label":"AI"},
"install.gaming": {"icon":"","label":"Gaming"}, "install.gaming": {"icon":"","label":"Gaming"},
"install.windows": {"icon":"󰍲","label":"Windows","when":"[[ ! -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"}, "install.windows": {"icon":"󰍲","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"}, "install.preinstalls": {"icon":"󰏓","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
"install.browser.chrome": {"icon":"","label":"Chrome","when":"! omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"}, "install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
"install.browser.edge": {"icon":"󰇩","label":"Edge","when":"! omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"}, "install.browser.edge": {"icon":"󰇩","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
"install.browser.brave": {"icon":"","label":"Brave","when":"! omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"}, "install.browser.brave": {"icon":"","label":"Brave","disabled":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"},
"install.browser.brave-origin": {"icon":"","label":"Brave Origin","when":"! omarchy-pkg-present brave-origin-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave-origin'"}, "install.browser.brave-origin": {"icon":"","label":"Brave Origin","disabled":"omarchy-pkg-present brave-origin-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave-origin'"},
"install.browser.firefox": {"icon":"","label":"Firefox","when":"! omarchy-pkg-present firefox","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser firefox'"}, "install.browser.firefox": {"icon":"","label":"Firefox","disabled":"omarchy-pkg-present firefox","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser firefox'"},
"install.browser.zen": {"icon":"󰖟","label":"Zen","when":"! omarchy-pkg-present zen-browser-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser zen'"}, "install.browser.zen": {"icon":"󰖟","label":"Zen","disabled":"omarchy-pkg-present zen-browser-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser zen'"},
"install.service.1password": {"icon":"󰢁","label":"1Password","when":"! omarchy-pkg-present 1password","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password"}, "install.service.1password": {"icon":"󰢁","label":"1Password","disabled":"omarchy-pkg-present 1password","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password"},
"install.service.dropbox": {"icon":"","label":"Dropbox","when":"! omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"}, "install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
"install.service.spotify": {"icon":"󰓇","label":"Spotify","when":"! omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"}, "install.service.spotify": {"icon":"󰓇","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
"install.service.signal": {"icon":"󰭹","label":"Signal","when":"! omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"}, "install.service.signal": {"icon":"󰭹","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
"install.service.tailscale": {"icon":"","label":"Tailscale","when":"! omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"}, "install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
"install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","when":"! omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"}, "install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
"install.service.once": {"icon":"󰏖","label":"ONCE","when":"! omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"}, "install.service.once": {"icon":"󰏖","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
"install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","when":"! omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"}, "install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","disabled":"omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"},
"install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]] && ! grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"}, "install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]]","disabled":"grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"},
"install.editor.vscode": {"icon":"","label":"VSCode","when":"! omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"}, "install.editor.vscode": {"icon":"","label":"VSCode","disabled":"omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"},
"install.editor.cursor": {"icon":"","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"}, "install.editor.cursor": {"icon":"","label":"Cursor","disabled":"omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"},
"install.editor.zed": {"icon":"","label":"Zed","when":"! omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"}, "install.editor.zed": {"icon":"","label":"Zed","disabled":"omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"},
"install.editor.sublime": {"icon":"","label":"Sublime Text","when":"! omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"}, "install.editor.sublime": {"icon":"","label":"Sublime Text","disabled":"omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"},
"install.editor.helix": {"icon":"","label":"Helix","when":"! omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"}, "install.editor.helix": {"icon":"","label":"Helix","disabled":"omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"},
"install.editor.vim": {"icon":"","label":"Vim","when":"! omarchy-pkg-present vim","action":"omarchy-install-app Vim vim"}, "install.editor.vim": {"icon":"","label":"Vim","disabled":"omarchy-pkg-present vim","action":"omarchy-install-app Vim vim"},
"install.editor.emacs": {"icon":"","label":"Emacs","when":"! omarchy-pkg-present omarchy-emacs","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-emacs"}, "install.editor.emacs": {"icon":"","label":"Emacs","disabled":"omarchy-pkg-present omarchy-emacs","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-emacs"},
"install.terminal.alacritty": {"icon":"","label":"Alacritty","when":"! omarchy-pkg-present alacritty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal alacritty'"}, "install.terminal.alacritty": {"icon":"","label":"Alacritty","disabled":"omarchy-pkg-present alacritty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal alacritty'"},
"install.terminal.foot": {"icon":"","label":"Foot","when":"! omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"}, "install.terminal.foot": {"icon":"","label":"Foot","disabled":"omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
"install.terminal.ghostty": {"icon":"","label":"Ghostty","when":"! omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"}, "install.terminal.ghostty": {"icon":"","label":"Ghostty","disabled":"omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
"install.terminal.kitty": {"icon":"","label":"Kitty","when":"! omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"}, "install.terminal.kitty": {"icon":"","label":"Kitty","disabled":"omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"! omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"}, "install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
"install.ai.dictation": {"icon":"","label":"Dictation","when":"! omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"}, "install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"! omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"}, "install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"}, "install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"! omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""}, "install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
"install.gaming.steam": {"icon":"","label":"Steam","when":"! omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"}, "install.gaming.steam": {"icon":"","label":"Steam","disabled":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
"install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","when":"! omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"}, "install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","disabled":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
"install.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","when":"! omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"}, "install.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","disabled":"omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
"install.gaming.geforce-now": {"icon":"󰢹","label":"NVIDIA GeForce NOW","when":"! flatpak info com.nvidia.geforcenow","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-geforce-now"}, "install.gaming.geforce-now": {"icon":"󰢹","label":"NVIDIA GeForce NOW","disabled":"flatpak info com.nvidia.geforcenow","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-geforce-now"},
"install.gaming.xbox-cloud": {"icon":"","label":"Xbox Cloud Gaming","when":"[[ ! -f \"$HOME/.local/share/applications/Xbox Cloud Gaming.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-cloud"}, "install.gaming.xbox-cloud": {"icon":"","label":"Xbox Cloud Gaming","disabled":"[[ -f \"$HOME/.local/share/applications/Xbox Cloud Gaming.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-cloud"},
"install.gaming.xbox-controllers": {"icon":"󰂯","label":"Xbox Controllers","when":"! omarchy-pkg-present xpadneo-dkms","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-controllers"}, "install.gaming.xbox-controllers": {"icon":"󰂯","label":"Xbox Controllers","disabled":"omarchy-pkg-present xpadneo-dkms","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-controllers"},
"install.gaming.battlenet": {"icon":"","label":"Battle.net","when":"[[ ! -d $HOME/Games/battlenet ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-battlenet"}, "install.gaming.battlenet": {"icon":"","label":"Battle.net","disabled":"[[ -d $HOME/Games/battlenet ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-battlenet"},
"install.gaming.lutris": {"icon":"","label":"Lutris","when":"! omarchy-pkg-present lutris","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-lutris"}, "install.gaming.lutris": {"icon":"","label":"Lutris","disabled":"omarchy-pkg-present lutris","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-lutris"},
"install.gaming.heroic": {"icon":"󱓟","label":"Heroic (Epic Games)","when":"! omarchy-pkg-present heroic-games-launcher-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-heroic"}, "install.gaming.heroic": {"icon":"󱓟","label":"Heroic (Epic Games)","disabled":"omarchy-pkg-present heroic-games-launcher-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-heroic"},
"install.gaming.retro-launcher": {"icon":"󰯉","label":"RetroArch Game Launcher","action":"omarchy-games-retro-install"}, "install.gaming.retro-launcher": {"icon":"󰯉","label":"RetroArch Game Launcher","action":"omarchy-games-retro-install"},
"install.development.rails": {"icon":"󰫏","label":"Ruby on Rails","when":"[[ ! -d $HOME/.local/share/mise/installs/ruby ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ruby'"}, "install.development.rails": {"icon":"󰫏","label":"Ruby on Rails","disabled":"[[ -d $HOME/.local/share/mise/installs/ruby ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ruby'"},
"install.development.docker-dbs": {"icon":"","label":"Docker DB","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-docker-dbs"}, "install.development.docker-dbs": {"icon":"","label":"Docker DB","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-docker-dbs"},
"install.development.javascript": {"icon":"","label":"JavaScript"}, "install.development.javascript": {"icon":"","label":"JavaScript"},
"install.development.go": {"icon":"","label":"Go","when":"[[ ! -d $HOME/.local/share/mise/installs/go ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env go'"}, "install.development.go": {"icon":"","label":"Go","disabled":"[[ -d $HOME/.local/share/mise/installs/go ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env go'"},
"install.development.php": {"icon":"","label":"PHP"}, "install.development.php": {"icon":"","label":"PHP"},
"install.development.python": {"icon":"","label":"Python","when":"[[ ! -d $HOME/.local/share/mise/installs/python ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env python'"}, "install.development.python": {"icon":"","label":"Python","disabled":"[[ -d $HOME/.local/share/mise/installs/python ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env python'"},
"install.development.elixir": {"icon":"","label":"Elixir"}, "install.development.elixir": {"icon":"","label":"Elixir"},
"install.development.zig": {"icon":"","label":"Zig","when":"[[ ! -d $HOME/.local/share/mise/installs/zig ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env zig'"}, "install.development.zig": {"icon":"","label":"Zig","disabled":"[[ -d $HOME/.local/share/mise/installs/zig ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env zig'"},
"install.development.rust": {"icon":"","label":"Rust","when":"[[ ! -d $HOME/.rustup ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env rust'"}, "install.development.rust": {"icon":"","label":"Rust","disabled":"[[ -d $HOME/.rustup ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env rust'"},
"install.development.java": {"icon":"","label":"Java","when":"[[ ! -d $HOME/.local/share/mise/installs/java ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env java'"}, "install.development.java": {"icon":"","label":"Java","disabled":"[[ -d $HOME/.local/share/mise/installs/java ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env java'"},
"install.development.dotnet": {"icon":"","label":".NET","when":"[[ ! -d $HOME/.local/share/mise/installs/dotnet ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env dotnet'"}, "install.development.dotnet": {"icon":"","label":".NET","disabled":"[[ -d $HOME/.local/share/mise/installs/dotnet ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env dotnet'"},
"install.development.ocaml": {"icon":"","label":"OCaml","when":"[[ ! -d $HOME/.opam ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ocaml'"}, "install.development.ocaml": {"icon":"","label":"OCaml","disabled":"[[ -d $HOME/.opam ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ocaml'"},
"install.development.clojure": {"icon":"","label":"Clojure","when":"[[ ! -d $HOME/.local/share/mise/installs/clojure ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env clojure'"}, "install.development.clojure": {"icon":"","label":"Clojure","disabled":"[[ -d $HOME/.local/share/mise/installs/clojure ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env clojure'"},
"install.development.scala": {"icon":"","label":"Scala","when":"[[ ! -d $HOME/.local/share/mise/installs/scala ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env scala'"}, "install.development.scala": {"icon":"","label":"Scala","disabled":"[[ -d $HOME/.local/share/mise/installs/scala ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env scala'"},
"install.development.javascript.node": {"icon":"","label":"Node.js","when":"[[ ! -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env node'"}, "install.development.javascript.node": {"icon":"","label":"Node.js","disabled":"[[ -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env node'"},
"install.development.javascript.bun": {"icon":"","label":"Bun","when":"[[ ! -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env bun'"}, "install.development.javascript.bun": {"icon":"","label":"Bun","disabled":"[[ -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env bun'"},
"install.development.javascript.deno": {"icon":"","label":"Deno","when":"[[ ! -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env deno'"}, "install.development.javascript.deno": {"icon":"","label":"Deno","disabled":"[[ -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env deno'"},
"install.development.php.php": {"icon":"","label":"PHP","when":"! omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"}, "install.development.php.php": {"icon":"","label":"PHP","disabled":"omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"},
"install.development.php.laravel": {"icon":"","label":"Laravel","when":"[[ ! -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"}, "install.development.php.laravel": {"icon":"","label":"Laravel","disabled":"[[ -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"},
"install.development.php.symfony": {"icon":"","label":"Symfony","when":"! omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env symfony'"}, "install.development.php.symfony": {"icon":"","label":"Symfony","disabled":"omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env symfony'"},
"install.development.elixir.elixir": {"icon":"","label":"Elixir","when":"[[ ! -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env elixir'"}, "install.development.elixir.elixir": {"icon":"","label":"Elixir","disabled":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env elixir'"},
"install.development.elixir.phoenix": {"icon":"","label":"Phoenix","when":"! compgen -G \"$HOME/.mix/archives/phx_new*\"","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env phoenix'"}, "install.development.elixir.phoenix": {"icon":"","label":"Phoenix","disabled":"compgen -G \"$HOME/.mix/archives/phx_new*\"","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env phoenix'"},
// Remove // Remove
"remove.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-remove"}, "remove.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-remove"},
+69 -16
View File
@@ -317,6 +317,7 @@ Item {
aliases: aliases, aliases: aliases,
when: "", when: "",
checked: "", checked: "",
disabled: "",
order: 0 order: 0
}) })
} }
@@ -382,6 +383,7 @@ Item {
aliases: [], aliases: [],
when: "", when: "",
checked: "", checked: "",
disabled: "",
order: 0 order: 0
}) })
} }
@@ -470,9 +472,10 @@ Item {
return MenuModel.isVisible(root.items, root.itemOrder, root.whenResults, entry) return MenuModel.isVisible(root.items, root.itemOrder, root.whenResults, entry)
} }
// Label with the ✓ marker baked in when `checked:` evaluated truthy. // Label with the ✓ marker baked in when `checked:` or `disabled:` evaluated
// truthy.
function labelFor(entry) { function labelFor(entry) {
return MenuModel.labelFor(entry, root.checkedResults) return MenuModel.labelFor(entry, root.checkedResults, root.disabledResults)
} }
function searchableToken(value) { function searchableToken(value) {
@@ -495,8 +498,17 @@ Item {
return MenuModel.descriptionTextMatches(query, text) return MenuModel.descriptionTextMatches(query, text)
} }
// Rows whose `disabled:` evaluated truthy stay listed but dimmed, and the
// cursor steps over them.
function isDisabled(entry) {
return MenuModel.isDisabled(root.disabledResults, entry)
}
// A disabled row earns its place in the submenu it belongs to, where the
// list around it is the point. Search is a list of what you can do, so it
// leaves them out.
function matchesQuery(entry, query) { function matchesQuery(entry, query) {
return MenuModel.matchesQuery(entry, query, root.isVisible(entry)) return MenuModel.matchesQuery(entry, query, root.isVisible(entry) && !root.isDisabled(entry))
} }
function searchScore(entry, query) { function searchScore(entry, query) {
@@ -504,7 +516,38 @@ Item {
} }
function displayRow(entry, detail, score, section) { function displayRow(entry, detail, score, section) {
return MenuModel.displayRow(root.items, root.itemOrder, root.checkedResults, entry, detail, score, section) return MenuModel.displayRow(root.items, root.itemOrder, root.checkedResults, root.disabledResults, entry, detail, score, section)
}
function rowSelectable(index) {
if (index < 0 || index >= displayModel.count) return false
return !displayModel.get(index).disabled
}
// First selectable row at or past `from`, continuing in the direction of
// travel and wrapping. -1 when every row is disabled, which leaves the menu
// with no cursor at all rather than one parked on a row Enter won't run.
function nextSelectable(from, direction) {
var count = displayModel.count
if (count === 0) return -1
var step = direction < 0 ? -1 : 1
var index = ((from % count) + count) % count
for (var i = 0; i < count; i++) {
if (root.rowSelectable(index)) return index
index = (index + step + count) % count
}
return -1
}
// Park the cursor on a selectable row after the rows underneath it changed.
// A menu with nothing selectable in it -- every app in it already installed
// -- shows no cursor at all, and grows one the moment a row can take it.
function settleCursor() {
var target = root.nextSelectable(root.selectedIndex, 1)
root.selectedIndex = target >= 0 ? target : 0
root.cursorActive = target >= 0
} }
function rebuildDmenuDisplay() { function rebuildDmenuDisplay() {
@@ -530,6 +573,7 @@ Item {
&& detail.toLowerCase().indexOf(query) < 0) continue && detail.toLowerCase().indexOf(query) < 0) continue
displayModel.append({ displayModel.append({
itemId: "dmenu." + i, itemId: "dmenu." + i,
disabled: false,
kind: "dmenu", kind: "dmenu",
icon: icon, icon: icon,
iconFont: "", iconFont: "",
@@ -630,9 +674,7 @@ Item {
for (var k = 0; k < rows.length; k++) displayModel.append(rows[k]) for (var k = 0; k < rows.length; k++) displayModel.append(rows[k])
layoutSerial += 1 layoutSerial += 1
if (displayModel.count === 0) selectedIndex = 0 root.settleCursor()
else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1
else if (selectedIndex < 0) selectedIndex = 0
Qt.callLater(function() { Qt.callLater(function() {
if (displayModel.count > 0) root.revealCursor() if (displayModel.count > 0) root.revealCursor()
@@ -665,12 +707,12 @@ Item {
if (displayModel.count === 0) return if (displayModel.count === 0) return
root.disarmPointer() root.disarmPointer()
if (!cursorActive) { var from = cursorActive ? selectedIndex + delta : (delta < 0 ? displayModel.count - 1 : 0)
cursorActive = true var target = root.nextSelectable(from, delta)
selectedIndex = delta < 0 ? displayModel.count - 1 : 0 if (target < 0) return
} else {
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count cursorActive = true
} selectedIndex = target
revealCursor() revealCursor()
} }
@@ -727,7 +769,7 @@ Item {
return return
} }
if (index < 0 || index >= displayModel.count) return if (!root.rowSelectable(index)) return
var row = displayModel.get(index) var row = displayModel.get(index)
if (row.kind === "menu" || row.kind === "link") { if (row.kind === "menu" || row.kind === "link") {
@@ -874,6 +916,7 @@ Item {
function selectFromPointer(index, item, mouse) { function selectFromPointer(index, item, mouse) {
if (!pointerGate.moved(item, mouse)) return if (!pointerGate.moved(item, mouse)) return
if (!root.rowSelectable(index)) return
root.cursorActive = true root.cursorActive = true
root.selectedIndex = index root.selectedIndex = index
} }
@@ -947,6 +990,7 @@ Item {
property var whenResults: ({}) // id → true|false (allow visibility) property var whenResults: ({}) // id → true|false (allow visibility)
property var checkedResults: ({}) // id → true|false (show ✓) property var checkedResults: ({}) // id → true|false (show ✓)
property var disabledResults: ({}) // id → true|false (dim, skip cursor)
property bool guardsPending: false property bool guardsPending: false
function evaluateGuards() { function evaluateGuards() {
@@ -966,6 +1010,7 @@ Item {
if (!script) { if (!script) {
root.whenResults = ({}) root.whenResults = ({})
root.checkedResults = ({}) root.checkedResults = ({})
root.disabledResults = ({})
return return
} }
guardProc.collected = "" guardProc.collected = ""
@@ -991,6 +1036,7 @@ Item {
var nextWhen = ({}) var nextWhen = ({})
var nextChecked = ({}) var nextChecked = ({})
var nextDisabled = ({})
var lines = guardProc.collected.split("\n") var lines = guardProc.collected.split("\n")
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim() var line = lines[i].trim()
@@ -1005,9 +1051,11 @@ Item {
var tag = rest.substring(tagAt + 1) var tag = rest.substring(tagAt + 1)
if (tag === "w") nextWhen[id] = value if (tag === "w") nextWhen[id] = value
else if (tag === "c") nextChecked[id] = value else if (tag === "c") nextChecked[id] = value
else if (tag === "d") nextDisabled[id] = value
} }
root.whenResults = nextWhen root.whenResults = nextWhen
root.checkedResults = nextChecked root.checkedResults = nextChecked
root.disabledResults = nextDisabled
if (root.opened) root.rebuildDisplay() if (root.opened) root.rebuildDisplay()
// Run the evaluation that had to stand aside. Deferred by a turn so the // Run the evaluation that had to stand aside. Deferred by a turn so the
// process is settled before its command is set again. // process is settled before its command is set again.
@@ -1108,7 +1156,7 @@ Item {
if (root.mode === "input") root.applyDmenuSelection(root.filterText) if (root.mode === "input") root.applyDmenuSelection(root.filterText)
else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0) else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0)
} else if (root.cursorActive) root.activateIndex(root.selectedIndex) } else if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = true else root.settleCursor()
event.accepted = true event.accepted = true
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) { } else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
root.setFilter(root.filterText + event.text) root.setFilter(root.filterText + event.text)
@@ -1211,6 +1259,7 @@ Item {
required property string path required property string path
required property string action required property string action
required property int childCount required property int childCount
required property bool disabled
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
readonly property bool isApp: row.kind === "app" readonly property bool isApp: row.kind === "app"
@@ -1218,6 +1267,9 @@ Item {
width: ListView.view.width width: ListView.view.width
height: root.rowHeightForDetail(row.detail) height: root.rowHeightForDetail(row.detail)
// Faded: the row is here to say the software is already
// installed, not to be picked.
opacity: row.disabled ? 0.4 : 1
radius: root.cornerRadius radius: root.cornerRadius
color: row.hasCursor ? root.selectedBackground : "transparent" color: row.hasCursor ? root.selectedBackground : "transparent"
borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none() borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none()
@@ -1330,7 +1382,7 @@ Item {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: row.disabled ? Qt.ArrowCursor : Qt.PointingHandCursor
onEntered: root.selectFromPointer(row.index, row, { onEntered: root.selectFromPointer(row.index, row, {
x: mouseArea.mouseX, x: mouseArea.mouseX,
y: mouseArea.mouseY y: mouseArea.mouseY
@@ -1339,6 +1391,7 @@ Item {
root.selectFromPointer(row.index, row, mouse) root.selectFromPointer(row.index, row, mouse)
} }
onClicked: { onClicked: {
if (row.disabled) return
root.cursorActive = true root.cursorActive = true
root.selectedIndex = row.index root.selectedIndex = row.index
root.activateIndex(row.index, true) root.activateIndex(row.index, true)
+25 -11
View File
@@ -34,7 +34,8 @@ function normalizeItem(id, raw) {
provider: value.provider || "", provider: value.provider || "",
aliases: aliases, aliases: aliases,
when: value.when || "", when: value.when || "",
checked: value.checked || "" checked: value.checked || "",
disabled: value.disabled || ""
} }
} }
@@ -83,7 +84,7 @@ function mergeMenuSources(defaultItems, userItems) {
} }
if (!nextItems.root) { if (!nextItems.root) {
nextItems.root = { id: "root", parent: "", kind: "menu", icon: "", iconFont: "", label: "Go", title: "", target: "", description: "", aliases: [], when: "", checked: "", action: "", provider: "" } nextItems.root = { id: "root", parent: "", kind: "menu", icon: "", iconFont: "", label: "Go", title: "", target: "", description: "", aliases: [], when: "", checked: "", disabled: "", action: "", provider: "" }
nextOrder.unshift("root") nextOrder.unshift("root")
} }
for (var k3 = 0; k3 < nextOrder.length; k3++) nextItems[nextOrder[k3]].order = k3 for (var k3 = 0; k3 < nextOrder.length; k3++) nextItems[nextOrder[k3]].order = k3
@@ -270,10 +271,20 @@ function isVisible(items, itemOrder, whenResults, entry, depth) {
return false return false
} }
function labelFor(entry, checkedResults) { // A `disabled:` row stays listed but goes dim and unselectable. The
// Install submenus use it so software already on the machine reads as
// installed rather than disappearing from the list it was installed from.
function isDisabled(disabledResults, entry) {
if (!entry || !entry.disabled) return false
return !!(disabledResults && disabledResults[entry.id])
}
// A disabled row is software you already have, which is the same thing the ✓
// says everywhere else in the menu, so it earns the same marker.
function labelFor(entry, checkedResults, disabledResults) {
if (!entry) return "" if (!entry) return ""
if (entry.checked && checkedResults && checkedResults[entry.id]) return entry.label + " ✓" var marked = (entry.checked && checkedResults && checkedResults[entry.id]) || isDisabled(disabledResults, entry)
return entry.label return marked ? entry.label + " ✓" : entry.label
} }
function searchableToken(value) { function searchableToken(value) {
@@ -351,16 +362,17 @@ function searchScore(items, entry, query) {
return score * 1000 + depthFor(items, entry.id) * 25 + entry.order return score * 1000 + depthFor(items, entry.id) * 25 + entry.order
} }
function displayRow(items, itemOrder, checkedResults, entry, detail, score, section) { function displayRow(items, itemOrder, checkedResults, disabledResults, entry, detail, score, section) {
var target = entry.kind === "link" ? entry.target : entry.id var target = entry.kind === "link" ? entry.target : entry.id
return { return {
itemId: entry.id, itemId: entry.id,
disabled: isDisabled(disabledResults, entry),
kind: entry.kind, kind: entry.kind,
icon: entry.icon, icon: entry.icon,
iconFont: entry.iconFont || "", iconFont: entry.iconFont || "",
appIcon: entry.appIcon || "", appIcon: entry.appIcon || "",
appId: entry.appId || "", appId: entry.appId || "",
label: labelFor(entry, checkedResults), label: labelFor(entry, checkedResults, disabledResults),
target: target, target: target,
detail: detail || "", detail: detail || "",
path: pathFor(items, entry.id), path: pathFor(items, entry.id),
@@ -459,10 +471,10 @@ function guardLine(id, tag, expression) {
+ id + ":" + tag + ":1; else echo " + id + ":" + tag + ":0; fi\n" + id + ":" + tag + ":1; else echo " + id + ":" + tag + ":0; fi\n"
} }
// One bash script for every `when:` and `checked:` in the menu, reporting // One bash script for every `when:`, `checked:` and `disabled:` in the menu,
// `<id>:<w|c>:<0|1>` per line. Speed is the whole point: the menu opens on // reporting `<id>:<w|c|d>:<0|1>` per line. Speed is the whole point: the menu
// the last evaluation's answers, so however long this takes is how long a row // opens on the last evaluation's answers, so however long this takes is how
// can contradict the state it describes. // long a row can contradict the state it describes.
function guardScript(items) { function guardScript(items) {
var guards = "" var guards = ""
var ids = Object.keys(items || {}) var ids = Object.keys(items || {})
@@ -472,6 +484,7 @@ function guardScript(items) {
if (!entry) continue if (!entry) continue
if (entry.when) guards += guardLine(ids[i], "w", entry.when) if (entry.when) guards += guardLine(ids[i], "w", entry.when)
if (entry.checked) guards += guardLine(ids[i], "c", entry.checked) if (entry.checked) guards += guardLine(ids[i], "c", entry.checked)
if (entry.disabled) guards += guardLine(ids[i], "d", entry.disabled)
} }
return guards ? guardPrelude(guards) + guards : "" return guards ? guardPrelude(guards) + guards : ""
@@ -497,6 +510,7 @@ if (typeof module !== "undefined") {
isDescendantOf: isDescendantOf, isDescendantOf: isDescendantOf,
childCount: childCount, childCount: childCount,
isVisible: isVisible, isVisible: isVisible,
isDisabled: isDisabled,
labelFor: labelFor, labelFor: labelFor,
searchableToken: searchableToken, searchableToken: searchableToken,
leafIdFor: leafIdFor, leafIdFor: leafIdFor,
+7 -2
View File
@@ -10,6 +10,7 @@ const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const items = { const items = {
'setup.default.browser.brave': { id: 'setup.default.browser.brave', when: 'omarchy-pkg-present brave-bin', checked: '[[ "$(omarchy-default-browser)" == "brave" ]]' }, 'setup.default.browser.brave': { id: 'setup.default.browser.brave', when: 'omarchy-pkg-present brave-bin', checked: '[[ "$(omarchy-default-browser)" == "brave" ]]' },
'setup.default.browser.zen': { id: 'setup.default.browser.zen', when: 'omarchy-pkg-present zen-browser-bin', checked: '[[ "$(omarchy-default-browser)" == "zen" ]]' }, 'setup.default.browser.zen': { id: 'setup.default.browser.zen', when: 'omarchy-pkg-present zen-browser-bin', checked: '[[ "$(omarchy-default-browser)" == "zen" ]]' },
'install.browser.zen': { id: 'install.browser.zen', disabled: 'omarchy-pkg-present zen-browser-bin' },
'plain': { id: 'plain', label: 'No guards' } 'plain': { id: 'plain', label: 'No guards' }
} }
const script = menu.guardScript(items) const script = menu.guardScript(items)
@@ -23,7 +24,11 @@ assert(
script.includes('then echo setup.default.browser.zen:c:1; else echo setup.default.browser.zen:c:0; fi'), script.includes('then echo setup.default.browser.zen:c:1; else echo setup.default.browser.zen:c:0; fi'),
'guard script reports a checked: as <id>:c:<0|1>' 'guard script reports a checked: as <id>:c:<0|1>'
) )
assert(!/\bplain:[wc]:/.test(script), 'guard script skips items with nothing to evaluate') assert(
script.includes('if { omarchy-pkg-present zen-browser-bin; } >/dev/null 2>&1; then echo install.browser.zen:d:1; else echo install.browser.zen:d:0; fi'),
'guard script reports a disabled: as <id>:d:<0|1>'
)
assert(!/\bplain:[wcd]:/.test(script), 'guard script skips items with nothing to evaluate')
assertEqual(menu.guardScript({ plain: items.plain }), '', 'guard script is empty when no item carries a guard') assertEqual(menu.guardScript({ plain: items.plain }), '', 'guard script is empty when no item carries a guard')
// The cost the menu is paying is per fork, not per expression, so what makes // The cost the menu is paying is per fork, not per expression, so what makes
@@ -64,7 +69,7 @@ assert(
// keeps forking once per row that reads it. // keeps forking once per row that reads it.
const fs = require('fs') const fs = require('fs')
const defaultItems = menu.parseMenuJsonc(fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8')) const defaultItems = menu.parseMenuJsonc(fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8'))
const guardText = defaultItems.map(item => `${item.when}\n${item.checked}`).join('\n') const guardText = defaultItems.map(item => `${item.when}\n${item.checked}\n${item.disabled}`).join('\n')
const repeated = [...new Set( const repeated = [...new Set(
(guardText.match(/\$\((omarchy-[a-z0-9-]+)\)/g) || []).map(match => match.slice(2, -1)) (guardText.match(/\$\((omarchy-[a-z0-9-]+)\)/g) || []).map(match => match.slice(2, -1))
)].filter(command => guardText.split(`$(${command})`).length > 2) )].filter(command => guardText.split(`$(${command})`).length > 2)
+90 -2
View File
@@ -43,7 +43,8 @@ assertDeepEqual(
provider: '', provider: '',
aliases: ['theme'], aliases: ['theme'],
when: '', when: '',
checked: '' checked: '',
disabled: ''
}, },
'menu normalizes parsed items' 'menu normalizes parsed items'
) )
@@ -63,6 +64,8 @@ assertEqual(menu.parentPathFor(merged.items, 'style.theme'), 'Style', 'menu buil
assert(menu.isDescendantOf(merged.items, 'style.theme', 'style'), 'menu detects descendants') assert(menu.isDescendantOf(merged.items, 'style.theme', 'style'), 'menu detects descendants')
assertEqual(menu.childCount(merged.items, merged.itemOrder, 'style'), 1, 'menu counts children') 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') assertEqual(menu.labelFor({ id: 'style.theme', label: 'Theme', checked: 'cmd' }, { 'style.theme': true }), 'Theme ✓', 'menu appends checked marker')
assertEqual(menu.labelFor({ id: 'install.browser.zen', label: 'Zen', disabled: 'cmd' }, {}, { 'install.browser.zen': true }), 'Zen ✓', 'menu marks a disabled row as something you already have')
assertEqual(menu.labelFor({ id: 'install.browser.zen', label: 'Zen', disabled: 'cmd' }, {}, { 'install.browser.zen': false }), 'Zen', 'menu leaves an uninstalled row unmarked')
const visibilityItems = { const visibilityItems = {
hardware: menu.normalizeItem('hardware', { label: 'Hardware' }), hardware: menu.normalizeItem('hardware', { label: 'Hardware' }),
@@ -78,6 +81,22 @@ assert(menu.isVisible(visibilityItems, visibilityOrder, { 'hardware.laptop': tru
assert(!menu.isVisible(visibilityItems, visibilityOrder, { 'nested.branch.leaf': false }, visibilityItems.nested), 'menu hides recursively empty submenus') 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') assert(menu.isVisible(visibilityItems, visibilityOrder, {}, visibilityItems.dynamic), 'menu keeps provider-backed submenus visible')
// `disabled:` is the softer guard: the row stays listed and only loses the
// cursor, which is how an already-installed app keeps its place in Install.
const installed = menu.normalizeItem('install.browser.zen', { label: 'Zen', disabled: 'omarchy-pkg-present zen-browser-bin', action: 'install-zen' })
assert(menu.isVisible({ 'install.browser.zen': installed }, ['install.browser.zen'], { 'install.browser.zen': false }, installed), 'menu keeps a disabled row visible')
assert(menu.isDisabled({ 'install.browser.zen': true }, installed), 'menu disables a row whose disabled: succeeded')
assert(!menu.isDisabled({ 'install.browser.zen': false }, installed), 'menu leaves a row selectable when its disabled: failed')
assert(!menu.isDisabled({ 'install.browser.zen': true }, visibilityItems.laptop), 'menu never disables a row that declares no disabled:')
assert(
menu.displayRow({ 'install.browser.zen': installed }, ['install.browser.zen'], {}, { 'install.browser.zen': true }, installed, '', 0).disabled,
'menu display rows carry their disabled state'
)
assert(
/function matchesQuery\(entry, query\) \{\s*\n\s*return MenuModel\.matchesQuery\(entry, query, root\.isVisible\(entry\) && !root\.isDisabled\(entry\)\)/.test(menuQml),
'menu search skips disabled rows, which belong to the submenu they sit in rather than a list of what you can do'
)
const entry = merged.items['style.theme'] const entry = merged.items['style.theme']
assert(menu.matchesQuery(entry, 'theme', true), 'menu matches labels and aliases') assert(menu.matchesQuery(entry, 'theme', true), 'menu matches labels and aliases')
assert(menu.matchesQuery(entry, 'colors', true), 'menu matches aliases') assert(menu.matchesQuery(entry, 'colors', true), 'menu matches aliases')
@@ -86,9 +105,10 @@ assert(!menu.matchesQuery(entry, 'theme', false), 'menu hides invisible matches'
assert(menu.searchScore(merged.items, entry, 'theme') < menu.searchScore(merged.items, entry, 'appearance'), 'menu scores name matches above description matches') assert(menu.searchScore(merged.items, entry, 'theme') < menu.searchScore(merged.items, entry, 'appearance'), 'menu scores name matches above description matches')
assertDeepEqual( assertDeepEqual(
menu.displayRow(merged.items, merged.itemOrder, {}, entry, 'Style', 12, 'search'), menu.displayRow(merged.items, merged.itemOrder, {}, {}, entry, 'Style', 12, 'search'),
{ {
itemId: 'style.theme', itemId: 'style.theme',
disabled: false,
kind: 'action', kind: 'action',
icon: '', icon: '',
iconFont: '', iconFont: '',
@@ -235,6 +255,40 @@ assert(
'menu always exposes every supported browser, terminal, and editor under Defaults' 'menu always exposes every supported browser, terminal, and editor under Defaults'
) )
assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI') assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI')
// Software you already have keeps its place in Install, dimmed rather than
// dropped, so the list reads as a catalog of what Omarchy can install.
// Chromium Account is the sole Install row with anything left to hide for, so
// any other `when:` here is a row that went back to vanishing once installed.
assertDeepEqual(
defaultItems
.filter(item => item.id.startsWith('install.') && item.action && item.when)
.map(item => item.id),
['install.service.chromium-account'],
'menu never hides an Install row because the software is already there'
)
assert(
['install.browser.zen', 'install.editor.vscode', 'install.gaming.steam', 'install.development.rust', 'install.windows'].every(
id => defaultById[id].disabled && !defaultById[id].when
),
'menu dims the Install rows for software that is already installed'
)
assertEqual(
defaultById['install.browser.zen'].disabled,
'omarchy-pkg-present zen-browser-bin',
'menu asks the same presence question it used to hide the row with'
)
// A guard can still be about something other than having the software: no
// Chromium at all means no account to wire up, and that row stays hidden.
assert(
defaultById['install.service.chromium-account'].when === '[[ -f ~/.config/chromium-flags.conf ]]'
&& defaultById['install.service.chromium-account'].disabled.includes('oauth2-client-id'),
'menu keeps hiding Chromium Account without Chromium, and dims it once the account is set up'
)
assert(
defaultItems.filter(item => item.id.startsWith('remove.')).every(item => !item.disabled)
&& defaultById['remove.browser.zen'].when === 'omarchy-pkg-present zen-browser-bin',
'menu still hides Remove rows for software that is not installed'
)
assert( assert(
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'), defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
'menu places Passwordless Sudo under Setup > Security' 'menu places Passwordless Sudo under Setup > Security'
@@ -367,6 +421,40 @@ assert(
/function select\(delta\)[\s\S]*root\.disarmPointer\(\)[\s\S]*selectedIndex =/.test(menuQml), /function select\(delta\)[\s\S]*root\.disarmPointer\(\)[\s\S]*selectedIndex =/.test(menuQml),
'menu keyboard navigation disarms pointer selection' 'menu keyboard navigation disarms pointer selection'
) )
// A dimmed row is not a target: the cursor steps over it, the pointer refuses
// to land on it, and neither Enter nor a click can reach it.
assert(
/function select\(delta\)[\s\S]*?var target = root\.nextSelectable\(from, delta\)\s*\n\s*if \(target < 0\) return/.test(menuQml),
'menu keyboard navigation skips disabled rows in the direction of travel'
)
assert(
/function rowSelectable\(index\)[\s\S]*?return !displayModel\.get\(index\)\.disabled/.test(menuQml),
'menu reads selectability off the row'
)
assert(
/function activateIndex\(index, fromPointer\)[\s\S]*?if \(!root\.rowSelectable\(index\)\) return/.test(menuQml),
'menu refuses to activate a disabled row'
)
assert(
/function selectFromPointer\(index, item, mouse\)[\s\S]*?if \(!root\.rowSelectable\(index\)\) return/.test(menuQml)
&& /onClicked: \{\s*\n\s*if \(row\.disabled\) return/.test(menuQml),
'menu leaves the cursor put when the pointer crosses a disabled row'
)
assert(
/opacity: row\.disabled \? 0\.4 : 1/.test(menuQml) && !/font\.italic/.test(menuQml),
'menu renders a disabled row faded, and leaves it at that'
)
assert(
/function rebuildDisplay\(\)[\s\S]*?root\.settleCursor\(\)/.test(menuQml),
'menu parks the cursor on a selectable row after the rows change'
)
// A menu with nothing selectable in it has no cursor, and Return must not
// conjure one onto a disabled row just because rows exist.
assert(
/function settleCursor\(\)[\s\S]*?root\.cursorActive = target >= 0/.test(menuQml)
&& /else if \(root\.cursorActive\) root\.activateIndex\(root\.selectedIndex\)\s*\n\s*else root\.settleCursor\(\)/.test(menuQml),
'menu ties the cursor to a selectable row existing, both ways'
)
assert( assert(
/function setFilter\(nextFilter\)[\s\S]*root\.disarmPointer\(\)/.test(menuQml), /function setFilter\(nextFilter\)[\s\S]*root\.disarmPointer\(\)/.test(menuQml),
'menu filter changes disarm pointer selection' 'menu filter changes disarm pointer selection'