#!/bin/bash # omarchy:summary=Launch the Quickshell Omarchy menu or jump straight to a submenu/action # omarchy:args=[menu-or-action|--json|--provider [menu-id]] # omarchy:examples=omarchy menu | omarchy menu setup | omarchy menu style.theme | omarchy menu --json | omarchy menu --provider fonts style.font set -o pipefail OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} DEFAULT_MENU="$OMARCHY_PATH/default/omarchy/omarchy-menu.jsonc" USER_MENU="$HOME/.config/omarchy/extensions/omarchy-menu.jsonc" # Menu hooks users may edit # Menu rows live in default/omarchy/omarchy-menu.jsonc. # Keep Bash here only for multi-step actions or truly runtime-generated lists. # Action helpers available to omarchy-menu.jsonc terminal() { xdg-terminal-exec --app-id=org.omarchy.terminal "$@" } present_terminal() { omarchy-launch-floating-terminal-with-presentation "$*" } open_in_editor() { omarchy-notification-send -u low "Editing config file" "$1" omarchy-launch-editor "$1" } hypr_config_file() { local name="$1" printf '%s/.config/hypr/%s.lua' "$HOME" "$name" } install() { present_terminal "echo 'Installing $1...'; omarchy-pkg-add $2" } install_and_launch() { present_terminal "echo 'Installing $1...'; omarchy-pkg-add $2 && setsid gtk-launch $3" } install_font() { present_terminal "echo 'Installing $1...'; omarchy-pkg-add $2 && sleep 2 && omarchy-font-set '$3'" } install_terminal() { present_terminal "omarchy-install-terminal $1" } show_custom_reminder_input() { local minutes minutes=$(omarchy-menu-input "Remind in minutes") if [[ $minutes =~ ^[0-9]+$ ]] && (( minutes > 0 )); then show_reminder_message_input "$minutes" elif [[ -n $minutes ]]; then omarchy-notification-send -g 󰔛 "Invalid reminder" "Enter the number of minutes" -u critical show_custom_reminder_input fi } show_reminder_message_input() { local minutes="$1" local message message=$(omarchy-menu-input "Reminder message") if [[ -n $message ]]; then omarchy-reminder "$minutes" "$message" else omarchy-reminder "$minutes" fi } get_webcam_list() { v4l2-ctl --list-devices 2>/dev/null | while IFS= read -r line; do if [[ $line != $'\t'* && -n $line ]]; then local name="$line" local device IFS= read -r device || break device=$(tr -d '\t' <<<"$device" | head -1) [[ -n $device ]] && echo "$device $name" fi done } show_webcam_select_menu() { local devices local count devices=$(get_webcam_list) if [[ -z $devices ]]; then count=0 else count=$(grep -c . <<<"$devices") fi if [[ -z $devices ]] || (( count == 0 )); then omarchy-notification-send "No webcam devices found" -u critical -t 3000 return 1 fi if (( count == 1 )); then printf '%s\n' "${devices%%[[:space:]]*}" else local selection selection=$(omarchy-menu-select "Select Webcam" "$devices") || return 1 printf '%s\n' "${selection%%[[:space:]]*}" fi } stop_active_screenrecording() { omarchy-capture-screenrecording --stop-recording } screenrecord_with_webcam() { local device device=$(show_webcam_select_menu) || return 1 omarchy-capture-screenrecording --with-desktop-audio --with-microphone-audio --with-webcam --webcam-device="$device" } # Runtime-generated providers users may edit # Use provider:"fonts" only when a submenu calls provider_fonts (or a command) # to return JSON. Normal submenus are inferred from dotted JSONC ids. provider_fonts() { local menu_id="$1" local current_font local index=0 local font local label current_font=$(omarchy-font-current 2>/dev/null) while IFS= read -r font; do [[ -n $font ]] || continue (( index++ )) label="$font$(current_marker "$font" "$current_font")" json_item "$menu_id.$index-$(slugify "$font")" "" "$label" "omarchy-font-set $(shell_quote "$font")" "$font typeface" done < <(omarchy-font-list 2>/dev/null) } provider_power_profiles() { local menu_id="$1" local current_profile local profile current_profile=$(powerprofilesctl get 2>/dev/null) while IFS= read -r profile; do [[ -n $profile ]] || continue json_item "$menu_id.$(slugify "$profile")" "󱐋" "$profile$(current_marker "$profile" "$current_profile")" "powerprofilesctl set $(shell_quote "$profile")" "$profile power profile" done < <(omarchy-powerprofiles-list 2>/dev/null) } # Framework helpers below this line # Most menu changes should not require editing below here. # Generic helpers shell_quote() { printf "'%s'" "${1//\'/\'\\\'\'}" } slugify() { local value="${1,,}" value="${value//[^a-z0-9]/-}" while [[ $value == *--* ]]; do value="${value//--/-}" done while [[ $value == -* ]]; do value="${value#-}" done while [[ $value == *- ]]; do value="${value%-}" done printf '%s' "${value:-item}" } current_marker() { if [[ $1 == $2 ]]; then printf ' ✓' fi } declare -A CMD_PRESENT_CACHE=() declare -A STATUS_CACHE=() cached_status() { local key="$1" shift if [[ -z ${STATUS_CACHE[$key]+x} ]]; then "$@" >/dev/null 2>&1 STATUS_CACHE[$key]=$? fi return "${STATUS_CACHE[$key]}" } omarchy-cmd-present() { local cmd for cmd in "$@"; do if [[ -z ${CMD_PRESENT_CACHE[$cmd]+x} ]]; then if command -v "$cmd" >/dev/null 2>&1; then CMD_PRESENT_CACHE[$cmd]=0 else CMD_PRESENT_CACHE[$cmd]=1 fi fi (( CMD_PRESENT_CACHE[$cmd] == 0 )) || return 1 done return 0 } omarchy-toggle-enabled() { cached_status "toggle:$*" command omarchy-toggle-enabled "$@" } omarchy-hibernation-available() { cached_status hibernation command omarchy-hibernation-available } omarchy-hw-hybrid-gpu() { cached_status hw-hybrid-gpu command omarchy-hw-hybrid-gpu } omarchy-hw-touchpad() { cached_status hw-touchpad command omarchy-hw-touchpad } omarchy-hw-touchscreen() { cached_status hw-touchscreen command omarchy-hw-touchscreen } omarchy-hw-dell-xps-haptic-touchpad() { cached_status hw-dell-xps-haptic-touchpad command omarchy-hw-dell-xps-haptic-touchpad } default_browser_is() { if [[ -z ${DEFAULT_BROWSER_CACHE+x} ]]; then DEFAULT_BROWSER_CACHE=$(command omarchy-default-browser 2>/dev/null || true) fi [[ $DEFAULT_BROWSER_CACHE == "$1" ]] } default_terminal_is() { if [[ -z ${DEFAULT_TERMINAL_CACHE+x} ]]; then DEFAULT_TERMINAL_CACHE=$(command omarchy-default-terminal 2>/dev/null || true) fi [[ $DEFAULT_TERMINAL_CACHE == "$1" ]] } default_editor_is() { if [[ -z ${DEFAULT_EDITOR_CACHE+x} ]]; then DEFAULT_EDITOR_CACHE=$(command omarchy-default-editor 2>/dev/null || true) fi [[ $DEFAULT_EDITOR_CACHE == "$1" ]] } haptic_touchpad_is() { if [[ -z ${HAPTIC_TOUCHPAD_CACHE+x} ]]; then HAPTIC_TOUCHPAD_CACHE=$(command dell-xps-touchpad-haptics get 2>/dev/null || true) fi [[ $HAPTIC_TOUCHPAD_CACHE == "$1" ]] } json_string() { local value="${1-}" value="${value//\\/\\\\}" value="${value//\"/\\\"}" value="${value//$'\t'/\\t}" value="${value//$'\r'/\\r}" value="${value//$'\n'/\\n}" printf '"%s"' "$value" } json_item() { local id="$1" local icon="$2" local label="$3" local action="${4:-}" local keywords="${5:-}" local description="${6:-}" local target="${7:-}" local provider="${8:-}" printf '{"id": ' json_string "$id" printf ', "icon": ' json_string "$icon" printf ', "label": ' json_string "$label" if [[ -n $action ]]; then printf ', "action": ' json_string "$action" fi if [[ -n $keywords ]]; then printf ', "keywords": ' json_string "$keywords" fi if [[ -n $description ]]; then printf ', "description": ' json_string "$description" fi if [[ -n $target ]]; then printf ', "target": ' json_string "$target" fi if [[ -n $provider ]]; then printf ', "provider": ' json_string "$provider" fi printf '}\n' } # Menu definition loading menu_sources() { local path for path in "$DEFAULT_MENU" "$USER_MENU"; do [[ -f $path ]] || continue jq -Rrs ' # Our menu files are JSONC: full-line // comments plus trailing commas. # Keep parsing in jq so the menu build path stays fast and dependency-free. gsub("(?m)^\\s*//[^\\n]*(\\n|$)"; "") | gsub(",(?\\s*[}\\]])"; "\(.tail)") | fromjson | if type == "object" then (if has("items") and (.items | type) == "object" then .items else . end) else {} end | to_entries[] | select(.value | type == "object") | .value + { id: .key } ' "$path" done } normalize_menu_json() { jq -sc ' def aliases_array: if ((.aliases // []) | type) == "array" then (.aliases // []) elif ((.aliases // "") != "") then [(.aliases // "")] else [] end; def normalize_item: def inferred_parent: if has("parent") then .parent elif (.id | contains(".")) then (.id | split(".")[:-1] | join(".")) else "root" end; def inferred_kind: if ((.action // "") != "") then "action" elif ((.target // "") != "") then "link" else "menu" end; def id_keywords: .id | gsub("[._-]+"; " "); def alias_keywords: aliases_array | map(gsub("[._-]+"; " ")) | join(" "); def keyword_string: ([id_keywords, alias_keywords, (.keywords // "")] | join(" ") | split(" ") | map(select(. != ""))) as $terms | reduce $terms[] as $term ([]; if index($term) then . else . + [$term] end) | join(" "); { id, parent: (if .id == "root" then "" else inferred_parent end), kind: inferred_kind, icon: (.icon // ""), label: (.label // .id), target: (.target // ""), keywords: keyword_string, description: (.description // ""), action: (.action // ""), provider: (.provider // ""), aliases: aliases_array, when: (.when // ""), checked: (.checked // "") }; reduce (.[] | select(.id)) as $item ({ order: [], by_id: {} }; if .by_id[$item.id] == null then .order += [$item.id] else . end | .by_id[$item.id] = ((.by_id[$item.id] // {}) + $item) ) | { items: [ .order[] as $id | (.by_id[$id] + { id: $id }) | normalize_item ] } ' } menu_all_json() { if [[ -z ${MENU_ALL_JSON_CACHE+x} ]]; then MENU_ALL_JSON_CACHE=$(menu_sources | normalize_menu_json) || return fi printf '%s\n' "$MENU_ALL_JSON_CACHE" } menu_static_json() { if [[ -z ${MENU_STATIC_JSON_CACHE+x} ]]; then MENU_STATIC_JSON_CACHE=$(menu_all_json | jq ' (.items | map({ key: .id, value: . }) | from_entries) as $by_id | def ancestor_ids($id): ($id | split(".")) as $parts | [range(1; $parts | length) | $parts[0:.] | join(".")]; def has_provider_ancestor($id): any(ancestor_ids($id)[]; (($by_id[.] // {}).provider // "") != ""); { items: [.items[] | select(has_provider_ancestor(.id) | not)] } ') || return fi printf '%s\n' "$MENU_STATIC_JSON_CACHE" } emit_config_items() { local fields local id local icon local label local action local keywords local description local target local provider local when local checked while IFS= read -r fields; do eval "set -- $fields" id="$1" icon="$2" label="$3" action="$4" keywords="$5" description="$6" target="$7" provider="$8" when="$9" checked="${10}" [[ -n $id ]] || continue if [[ -n $when ]] && ! eval "$when" >/dev/null 2>&1; then continue fi if [[ -n $checked ]] && eval "$checked" >/dev/null 2>&1; then label="$label ✓" fi json_item "$id" "$icon" "$label" "$action" "$keywords" "$description" "$target" "$provider" done < <(jq -r '.items[] | [ (.id // ""), (.icon // ""), (.label // .id // ""), (.action // ""), (.keywords // ""), (.description // ""), (.target // ""), (.provider // ""), (.when // ""), (.checked // "") ] | @sh') } menu_evaluated_json() { if [[ -z ${MENU_EVALUATED_JSON_CACHE+x} ]]; then MENU_EVALUATED_JSON_CACHE=$(menu_all_json | emit_config_items | normalize_menu_json) || return fi printf '%s\n' "$MENU_EVALUATED_JSON_CACHE" } menu_json() { if [[ -z ${MENU_JSON_CACHE+x} ]]; then MENU_JSON_CACHE=$(menu_static_json | emit_config_items | normalize_menu_json) || return fi printf '%s\n' "$MENU_JSON_CACHE" } # Provider dispatch provider_mount_id() { local provider="$1" menu_all_json | jq -r --arg provider "$provider" '.items[] | select(.provider == $provider) | .id' | head -n 1 } provider_json() { local provider="$1" local menu_id="${2:-}" local provider_function [[ -n $provider ]] || return 1 menu_id=${menu_id:-$(provider_mount_id "$provider")} [[ -n $menu_id ]] || return 1 if [[ $provider =~ ^[a-z0-9-]+$ ]]; then provider_function="provider_${provider//-/_}" case "$provider_function" in provider_mount_id | provider_json) ;; *) if declare -F "$provider_function" >/dev/null; then "$provider_function" "$menu_id" | normalize_menu_json return fi ;; esac if omarchy-cmd-present "$provider"; then "$provider" "$menu_id" | normalize_menu_json return fi fi echo "omarchy-menu: unknown provider '$provider'" >&2 return 1 } # Routes and actions route_target() { local route="${1,,}" local target local all_json route="${route//_/-}" if [[ -z $route || $route == "go" || $route == "menu" ]]; then printf 'root' return fi all_json=$(menu_all_json) || return target=$(jq -r --arg route "$route" ' def aliases: if (.aliases | type) == "array" then .aliases elif (.aliases | type) == "string" then [.aliases] else [] end; first(.items[] | select(any(aliases[]?; (ascii_downcase | gsub("_"; "-")) == $route)) | .id) // empty ' <<<"$all_json") || return printf '%s' "${target:-$route}" } route_field() { local target="$1" local field="$2" local all_json local fields local when local value all_json=$(menu_all_json) || return fields=$(jq -r --arg id "$target" --arg field "$field" ' first(.items[] | select(.id == $id) | [(.when // ""), (.[$field] // "")] | @sh) // empty ' <<<"$all_json") || return [[ -n $fields ]] || return eval "set -- $fields" when="$1" value="$2" if [[ -n $when ]] && ! eval "$when" >/dev/null 2>&1; then return fi printf '%s' "$value" } route_kind() { local target="$1" if [[ $target == "root" ]]; then printf 'menu' return fi route_field "$target" kind } route_link_target() { route_field "$1" target } route_action() { route_field "$1" action } run_action() { local id="$1" local action="${2:-}" if [[ -z $action ]]; then action=$(route_action "$id") || return 0 fi [[ -n $action ]] || return 0 eval "$action" } # Quickshell launcher ensure_menu_style_file() { local toggles_dir="$HOME/.local/state/omarchy/toggles" local style_file="$toggles_dir/quickshell-menu.json" local radius=0 [[ -f $style_file ]] && return mkdir -p "$toggles_dir" if [[ -f $toggles_dir/walker.css ]] && grep -q 'border-radius: 6px;' "$toggles_dir/walker.css"; then radius=6 fi printf '{ "radius": %s }\n' "$radius" >"$style_file" } usage() { cat < [menu-id] Launch the Quickshell Omarchy menu. Pass a menu name (install, style, system, capture, toggle, etc.) to open there, or an action id to run it directly. Options: --json Print the static menu JSON used by Quickshell --provider [id] Print JSON rows from a runtime provider -h, --help Show this help USAGE } menu_layer_visible() { hyprctl layers -j 2>/dev/null | jq -e '.. | objects | select(.namespace? == "omarchy-menu")' >/dev/null } menu_lock_path() { printf '%s/omarchy-menu.lock' "${XDG_RUNTIME_DIR:-/run/user/$UID}" } menu_payload_json() { local menu_file="$1" local initial_menu="$2" local selection_file="$3" local done_file="$4" local colors_payload_base64="$5" local font_family="$6" jq -nc \ --arg menu_file "$menu_file" \ --arg initial_menu "$initial_menu" \ --arg selection_file "$selection_file" \ --arg done_file "$done_file" \ --arg colors_payload_base64 "$colors_payload_base64" \ --arg font_family "$font_family" \ '{ menuJsonFile: $menu_file, initialMenu: $initial_menu, selectionFile: $selection_file, doneFile: $done_file, colorsRawBase64: $colors_payload_base64, fontFamily: $font_family }' } close_visible_quickshell_menu() { menu_layer_visible || return 1 omarchy-shell-ipc shell hide omarchy.menu >/dev/null 2>&1 || return 1 } open_quickshell_menu() { local initial_menu="$1" local menu_payload local colors_file="$HOME/.config/omarchy/current/theme/colors.toml" local colors_payload_base64="" local font_family local menu_file="" local selection_file local done_file local selected_id local selected_action local menu_started=false local lock_path local lock_fd local summon_result local payload lock_path=$(menu_lock_path) exec {lock_fd}>"$lock_path" || return 1 if ! flock -n "$lock_fd"; then close_visible_quickshell_menu && return 0 flock "$lock_fd" || return 1 fi if close_visible_quickshell_menu; then return 0 fi ensure_menu_style_file selection_file=$(mktemp) done_file=$(mktemp) rm -f "$done_file" trap "rm -f $(shell_quote "$selection_file") $(shell_quote "$done_file")" EXIT menu_payload=$(menu_json) || return if [[ $initial_menu != "root" ]] && ! jq -e --arg id "$initial_menu" '.items[] | select(.id == $id)' <<<"$menu_payload" >/dev/null; then menu_payload=$(menu_evaluated_json) || return fi if [[ -f $colors_file ]]; then colors_payload_base64=$(base64 -w0 "$colors_file") fi font_family=$(omarchy-font-current 2>/dev/null || true) font_family=${font_family:-monospace} menu_file=$(mktemp) printf '%s' "$menu_payload" >"$menu_file" trap "rm -f $(shell_quote "$menu_file") $(shell_quote "$selection_file") $(shell_quote "$done_file")" EXIT payload=$(menu_payload_json "$menu_file" "$initial_menu" "$selection_file" "$done_file" "$colors_payload_base64" "$font_family") || return summon_result=$(omarchy-shell-ipc shell summon omarchy.menu "$payload" 2>/dev/null || true) if [[ $summon_result != "ok" ]]; then echo "omarchy-menu: Omarchy shell did not accept the menu request" >&2 return 1 fi for (( i = 0; i < 500; i++ )); do if [[ -e $done_file ]] || menu_layer_visible; then menu_started=true break fi sleep 0.01 done if [[ $menu_started != true ]]; then omarchy-shell-ipc shell hide omarchy.menu >/dev/null 2>&1 || true echo "omarchy-menu: Quickshell menu did not open" >&2 return 1 fi while [[ ! -e $done_file ]]; do menu_layer_visible || break sleep 0.01 done if [[ -s $selection_file ]]; then IFS=$'\t' read -r selected_id selected_action <"$selection_file" exec {lock_fd}>&- run_action "$selected_id" "$selected_action" fi } # CLI dispatch case "${1:-}" in --help | -h) usage exit 0 ;; --json) menu_json || exit $? exit 0 ;; --provider) provider_json "${2:-}" "${3:-}" exit $? ;; screenrecord | screenrecording | screen-record) close_visible_quickshell_menu || true stop_active_screenrecording && exit 0 ;; esac if [[ -z ${1:-} ]]; then open_quickshell_menu root exit $? fi target=$(route_target "${1:-}") || exit $? kind=$(route_kind "$target") || exit $? case "$kind" in action) close_visible_quickshell_menu || true run_action "$target" ;; menu) open_quickshell_menu "$target" ;; link) open_quickshell_menu "$(route_link_target "$target")" ;; *) open_quickshell_menu root ;; esac