Files
arthur-os/bin/omarchy-menu
T

822 lines
20 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Launch the Quickshell Omarchy menu or jump straight to a submenu/action
# omarchy:args=[menu-or-action|--json|--provider <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(",(?<tail>\\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 <<USAGE
Usage: omarchy-menu [menu-or-action]
omarchy-menu --json
omarchy-menu --provider <provider> [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 <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