Drop walker + elephant

This commit is contained in:
David Heinemeier Hansson
2026-05-19 20:54:20 +02:00
parent 7fe472bf8a
commit abf0a68b54
64 changed files with 1024 additions and 1033 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ Do not define:
false flags or empty args
Examples:
# omarchy:summary=Restart Walker and related user services
# omarchy:summary=Restart the shell
# omarchy:summary=Take a screenshot
# omarchy:group=capture
-1
View File
@@ -18,7 +18,6 @@ if [[ -f $FIRST_RUN_MODE ]]; then
bash "$OMARCHY_PATH/install/first-run/gnome-theme.sh"
bash "$OMARCHY_PATH/install/first-run/gdk-scale.sh"
bash "$OMARCHY_PATH/install/first-run/gtk-primary-paste.sh"
bash "$OMARCHY_PATH/install/first-run/elephant.sh"
omarchy-hook-install post-update "$OMARCHY_PATH/install/first-run/install-voxtype.hook"
sudo rm -f /etc/sudoers.d/first-run
-1
View File
@@ -62,7 +62,6 @@ install_admin_webapp() {
mkdir -p "$WEBAPP_ICON_DIR"
cp "$SUNSHINE_ICON_SOURCE" "$WEBAPP_ICON_DIR/$SUNSHINE_ICON_NAME"
omarchy-webapp-install "$SUNSHINE_ADMIN_APP" "$SUNSHINE_ADMIN_URL" "$SUNSHINE_ICON_NAME" "$SUNSHINE_ADMIN_EXEC"
omarchy-restart-walker
}
enable_hyprland_autostart() {
-3
View File
@@ -14,9 +14,6 @@ if omarchy-toggle-enabled screensaver-off && [[ $1 != "force" ]]; then
exit 1
fi
# Silently quit Walker on overlay
walker -q
focused=$(omarchy-hyprland-monitor-focused)
terminal=$(xdg-terminal-exec --print-id)
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# omarchy:summary=Launch Walker and ensure its Elephant data provider is running
if ! pgrep -x elephant > /dev/null; then
setsid uwsm-app -- elephant &
fi
# Ensure walker service is running
if ! pgrep -f "walker --gapplication-service" > /dev/null; then
setsid uwsm-app -- env GSK_RENDERER=cairo walker --gapplication-service &
fi
exec walker --width 644 --maxheight 300 --minheight 300 "$@"
+5 -5
View File
@@ -1,15 +1,15 @@
#!/bin/bash
# omarchy:summary=Pick a file with Walker
# omarchy:summary=Pick a file from a menu
# omarchy:group=menu
# omarchy:name=file
# omarchy:args=label paths formats [walker args...]
# omarchy:args=label paths formats [menu args...]
# omarchy:examples=omarchy menu file "Select image" "$HOME/Pictures" "jpg png webp"|omarchy-menu-file "Select media" "$HOME/Pictures:$HOME/Videos" "jpg png mp4 mov" --width 800
set -euo pipefail
if (( $# < 3 )); then
echo "Usage: omarchy-menu-file <label> <paths> <formats> [walker args...]" >&2
echo "Usage: omarchy-menu-file <label> <paths> <formats> [menu args...]" >&2
exit 1
fi
@@ -22,7 +22,7 @@ IFS=: read -r -a paths <<<"$paths_arg"
read -r -a formats <<<"$formats_arg"
if (( ${#paths[@]} == 0 || ${#formats[@]} == 0 )); then
echo "Usage: omarchy-menu-file <label> <paths> <formats> [walker args...]" >&2
echo "Usage: omarchy-menu-file <label> <paths> <formats> [menu args...]" >&2
exit 1
fi
@@ -45,4 +45,4 @@ done
find_args+=(")" -printf '%T@\t%p\n')
find "${find_args[@]}" 2>/dev/null | sort -rn | cut -f2- |
omarchy-launch-walker --dmenu --width 800 --minheight 1 --maxheight 500 -p "$label…" "$@" 2>/dev/null
omarchy-menu-select "$label" -- --width 800 --maxheight 500 "$@"
+45 -3
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# omarchy:summary=Prompt for text input with Walker
# omarchy:summary=Prompt for text input from a menu
# omarchy:group=menu
# omarchy:name=input
# omarchy:args=prompt [walker args...]
# omarchy:args=prompt [menu args...]
# omarchy:examples=omarchy menu input Reminder|omarchy-menu-input "Reminder in minutes" --width 400
set -euo pipefail
@@ -13,4 +13,46 @@ if (( $# > 0 )); then
shift
fi
omarchy-launch-walker --dmenu --inputonly --width 295 --minheight 1 --maxheight 1 -p "$prompt…" "$@" 2>/dev/null
menu_width=""
while (( $# > 0 )); do
case "$1" in
--width)
shift
if (( $# == 0 )); then
echo "omarchy-menu-input: --width requires a value" >&2
exit 1
fi
menu_width="$1"
;;
esac
shift
done
selection_file=$(mktemp)
done_file=$(mktemp)
rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file"' EXIT
payload=$(perl -MEncode=decode -MJSON::PP=encode_json -e '
my $payload = {
mode => "input",
prompt => decode("UTF-8", $ARGV[0]),
selectionFile => decode("UTF-8", $ARGV[1]),
doneFile => decode("UTF-8", $ARGV[2])
};
$payload->{width} = int($ARGV[3]) if length($ARGV[3] // "");
print encode_json($payload)
' "$prompt" "$selection_file" "$done_file" "$menu_width")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null
while [[ ! -e $done_file ]]; do
sleep 0.05
done
if [[ -s $selection_file ]]; then
cat "$selection_file"
else
exit 1
fi
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Display Hyprland keybindings defined in your configuration using walker for an interactive search menu.
# omarchy:summary=Display Hyprland keybindings defined in your configuration using an interactive search menu.
declare -A KEYCODE_SYM_MAP
# Hyprland reports XKB keycodes for code: bindings. Keep a small fallback for
@@ -600,7 +600,7 @@ else
monitor_height=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .height')
menu_height=$((monitor_height * 40 / 100))
selection=$(cut -f1 <<<"$records" |
walker --dmenu -p 'Keybindings' --width 800 --height "$menu_height")
omarchy-menu-select 'Keybindings' -- --width 800 --height "$menu_height")
if [[ -n $selection ]]; then
record=$(awk -F '\t' -v selection="$selection" '$1 == selection { print; exit }' <<<"$records")
+63 -8
View File
@@ -1,15 +1,15 @@
#!/bin/bash
# omarchy:summary=Pick one option with Walker
# omarchy:summary=Pick one option from a menu
# omarchy:group=menu
# omarchy:name=select
# omarchy:args=prompt option... [-- walker args...]
# omarchy:args=prompt [option...] [-- menu args...]
# omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400
set -euo pipefail
if (( $# < 2 )); then
echo "Usage: omarchy-menu-select <prompt> <option>... [-- walker args...]" >&2
if (( $# < 1 )); then
echo "Usage: omarchy-menu-select <prompt> [option...] [-- menu args...]" >&2
exit 1
fi
@@ -17,12 +17,34 @@ prompt="$1"
shift
options=()
walker_args=()
menu_width=""
menu_maxheight=""
while (( $# > 0 )); do
if [[ $1 == "--" ]]; then
shift
walker_args=("$@")
while (( $# > 0 )); do
case "$1" in
--width)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --width requires a value" >&2
exit 1
fi
menu_width="$1"
;;
--height|--maxheight)
arg="$1"
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: $arg requires a value" >&2
exit 1
fi
menu_maxheight="$1"
;;
esac
shift
done
break
fi
@@ -30,9 +52,42 @@ while (( $# > 0 )); do
shift
done
if (( ${#options[@]} == 0 )) && [[ ! -t 0 ]]; then
mapfile -t options
fi
if (( ${#options[@]} == 0 )); then
echo "Usage: omarchy-menu-select <prompt> <option>... [-- walker args...]" >&2
echo "Usage: omarchy-menu-select <prompt> [option...] [-- menu args...]" >&2
exit 1
fi
printf '%s\n' "${options[@]}" | omarchy-launch-walker --dmenu --width 295 --minheight 1 --maxheight 300 -p "$prompt…" "${walker_args[@]}" 2>/dev/null
selection_file=$(mktemp)
done_file=$(mktemp)
rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file"' EXIT
options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}")
payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e '
my $payload = {
mode => "select",
prompt => decode("UTF-8", $ARGV[0]),
options => decode_json($ARGV[1]),
selectionFile => decode("UTF-8", $ARGV[2]),
doneFile => decode("UTF-8", $ARGV[3])
};
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
print encode_json($payload)
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null
while [[ ! -e $done_file ]]; do
sleep 0.05
done
if [[ -s $selection_file ]]; then
cat "$selection_file"
else
exit 1
fi
+2 -7
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Display Tmux keybindings defined in your configuration using walker for an interactive search menu.
# omarchy:summary=Display Tmux keybindings defined in your configuration using an interactive search menu.
# omarchy:args=[--print|-p] [--config <path>]
print_only=false
@@ -233,11 +233,6 @@ fi
records=$(output_keybindings)
if [[ -z $WAYLAND_DISPLAY ]] || omarchy-cmd-missing walker; then
printf '%s\n' "$records"
exit 0
fi
monitor_height=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.focused == true) | .height' 2>/dev/null)
if [[ ! $monitor_height =~ ^[0-9]+$ ]] || ((monitor_height <= 0)); then
@@ -245,4 +240,4 @@ if [[ ! $monitor_height =~ ^[0-9]+$ ]] || ((monitor_height <= 0)); then
fi
menu_height=$((monitor_height * 40 / 100))
printf '%s\n' "$records" | walker --dmenu -p 'Tmux keybindings' --width 800 --height "$menu_height" >/dev/null
printf '%s\n' "$records" | omarchy-menu-select 'Tmux keybindings' -- --width 800 --height "$menu_height" >/dev/null || true
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
# omarchy:summary=Overwrite the user configs for the Walker application launcher (which also powers the Omarchy Menu) and restart the services.
mkdir -p ~/.config/autostart/
cp $OMARCHY_PATH/default/walker/walker.desktop ~/.config/autostart/
# And restarts if it crashes or is killed
mkdir -p ~/.config/systemd/user/app-walker@autostart.service.d/
cp $OMARCHY_PATH/default/walker/restart.conf ~/.config/systemd/user/app-walker@autostart.service.d/restart.conf
systemctl --user daemon-reload
# Refresh configs
omarchy-refresh-config walker/config.toml
omarchy-refresh-config elephant/calc.toml
omarchy-refresh-config elephant/desktopapplications.toml
# Link all elephant menus
mkdir -p ~/.config/elephant/menus
for menu in $OMARCHY_PATH/default/elephant/*.lua; do
ln -snf "$menu" ~/.config/elephant/menus/"$(basename "$menu")"
done
# Restart service
omarchy-restart-walker
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
# omarchy:summary=Restart Walker and related user services
restart_services() {
if systemctl --user is-enabled elephant.service &>/dev/null; then
systemctl --user restart elephant.service
fi
if systemctl --user is-enabled app-walker@autostart.service &>/dev/null; then
systemctl --user restart app-walker@autostart.service
else
echo -e "\e[31mUnable to restart Walker -- RESTART MANUALLY\e[0m"
fi
}
if (( EUID == 0 )); then
SCRIPT_OWNER=$(stat -c '%U' "$0")
USER_UID=$(id -u "$SCRIPT_OWNER")
systemd-run --uid="$SCRIPT_OWNER" --setenv=XDG_RUNTIME_DIR="/run/user/$USER_UID" \
bash -c "$(declare -f restart_services); restart_services"
else
restart_services
fi
+3 -3
View File
@@ -74,9 +74,9 @@ perl -MCwd=abs_path -MEncode=encode,decode -MSocket -MIO::Handle \
my $shell_qml_arg = shift @ARGV;
my $shell_qml = abs_path($shell_qml_arg);
my $target = shift @ARGV;
my $function = shift @ARGV;
my @args = @ARGV;
my $target = decode("UTF-8", shift @ARGV);
my $function = decode("UTF-8", shift @ARGV);
my @args = map { decode("UTF-8", $_) } @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring($target) . qstring($function) . pack("N", scalar @args) . join("", map { qstring($_) } @args);
my @candidates;
+3 -5
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Set Hyprland and Walker corners to sharp or round (the shell mirrors Hyprland)
# omarchy:summary=Set Hyprland corners to sharp or round (the shell mirrors Hyprland)
# omarchy:args=<sharp|round>
# omarchy:examples=omarchy style corners round | omarchy style corners sharp
@@ -10,11 +10,9 @@ if [[ $1 != "sharp" && $1 != "round" ]]; then
fi
# The shell tracks Hyprland's `decoration:rounding` directly via
# Quickshell's qs.Commons.Style singleton; no separate quickshell-corners
# state file is written. The old omarchy-style-corners-quickshell shim is
# kept as a no-op so external callers don't error.
# Quickshell's qs.Commons.Style singleton; no separate shell state file is
# written.
omarchy-style-corners-hyprland "$1"
omarchy-style-corners-walker "$1"
case $1 in
sharp) omarchy-notification-send "Sharp corners enabled" -g 󰝣 ;;
+5 -5
View File
@@ -4,11 +4,11 @@
# omarchy:args=[toggle|sharp|round]
# omarchy:examples=omarchy style corners quickshell toggle | omarchy style corners quickshell round | omarchy style corners quickshell sharp
# qs.Commons.Style reads decoration:rounding from hyprctl and watches the
# theme name + rounded-corners toggle files for changes. There is no
# separate quickshell-menu.json to write anymore. This shim stays around
# so anyone still calling the old subcommand (or the dispatcher in
# omarchy-style-corners) doesn't error out.
# qs.Commons.Style reads decoration:rounding from hyprctl. Theme changes
# refresh it through shell IPC, while user corner toggles still trigger their
# own refresh. There is no separate quickshell-menu.json to write anymore.
# This shim stays around so anyone still calling the old subcommand (or the
# dispatcher in omarchy-style-corners) doesn't error out.
case "${1:-toggle}" in
round|sharp|toggle) exit 0 ;;
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
# omarchy:summary=Set or toggle rounded Walker corners
# omarchy:args=[toggle|sharp|round]
# omarchy:examples=omarchy style corners walker toggle | omarchy style corners walker round | omarchy style corners walker sharp
TOGGLES_CSS="$HOME/.local/state/omarchy/toggles/walker.css"
set_radius() {
local radius="$1"
mkdir -p "$(dirname "$TOGGLES_CSS")"
printf '.box-wrapper { border-radius: %spx; }\n' "$radius" >"$TOGGLES_CSS"
}
case "${1:-toggle}" in
round) set_radius 6 ;;
sharp) set_radius 0 ;;
toggle)
if [[ -f $TOGGLES_CSS ]] && grep -q 'border-radius: 6px;' "$TOGGLES_CSS"; then
set_radius 0
else
set_radius 6
fi
;;
*)
echo "Usage: omarchy-style-corners-walker [toggle|sharp|round]"
exit 1
;;
esac
omarchy-restart-walker
+2 -6
View File
@@ -122,10 +122,6 @@ fi
exec 9>"$THEME_SET_LOCK"
flock 9
# Prevent file-watch reloads from changing shell colors before the background
# reveal is ready. The theme IPC below resumes reloads as it applies colors.
shell_ipc shell suspendThemeReloads || true
# Setup clean next theme directory (for atomic theme config swapping)
rm -rf "$NEXT_THEME_PATH"
mkdir -p "$NEXT_THEME_PATH"
@@ -154,8 +150,8 @@ mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"
# Store theme name for reference
echo "$THEME_NAME" >"$HOME/.config/omarchy/current/theme.name"
# Make the running shell pick up the new palette immediately instead of waiting
# for file-watch debounce while the rest of the theme hooks run.
# Make the running shell pick up the new palette immediately while the rest of
# the theme hooks run.
colors_payload=$([[ -f $CURRENT_THEME_PATH/colors.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/colors.toml")
shell_payload=$([[ -f $CURRENT_THEME_PATH/shell.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/shell.toml")
if [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]]; then
+1 -1
View File
@@ -13,7 +13,7 @@ usage() {
Usage:
omarchy transcode [--path path] [input] [format] [resolution]
With no input, pick a picture or video with Walker from ~/Pictures and ~/Videos,
With no input, pick a picture or video from ~/Pictures and ~/Videos,
or only from --path when provided. Then pick the output format and resolution.
Options:
+3 -1
View File
@@ -45,4 +45,6 @@ for APP_NAME in "${APP_NAMES[@]}"; do
echo "Removed $APP_NAME"
done
omarchy-restart-walker
if command -v update-desktop-database &>/dev/null; then
update-desktop-database "$DESKTOP_DIR" &>/dev/null || true
fi
-2
View File
@@ -32,6 +32,4 @@ if command -v update-desktop-database &>/dev/null; then
update-desktop-database "$APP_DIR" &>/dev/null || true
fi
omarchy-restart-walker
echo "Web apps removed successfully."
-1
View File
@@ -1 +0,0 @@
async = false
-3
View File
@@ -1,3 +0,0 @@
show_actions = false
only_search_title = true
history = false
-1
View File
@@ -1 +0,0 @@
command = 'wl-copy && ((hyprctl dispatch "hl.dsp.send_key_state({ mods = \"SHIFT\", key = \"Insert\", state = \"down\", window = \"activewindow\" })" && sleep 0.05 && hyprctl dispatch "hl.dsp.send_key_state({ mods = \"SHIFT\", key = \"Insert\", state = \"up\", window = \"activewindow\" })") || hyprctl dispatch sendshortcut "SHIFT, Insert, activewindow")'
-49
View File
@@ -1,49 +0,0 @@
force_keyboard_focus = true # forces keyboard forcus to stay in Walker
selection_wrap = true # wrap list if at bottom or top
theme = "omarchy-default" # theme to use
additional_theme_location = "~/.local/share/omarchy/default/walker/themes/"
hide_action_hints = true # globally hide the action hints
[placeholders]
"default" = { input = " Search...", list = "No Results" } # placeholders for input and empty list, key is the providers name, so f.e. "desktopapplications" or "menus:other"
[keybinds]
quick_activate = []
[columns]
symbols = 1 # providers to be queried by default
[providers]
max_results = 256 # 256 should be enough for everyone
default = [
"desktopapplications",
"websearch",
]
[[providers.prefixes]]
prefix = "/"
provider = "providerlist"
[[providers.prefixes]]
prefix = "."
provider = "files"
[[providers.prefixes]]
prefix = ":"
provider = "symbols"
[[providers.prefixes]]
prefix = "="
provider = "calc"
[[providers.prefixes]]
prefix = "@"
provider = "websearch"
[[providers.prefixes]]
prefix = "$"
provider = "clipboard"
[[emergencies]]
text = "Restart Walker"
command = "omarchy-restart-walker"
@@ -1,73 +0,0 @@
Name = "omarchyBackgroundSelector"
NamePretty = "Omarchy Background Selector"
Cache = false
HideFromProviderlist = true
SearchName = true
local function ShellEscape(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
function FormatName(filename)
-- Remove leading number and dash
local name = filename:gsub("^%d+", ""):gsub("^%-", "")
-- Remove extension
name = name:gsub("%.[^%.]+$", "")
-- Replace dashes with spaces
name = name:gsub("-", " ")
-- Capitalize each word
name = name:gsub("%S+", function(word)
return word:sub(1, 1):upper() .. word:sub(2):lower()
end)
return name
end
function GetEntries()
local entries = {}
local home = os.getenv("HOME")
-- Read current theme name
local theme_name_file = io.open(home .. "/.config/omarchy/current/theme.name", "r")
local theme_name = theme_name_file and theme_name_file:read("*l") or nil
if theme_name_file then
theme_name_file:close()
end
-- Directories to search
local dirs = {
home .. "/.config/omarchy/current/theme/backgrounds",
}
if theme_name then
table.insert(dirs, home .. "/.config/omarchy/backgrounds/" .. theme_name)
end
-- Track added files to avoid duplicates
local seen = {}
for _, background_dir in ipairs(dirs) do
local handle = io.popen(
"find -L " .. ShellEscape(background_dir)
.. " -maxdepth 1 -type f \\( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' -o -name '*.gif' -o -name '*.bmp' -o -name '*.webp' \\) 2>/dev/null | sort"
)
if handle then
for background in handle:lines() do
local filename = background:match("([^/]+)$")
if filename and not seen[filename] then
seen[filename] = true
table.insert(entries, {
Text = FormatName(filename),
Value = background,
Actions = {
activate = "omarchy-theme-bg-set " .. ShellEscape(background),
},
Preview = background,
PreviewType = "file",
})
end
end
handle:close()
end
end
return entries
end
-96
View File
@@ -1,96 +0,0 @@
--
-- Dynamic Omarchy Theme Menu for Elephant/Walker
--
Name = "omarchythemes"
NamePretty = "Omarchy Themes"
HideFromProviderlist = true
-- Check if file exists using Lua (no subprocess)
local function file_exists(path)
local f = io.open(path, "r")
if f then
f:close()
return true
end
return false
end
-- Get first matching file from directory using ls (single call for fallback)
local function first_image_in_dir(dir)
local handle = io.popen("ls -1 '" .. dir .. "' 2>/dev/null | head -n 1")
if handle then
local file = handle:read("*l")
handle:close()
if file and file ~= "" then
return dir .. "/" .. file
end
end
return nil
end
-- Find preview.png, preview.jpg, or first backgrounds/ image in a theme dir
local function find_preview_path(dir)
local png = dir .. "/preview.png"
local jpg = dir .. "/preview.jpg"
if file_exists(png) then return png end
if file_exists(jpg) then return jpg end
return first_image_in_dir(dir .. "/backgrounds")
end
-- The main function elephant will call
function GetEntries()
local entries = {}
local user_theme_dir = os.getenv("HOME") .. "/.config/omarchy/themes"
local omarchy_path = os.getenv("OMARCHY_PATH") or ""
local default_theme_dir = omarchy_path .. "/themes"
local seen_themes = {}
-- Helper function to process themes from a directory
local function process_themes_from_dir(theme_dir)
-- Single find call to get all theme directories
local handle = io.popen("find -L '" .. theme_dir .. "' -mindepth 1 -maxdepth 1 -type d 2>/dev/null")
if not handle then
return
end
for theme_path in handle:lines() do
local theme_name = theme_path:match(".*/(.+)$")
if theme_name and not seen_themes[theme_name] then
seen_themes[theme_name] = true
-- Check the theme dir, then fall back to the default theme dir
-- (for partial user customizations that don't ship a preview)
local preview_path = find_preview_path(theme_path)
or find_preview_path(default_theme_dir .. "/" .. theme_name)
if preview_path and preview_path ~= "" then
local display_name = theme_name:gsub("_", " "):gsub("%-", " ")
display_name = display_name:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
display_name = display_name .. " "
table.insert(entries, {
Text = display_name,
Preview = preview_path,
PreviewType = "file",
Actions = {
activate = "omarchy-theme-set " .. theme_name,
},
})
end
end
end
handle:close()
end
-- Process user themes first (they take precedence)
process_themes_from_dir(user_theme_dir)
-- Then process default themes (only if not already seen)
process_themes_from_dir(default_theme_dir)
return entries
end
-92
View File
@@ -1,92 +0,0 @@
--
-- Dynamic Omarchy Unlocks Menu for Elephant/Walker
--
-- A "Default" entry restores the omarchy-shipped Plymouth via
-- omarchy-plymouth-reset. After that, every theme that has a preview-unlock.png
-- appears as a customised unlock; picking one runs omarchy-plymouth-set-by-theme
-- <theme>. Both run in a floating terminal so sudo can prompt.
--
Name = "omarchyunlocks"
NamePretty = "Omarchy Unlocks"
HideFromProviderlist = true
FixedOrder = true
local function file_exists(path)
local f = io.open(path, "r")
if f then
f:close()
return true
end
return false
end
local function shell_escape(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
function GetEntries()
local entries = {}
local home = os.getenv("HOME")
local user_themes_dir = home .. "/.config/omarchy/themes"
local omarchy_path = os.getenv("OMARCHY_PATH") or ""
local default_themes_dir = omarchy_path .. "/themes"
local default_preview = omarchy_path .. "/default/plymouth/preview-unlock.png"
local seen_themes = {}
local function process_themes_from_dir(themes_dir)
local handle = io.popen("find -L '" .. themes_dir .. "' -mindepth 1 -maxdepth 1 -type d 2>/dev/null")
if not handle then
return
end
for theme_path in handle:lines() do
local theme_name = theme_path:match(".*/(.+)$")
if theme_name and not seen_themes[theme_name] then
seen_themes[theme_name] = true
local preview_path = theme_path .. "/preview-unlock.png"
if file_exists(preview_path) then
local display_name = theme_name:gsub("_", " "):gsub("%-", " ")
display_name = display_name:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
display_name = display_name .. " "
table.insert(entries, {
Text = display_name,
Preview = preview_path,
PreviewType = "file",
Actions = {
activate = "omarchy-launch-floating-terminal-with-presentation "
.. shell_escape("omarchy-plymouth-set-by-theme " .. shell_escape(theme_name)),
},
})
end
end
end
handle:close()
end
process_themes_from_dir(user_themes_dir)
process_themes_from_dir(default_themes_dir)
-- Default entry last — restores the shipped Plymouth.
local default_entry = {
Text = "Default ",
Actions = {
activate = "omarchy-launch-floating-terminal-with-presentation "
.. shell_escape("omarchy-plymouth-reset"),
},
}
if file_exists(default_preview) then
default_entry.Preview = default_preview
default_entry.PreviewType = "file"
end
table.insert(entries, default_entry)
return entries
end
+2 -1
View File
@@ -6,7 +6,8 @@
hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation = "none" })
hl.layer_rule({ match = { namespace = "omarchy-menu" }, no_anim = true, animation = "none" })
-- Image selector, emoji picker, and clipboard overlays should also pop without animation.
-- App launcher, image selector, emoji picker, and clipboard overlays should also pop without animation.
hl.layer_rule({ match = { namespace = "omarchy-app-launcher" }, no_anim = true, animation = "none" })
hl.layer_rule({ match = { namespace = "omarchy-image-selector" }, no_anim = true, animation = "none" })
hl.layer_rule({ match = { namespace = "omarchy-emoji-picker" }, no_anim = true, animation = "none" })
hl.layer_rule({ match = { namespace = "omarchy-clipboard-picker" }, no_anim = true, animation = "none" })
-2
View File
@@ -1,2 +0,0 @@
-- Application-specific animation.
hl.layer_rule({ match = { namespace = "walker" }, no_anim = true, animation = "none" })
+1 -1
View File
@@ -1,4 +1,4 @@
hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("omarchy-launch-walker"), { description = "Launch apps" })
hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("omarchy-shell shell toggle omarchy.app-launcher \"{}\""), { description = "Launch apps" })
hl.bind("SUPER + CTRL + E", hl.dsp.exec_cmd("omarchy-shell shell toggle omarchy.emoji-picker \"{}\""), { description = "Emoji picker" })
hl.bind("SUPER + CTRL + C", hl.dsp.exec_cmd("omarchy-shell menu toggle capture"), { description = "Capture menu" })
hl.bind("SUPER + CTRL + O", hl.dsp.exec_cmd("omarchy-shell menu toggle toggle"), { description = "Toggle menu" })
+5 -6
View File
@@ -2,10 +2,10 @@
name: omarchy
description: >
REQUIRED for end-user customization of Linux desktop, window manager, or system config.
Use when editing ~/.config/hypr/, ~/.config/omarchy/, ~/.config/walker/,
Use when editing ~/.config/hypr/, ~/.config/omarchy/,
~/.config/alacritty/, ~/.config/foot/, ~/.config/kitty/, or ~/.config/ghostty/.
Triggers: Hyprland, window rules, animations, keybindings, monitors, gaps, borders,
blur, opacity, omarchy-shell, bar, walker, terminal config, themes, background,
blur, opacity, omarchy-shell, bar, terminal config, themes, background,
night light, idle, lock screen, screenshots, reminders, layer rules, workspace
settings, display config, and user-facing omarchy commands. Excludes Omarchy
source development in ~/.local/share/omarchy/ and `omarchy dev` workflows.
@@ -23,7 +23,7 @@ It is not for contributing to Omarchy source code.
**ALWAYS invoke this skill for end-user requests involving ANY of these:**
- Editing ANY file in `~/.config/hypr/` (window rules, animations, keybindings, monitors, etc.)
- Editing `~/.config/omarchy/shell.json` (status bar layout, widgets) or `~/.config/walker/`
- Editing `~/.config/omarchy/shell.json` (status bar layout, widgets)
- Editing terminal configs (alacritty, foot, kitty, ghostty)
- Editing ANY file in `~/.config/omarchy/`
- Window behavior, animations, opacity, blur, gaps, borders
@@ -78,7 +78,7 @@ Omarchy is built on:
| **Arch Linux** | Base OS | `/etc/`, `~/.config/` |
| **Hyprland** | Wayland compositor/WM | `~/.config/hypr/` |
| **Omarchy shell** | Status bar + notifications (Quickshell) | `~/.config/omarchy/shell.json` |
| **Walker** | App launcher | `~/.config/walker/` |
| **App launcher** | Quickshell app launcher | `~/.config/omarchy/shell.json` |
| **Alacritty/Foot/Kitty/Ghostty** | Terminals | `~/.config/<terminal>/` |
| **Omarchy OSD** | On-screen display | Quickshell plugin |
@@ -182,7 +182,6 @@ changes. For more invasive changes (new plugin, packaged update):
| lazygit | `~/.config/lazygit/config.yml` |
| starship | `~/.config/starship.toml` |
| git | `~/.config/git/config` |
| walker | `~/.config/walker/config.toml` |
## Safe Customization Patterns
@@ -202,7 +201,7 @@ cp ~/.config/hypr/bindings.conf ~/.config/hypr/bindings.conf.bak.$(date +%s)
# 4. Apply changes
# - Hyprland: auto-reloads on save, but MUST validate with `hyprctl reload` and `hyprctl configerrors`
# - Omarchy shell: shell.json hot-reloads; use `omarchy-restart-shell` for plugin/widget code changes
# - Walker: MUST restart with `omarchy restart walker`
# - App launcher: restart with `omarchy restart shell`
# - Terminals: MUST restart with `omarchy restart terminal`
```
+2 -6
View File
@@ -13,7 +13,7 @@
// checked shell condition; append ✓ when it succeeds
// Root Menu
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"keywords":"launch launcher","action":"omarchy-launch-walker -p 'Launch…'"},
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"keywords":"launch launcher quickshell","action":"omarchy-shell shell summon omarchy.app-launcher '{}'"},
"learn": {"icon":"󰧑","label":"Learn","keywords":"manual docs help reference"},
"trigger": {"icon":"󱓞","label":"Trigger","keywords":"actions capture share toggle hardware reminder"},
"style": {"icon":"","label":"Style","keywords":"theme background font bar quickshell hyprland appearance"},
@@ -87,7 +87,6 @@
// Style
"style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"keywords":"colors","action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""},
"style.unlock": {"icon":"󰟵","label":"Unlock","keywords":"themes","action":"omarchy-launch-walker -m menus:omarchyunlocks --width 800 --minheight 400"},
"style.font": {"icon":"","label":"Font","keywords":"typeface","provider":"fonts"},
"style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""},
"style.bar": {"icon":"󰍜","label":"Bar","keywords":"quickshell top bottom left right settings"},
@@ -158,12 +157,11 @@
"setup.dns.google": {"icon":"󰖩","label":"Google","checked":"[[ $(omarchy-dns) == \"Google\" ]]","action":"omarchy-dns Google"},
"setup.dns.custom": {"icon":"󰖩","label":"Custom","checked":"[[ $(omarchy-dns) == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-dns Custom"},
"setup.security": {"icon":"","label":"Security","keywords":"fingerprint fido2"},
"setup.config": {"icon":"","label":"Config","keywords":"files hyprland bar quickshell walker"},
"setup.config": {"icon":"","label":"Config","keywords":"files hyprland bar quickshell"},
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"setup.security.fido2": {"icon":"","label":"Fido2","keywords":"key","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"night light","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
"setup.config.walker": {"icon":"󰌧","label":"Walker","keywords":"launcher","action":"omarchy-launch-config-editor ~/.config/walker/config.toml && omarchy-restart-walker"},
"setup.config.bar": {"icon":"󰍜","label":"Bar","keywords":"quickshell shell config settings widgets position","action":"omarchy-launch-bar-settings"},
"setup.config.xcompose": {"icon":"󰞅","label":"XCompose","keywords":"compose key","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
@@ -311,14 +309,12 @@
"update.channel.dev": {"icon":"🔴","label":"Dev","keywords":"development","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set dev'"},
"update.process.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"restart","action":"omarchy-restart-hyprsunset"},
"update.process.osd": {"icon":"","label":"OSD","keywords":"restart","action":"omarchy-restart-shell"},
"update.process.walker": {"icon":"󰌧","label":"Walker","keywords":"restart launcher","action":"omarchy-restart-walker"},
"update.process.shell": {"icon":"󰍜","label":"Shell","keywords":"restart bar quickshell omarchy-shell","action":"omarchy-restart-shell"},
"update.config.hyprland": {"icon":"","label":"Hyprland","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprland"},
"update.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprsunset"},
"update.config.plymouth": {"icon":"󱣴","label":"Plymouth","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-plymouth"},
"update.config.osd": {"icon":"","label":"OSD","keywords":"refresh","action":"omarchy-restart-shell"},
"update.config.tmux": {"icon":"","label":"Tmux","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-tmux"},
"update.config.walker": {"icon":"󰌧","label":"Walker","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-walker"},
"update.config.shell": {"icon":"󰍜","label":"Shell","keywords":"refresh bar quickshell","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-shell"},
"update.hardware.audio": {"icon":"","label":"Audio","keywords":"restart pipewire","action":"omarchy-launch-floating-terminal-with-presentation omarchy-restart-pipewire"},
"update.hardware.wifi": {"icon":"󱚾","label":"Wi-Fi","keywords":"restart wireless","action":"omarchy-launch-floating-terminal-with-presentation omarchy-restart-wifi"},
-6
View File
@@ -1,6 +0,0 @@
@define-color selected-text {{ accent }};
@define-color text {{ foreground }};
@define-color base {{ background }};
@define-color border {{ foreground }};
@define-color foreground {{ foreground }};
@define-color background {{ background }};
-3
View File
@@ -1,3 +0,0 @@
[Service]
Restart=always
RestartSec=2
@@ -1,156 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"></requires>
<object class="GtkWindow" id="Window">
<style>
<class name="window"></class>
</style>
<property name="resizable">true</property>
<property name="title">Walker</property>
<child>
<object class="GtkBox" id="BoxWrapper">
<style>
<class name="box-wrapper"></class>
</style>
<property name="width-request">644</property>
<property name="overflow">hidden</property>
<property name="orientation">horizontal</property>
<property name="valign">center</property>
<property name="halign">center</property>
<child>
<object class="GtkBox" id="Box">
<style>
<class name="box"></class>
</style>
<property name="orientation">vertical</property>
<property name="hexpand-set">true</property>
<property name="hexpand">true</property>
<property name="spacing">10</property>
<child>
<object class="GtkBox" id="SearchContainer">
<style>
<class name="search-container"></class>
</style>
<property name="overflow">hidden</property>
<property name="orientation">horizontal</property>
<property name="halign">fill</property>
<property name="hexpand-set">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkEntry" id="Input">
<style>
<class name="input"></class>
</style>
<property name="halign">fill</property>
<property name="hexpand-set">true</property>
<property name="hexpand">true</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="ContentContainer">
<style>
<class name="content-container"></class>
</style>
<property name="orientation">horizontal</property>
<property name="spacing">10</property>
<property name="vexpand">true</property>
<property name="vexpand-set">true</property>
<child>
<object class="GtkLabel" id="ElephantHint">
<style>
<class name="elephant-hint"></class>
</style>
<property name="hexpand">true</property>
<property name="height-request">100</property>
<property name="label">Waiting for elephant...</property>
</object>
</child>
<child>
<object class="GtkLabel" id="Placeholder">
<style>
<class name="placeholder"></class>
</style>
<property name="label">No Results</property>
<property name="yalign">0.0</property>
<property name="hexpand">true</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="Scroll">
<style>
<class name="scroll"></class>
</style>
<property name="hexpand">true</property>
<property name="can_focus">false</property>
<property name="overlay-scrolling">true</property>
<property name="max-content-width">600</property>
<property name="max-content-height">300</property>
<property name="min-content-height">0</property>
<property name="propagate-natural-height">true</property>
<property name="propagate-natural-width">true</property>
<property name="hscrollbar-policy">automatic</property>
<property name="vscrollbar-policy">automatic</property>
<child>
<object class="GtkGridView" id="List">
<style>
<class name="list"></class>
</style>
<property name="max_columns">1</property>
<property name="can_focus">false</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="Preview">
<style>
<class name="preview"></class>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="Keybinds">
<property name="hexpand">true</property>
<property name="margin-top">10</property>
<style>
<class name="keybinds"></class>
</style>
<child>
<object class="GtkBox" id="GlobalKeybinds">
<property name="spacing">10</property>
<style>
<class name="global-keybinds"></class>
</style>
</object>
</child>
<child>
<object class="GtkBox" id="ItemKeybinds">
<property name="hexpand">true</property>
<property name="halign">end</property>
<property name="spacing">10</property>
<style>
<class name="item-keybinds"></class>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="Error">
<style>
<class name="error"></class>
</style>
<property name="xalign">0</property>
<property name="visible">false</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
@@ -1,122 +0,0 @@
@import "../../../../../../../.config/omarchy/current/theme/walker.css";
@import "../../../../../../../.local/state/omarchy/toggles/walker.css";
* {
all: unset;
}
* {
font-family: monospace;
font-size: 18px;
color: @text;
}
scrollbar {
opacity: 0;
}
.normal-icons {
-gtk-icon-size: 16px;
}
.large-icons {
-gtk-icon-size: 32px;
}
.box-wrapper {
background: alpha(@base, 0.95);
padding: 20px;
border: 2px solid @border;
}
.preview-box {
}
.box {
}
.search-container {
background: @base;
padding: 10px;
}
.input placeholder {
opacity: 0.5;
}
.input {
}
.input:focus,
.input:active {
box-shadow: none;
outline: none;
}
.content-container {
}
.placeholder {
}
.scroll {
}
.list {
}
child,
child > * {
}
child:hover .item-box {
}
child:selected .item-box {
}
child:selected .item-box * {
color: @selected-text;
}
child:selected {
background: alpha(@text, 0.07);
}
.item-box {
padding-left: 14px;
}
.item-text-box {
all: unset;
padding: 14px 0;
}
.item-text {
}
.item-subtext {
font-size: 0px;
min-height: 0px;
margin: 0px;
padding: 0px;
}
.item-image {
margin-right: 14px;
-gtk-icon-transform: scale(0.9);
}
.current {
font-style: italic;
}
.keybind-hints {
background: @background;
padding: 10px;
margin-top: 10px;
}
.preview {
}
-7
View File
@@ -1,7 +0,0 @@
[Desktop Entry]
Name=Walker
Comment=Walker Service
Exec=walker --gapplication-service
StartupNotify=false
Terminal=false
Type=Application
-1
View File
@@ -20,7 +20,6 @@ run_logged $OMARCHY_INSTALL/config/user-dirs.sh
run_logged $OMARCHY_INSTALL/config/toggles.sh
run_logged $OMARCHY_INSTALL/config/nautilus-python.sh
run_logged $OMARCHY_INSTALL/config/localdb.sh
run_logged $OMARCHY_INSTALL/config/walker-elephant.sh
run_logged $OMARCHY_INSTALL/config/fast-shutdown.sh
run_logged $OMARCHY_INSTALL/config/unmount-fuse.sh
run_logged $OMARCHY_INSTALL/config/sudoless-asdcontrol.sh
-1
View File
@@ -1,3 +1,2 @@
# Make sure toggles are available
mkdir -p ~/.local/state/omarchy/toggles
touch ~/.local/state/omarchy/toggles/walker.css
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
# Ensure Walker service is started automatically on boot
mkdir -p ~/.config/autostart/
cp $OMARCHY_PATH/default/walker/walker.desktop ~/.config/autostart/
# And is restarted if it crashes or is killed
mkdir -p ~/.config/systemd/user/app-walker@autostart.service.d/
cp $OMARCHY_PATH/default/walker/restart.conf ~/.config/systemd/user/app-walker@autostart.service.d/restart.conf
# Create pacman hook to restart walker after updates
sudo mkdir -p /etc/pacman.d/hooks
sudo tee /etc/pacman.d/hooks/walker-restart.hook > /dev/null << EOF
[Trigger]
Type = Package
Operation = Upgrade
Target = walker
Target = walker-debug
Target = elephant*
[Action]
Description = Restarting Walker services after system update
When = PostTransaction
Exec = $OMARCHY_PATH/bin/omarchy-restart-walker
EOF
# Link the visual theme menu config
mkdir -p ~/.config/elephant/menus
ln -snf $OMARCHY_PATH/default/elephant/omarchy_themes.lua ~/.config/elephant/menus/omarchy_themes.lua
ln -snf $OMARCHY_PATH/default/elephant/omarchy_background_selector.lua ~/.config/elephant/menus/omarchy_background_selector.lua
ln -snf $OMARCHY_PATH/default/elephant/omarchy_unlocks.lua ~/.config/elephant/menus/omarchy_unlocks.lua
-2
View File
@@ -1,2 +0,0 @@
elephant service enable
systemctl --user start elephant.service
+1 -1
View File
@@ -39,6 +39,7 @@ ffmpegthumbnailer
fontconfig
foot
fzf
git
github-cli
gnome-calculator
gnome-keyring
@@ -88,7 +89,6 @@ nvim
obs-studio
obsidian
omarchy-nvim
omarchy-walker
pamixer
pinta
playerctl
-1
View File
@@ -9,7 +9,6 @@ broadcom-wl
btrfs-progs
dkms
egl-wayland
git
gst-plugin-pipewire
gtk4-layer-shell
libpulse
-11
View File
@@ -1,11 +0,0 @@
echo "Replace wofi with walker as the default launcher"
if omarchy-cmd-missing walker; then
omarchy-pkg-add walker-bin libqalculate
omarchy-pkg-drop wofi
rm -rf ~/.config/wofi
mkdir -p ~/.config/walker
cp -r ~/.local/share/omarchy/config/walker/* ~/.config/walker/
fi
-55
View File
@@ -1,55 +0,0 @@
echo "Migrate to Walker 2.0.0"
NEEDS_MIGRATION=false
PACKAGES=(
"elephant"
"elephant-calc"
"elephant-clipboard"
"elephant-bluetooth"
"elephant-desktopapplications"
"elephant-files"
"elephant-menus"
"elephant-providerlist"
"elephant-runner"
"elephant-symbols"
"elephant-unicode"
"elephant-websearch"
"elephant-todo"
"walker"
"libqalculate"
)
for pkg in "${PACKAGES[@]}"; do
if ! omarchy-pkg-present "$pkg"; then
NEEDS_MIGRATION=true
break
fi
done
WALKER_MAJOR=$(walker -v 2>&1 | grep -oP '^\d+' || echo "0")
if (( WALKER_MAJOR < 2 )); then
NEEDS_MIGRATION=true
fi
# Ensure basic config is present
mkdir -p ~/.config/walker
cp -r ~/.local/share/omarchy/config/walker/* ~/.config/walker/
if $NEEDS_MIGRATION; then
kill -9 $(pgrep -x walker) 2>/dev/null || true
omarchy-pkg-drop walker-bin walker-bin-debug
omarchy-pkg-add "${PACKAGES[@]}"
source $OMARCHY_PATH/install/config/walker-elephant.sh
rm -rf ~/.config/walker/themes
omarchy-refresh-config walker/config.toml
omarchy-refresh-config elephant/calc.toml
omarchy-refresh-config elephant/desktopapplications.toml
fi
echo # Assure final success
-2
View File
@@ -1,2 +0,0 @@
echo "Install omarchy-walker meta package"
omarchy-pkg-add omarchy-walker
-10
View File
@@ -1,10 +0,0 @@
echo "Switch Elephant to run as a systemd service and walker to be autostarted on login"
pkill elephant
elephant service enable
systemctl --user start elephant.service
pkill walker
mkdir -p ~/.config/autostart/
cp $OMARCHY_PATH/default/walker/walker.desktop ~/.config/autostart/
setsid walker --gapplication-service &
-3
View File
@@ -1,3 +0,0 @@
echo "Ensure emoji menu (Super + Ctrl + E) uses single column design"
omarchy-refresh-walker
-11
View File
@@ -1,11 +0,0 @@
echo "Add emergency entry for Walker"
CONFIG_FILE="$HOME/.config/walker/config.toml"
if [[ -f $CONFIG_FILE ]] && ! grep -q 'command = "omarchy-restart-walker"' "$CONFIG_FILE"; then
cat >> "$CONFIG_FILE" << 'EOF'
[[emergencies]]
text = "Restart Walker"
command = "omarchy-restart-walker"
EOF
fi
-4
View File
@@ -1,4 +0,0 @@
echo "Enable auto-pasting for the emoji picker"
omarchy-refresh-config elephant/symbols.toml
omarchy-restart-walker
-6
View File
@@ -1,6 +0,0 @@
echo "Ensure walker service is restarted if it's killed or crashes"
mkdir -p ~/.config/systemd/user/app-walker@autostart.service.d/
cp $OMARCHY_PATH/default/walker/restart.conf ~/.config/systemd/user/app-walker@autostart.service.d/restart.conf
systemctl --user daemon-reload
-5
View File
@@ -1,5 +0,0 @@
echo "Use interactive background selector menu"
mkdir -p ~/.config/elephant/menus
ln -snf $OMARCHY_PATH/default/elephant/omarchy_background_selector.lua ~/.config/elephant/menus/omarchy_background_selector.lua
omarchy-restart-walker
-5
View File
@@ -1,5 +0,0 @@
echo "Use interactive unlock (Plymouth) selector menu"
mkdir -p ~/.config/elephant/menus
ln -snf $OMARCHY_PATH/default/elephant/omarchy_unlocks.lua ~/.config/elephant/menus/omarchy_unlocks.lua
omarchy-restart-walker
-4
View File
@@ -1,4 +0,0 @@
echo "Create Walker toggle config"
mkdir -p ~/.local/state/omarchy/toggles
[[ -f ~/.local/state/omarchy/toggles/walker.css ]] || touch ~/.local/state/omarchy/toggles/walker.css
-9
View File
@@ -1,9 +0,0 @@
echo "Migrate emoji picker shortcut to use Quickshell emoji picker"
if [[ -f ~/.config/hypr/bindings/utilities.lua ]]; then
sed -i 's/omarchy-launch-walker -m symbols/omarchy-shell-ipc shell toggle omarchy.emoji-picker "{}"/' ~/.config/hypr/bindings/utilities.lua
fi
if [[ -f ~/.config/hypr/bindings.lua ]]; then
sed -i 's/omarchy-launch-walker -m symbols/omarchy-shell-ipc shell toggle omarchy.emoji-picker "{}"/' ~/.config/hypr/bindings.lua
fi
-5
View File
@@ -1,5 +0,0 @@
echo "Migrate clipboard manager shortcut to use Quickshell clipboard picker"
if [[ -f ~/.config/hypr/bindings/clipboard.lua ]]; then
sed -i 's/walker -m clipboard/omarchy-shell-ipc shell toggle omarchy.clipboard-picker "{}"/' ~/.config/hypr/bindings/clipboard.lua
fi
+32
View File
@@ -0,0 +1,32 @@
echo "Remove Walker app-launcher services and Elephant providers"
pkill elephant 2>/dev/null || true
systemctl --user disable --now elephant.service 2>/dev/null || true
systemctl --user disable --now app-walker@autostart.service 2>/dev/null || true
rm -f ~/.config/autostart/walker.desktop
rm -rf ~/.config/elephant
rm -rf ~/.config/systemd/user/app-walker@autostart.service.d
sudo rm -f /etc/pacman.d/hooks/walker-restart.hook
for bindings_file in ~/.config/hypr/bindings/utilities.lua ~/.config/hypr/bindings.lua; do
if [[ -f $bindings_file ]]; then
sed -i 's#omarchy-launch-walker#omarchy-shell shell toggle omarchy.app-launcher \\"{}\\"#g' "$bindings_file"
fi
done
omarchy-pkg-drop \
omarchy-walker \
elephant-all \
elephant \
elephant-calc \
elephant-clipboard \
elephant-bluetooth \
elephant-desktopapplications \
elephant-files \
elephant-menus \
elephant-providerlist \
elephant-runner \
elephant-symbols \
elephant-unicode \
elephant-websearch \
elephant-todo
+1
View File
@@ -29,6 +29,7 @@ shell/
plugins/
bar/ first-party plugins (see plugins/README.md)
settings/
app-launcher/
image-picker/
menu/
notifications/
+10 -1
View File
@@ -13,6 +13,7 @@ User-installed plugins live alongside these conceptually but on disk under
|---------------|---------------------------|-----------|---------------------------------------|
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` |
| App launcher | `omarchy.app-launcher` | `overlay` | `app-launcher/AppLauncher.qml` |
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
| Emoji picker | `omarchy.emoji-picker` | `overlay` | `emoji-picker/EmojiPicker.qml` |
| Clipboard mgr | `omarchy.clipboard-picker`| `overlay` | `clipboard-picker/ClipboardPicker.qml`|
@@ -43,6 +44,14 @@ Visual editor for the bar layout. Summoned by
- dynamic per-widget settings forms that write inline back to the
corresponding shell.json entry
## App launcher
Quickshell-powered app launcher. It uses Quickshell's native
`DesktopEntries` model for discovery/activation and renders inside the
long-running shell with the legacy launcher card dimensions, colors, row
spacing, icon sizing, and keyboard behavior. Summoned directly over shell IPC
by the `SUPER + SPACE` binding and the Omarchy menu Apps row.
## Image picker
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
@@ -87,7 +96,7 @@ runs inside the long-lived `omarchy-shell` process, replacing the old
## Omarchy menu
Quickshell-powered replacement for the legacy Walker-driven `omarchy-menu`.
Quickshell-powered Omarchy command menu.
The menu UI lives in `menu/Menu.qml` as a first-party `menu` plugin and is
summoned through the shell (`omarchy-shell shell summon omarchy.menu ...`),
so it shares the long-running `omarchy-shell` process instead of starting a
+434
View File
@@ -0,0 +1,434 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import QtQuick
import qs.Commons
Item {
id: root
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
property var shell: null
property var manifest: null
property bool opened: false
property string placeholder: "\uf002 Search..."
property string filterText: ""
property int selectedIndex: 0
property bool cursorActive: true
property bool hoverArmed: false
property var filteredEntries: []
property color accent: Color.menu.selected
property color background: Color.menu.background
property color foreground: Color.menu.text
property color border: foreground
property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace"
property int cardWidth: 644
property int cardHeight: 400
property int contentMargin: 20
property int contentSpacing: 10
property int searchHeight: 44
property int rowHeight: 50
property int iconSlotWidth: 44
property int iconSize: 24
readonly property int listHeight: cardHeight - contentMargin * 2 - searchHeight - contentSpacing
function open(payloadJson) {
var payload = ({})
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
root.placeholder = payload.placeholder || "\uf002 Search..."
root.cardWidth = Math.max(300, Number(payload.width || 644))
var requestedListHeight = Number(payload.listHeight || payload.maxHeight || 0)
root.cardHeight = requestedListHeight > 0
? root.contentMargin * 2 + root.searchHeight + root.contentSpacing + requestedListHeight
: 400
root.opened = true
root.filterText = payload.query || ""
root.selectedIndex = 0
root.cursorActive = true
root.hoverArmed = false
root.rebuildDisplay()
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.app-launcher")
}
function withAlpha(color, alpha) {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function fileUrl(path) {
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
}
function iconSource(icon) {
var value = String(icon || "")
if (value.length === 0) return Quickshell.iconPath("application-x-executable", true)
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
if (value.charAt(0) === "/") return root.fileUrl(value)
return Quickshell.iconPath(value, true)
}
function entryName(entry) {
return String((entry && entry.name) || (entry && entry.id) || "")
}
function entrySubtext(entry) {
return String((entry && entry.genericName) || "")
}
function entrySortKey(entry) {
return String((entry && entry.id) || root.entryName(entry)).toLowerCase()
}
function entrySearchText(entry) {
if (!entry) return ""
var keywords = ""
try { keywords = (entry.keywords || []).join(" ") } catch (e) { keywords = "" }
return [entry.name, entry.genericName, entry.comment, keywords, entry.id].join(" ").toLowerCase()
}
function isHiddenEntry(entry) {
var id = String((entry && entry.id) || "")
return id === "avahi-discover" || id === "bssh" || id === "bvnc"
}
function fuzzyScore(entry, query) {
var q = String(query || "").trim().toLowerCase()
if (!q) return 0
var name = root.entryName(entry).toLowerCase()
var id = String((entry && entry.id) || "").toLowerCase()
var haystack = root.entrySearchText(entry)
var directName = name.indexOf(q)
var directId = id.indexOf(q)
if (directName === 0) return 10000 - name.length
if (directId === 0) return 9500 - id.length
if (directName > 0) return 8000 - directName * 10 - name.length
if (directId > 0) return 7600 - directId * 10 - id.length
var hayIndex = haystack.indexOf(q)
if (hayIndex >= 0) return 6000 - hayIndex
var pos = 0
var first = -1
var last = -1
for (var i = 0; i < haystack.length && pos < q.length; i++) {
if (haystack.charAt(i) !== q.charAt(pos)) continue
if (first < 0) first = i
last = i
pos++
}
if (pos !== q.length) return -1
return 3000 - (last - first) - haystack.length * 0.01
}
function sortedEntries(query) {
var values = DesktopEntries.applications.values || []
var q = String(query || "").trim()
var rows = []
for (var i = 0; i < values.length; i++) {
var entry = values[i]
if (!entry || entry.noDisplay || root.isHiddenEntry(entry)) continue
var name = root.entryName(entry)
if (!name) continue
var score = root.fuzzyScore(entry, q)
if (score < 0) continue
rows.push({ entry: entry, score: score, key: root.entrySortKey(entry), name: name.toLowerCase() })
}
rows.sort(function(a, b) {
if (q && a.score !== b.score) return b.score - a.score
if (a.key < b.key) return -1
if (a.key > b.key) return 1
if (a.name < b.name) return -1
if (a.name > b.name) return 1
return 0
})
return rows
}
function rebuildDisplay() {
displayModel.clear()
var rows = root.sortedEntries(root.filterText)
var entries = []
var count = Math.min(rows.length, 256)
for (var i = 0; i < count; i++) {
var entry = rows[i].entry
entries.push(entry)
displayModel.append({
name: root.entryName(entry),
subtext: root.entrySubtext(entry),
icon: String(entry.icon || "")
})
}
root.filteredEntries = entries
if (displayModel.count === 0) root.selectedIndex = 0
else if (root.selectedIndex >= displayModel.count) root.selectedIndex = displayModel.count - 1
else if (root.selectedIndex < 0) root.selectedIndex = 0
Qt.callLater(function() {
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
})
}
function setFilter(nextFilter) {
root.filterText = nextFilter
root.selectedIndex = 0
root.cursorActive = true
root.rebuildDisplay()
}
function select(delta) {
if (displayModel.count === 0) return
root.cursorActive = true
root.selectedIndex = (root.selectedIndex + delta + displayModel.count) % displayModel.count
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
}
function activateIndex(index) {
if (index < 0 || index >= root.filteredEntries.length) return
var entry = root.filteredEntries[index]
if (!entry) return
root.dismiss()
entry.execute()
}
ListModel { id: displayModel }
Connections {
target: DesktopEntries.applications
function onValuesChanged() {
if (root.opened) root.rebuildDisplay()
}
}
PanelWindow {
id: panel
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "omarchy-app-launcher"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
Rectangle {
anchors.fill: parent
color: root.withAlpha(root.background, 0.5)
}
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Rectangle {
id: card
width: Math.min(root.cardWidth, panel.width - Style.gapsOut * 2)
height: Math.min(root.cardHeight, panel.height - Style.gapsOut * 2)
radius: Style.cornerRadius
anchors.centerIn: parent
color: root.withAlpha(root.background, 0.95)
border.color: root.border
border.width: 2
clip: true
MouseArea { anchors.fill: parent; onClicked: {} }
Item {
id: keyCatcher
anchors.fill: parent
focus: true
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
if (root.filterText.length > 0) root.setFilter("")
else root.dismiss()
event.accepted = true
} else if (event.key === Qt.Key_Backspace) {
if (root.filterText.length > 0) root.setFilter(root.filterText.slice(0, -1))
event.accepted = true
} else if (event.key === Qt.Key_Up) {
root.select(-1)
event.accepted = true
} else if (event.key === Qt.Key_Down) {
root.select(1)
event.accepted = true
} else if (event.key === Qt.Key_PageUp) {
root.select(-6)
event.accepted = true
} else if (event.key === Qt.Key_PageDown) {
root.select(6)
event.accepted = true
} else if (event.key === Qt.Key_Home) {
if (displayModel.count > 0) {
root.cursorActive = true
root.selectedIndex = 0
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
}
event.accepted = true
} else if (event.key === Qt.Key_End) {
if (displayModel.count > 0) {
root.cursorActive = true
root.selectedIndex = displayModel.count - 1
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
}
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.activateIndex(root.selectedIndex)
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)) {
root.setFilter(root.filterText + event.text)
event.accepted = true
}
}
}
Column {
anchors.fill: parent
anchors.margins: root.contentMargin
spacing: root.contentSpacing
Rectangle {
width: parent.width
height: root.searchHeight
radius: 0
color: root.background
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: root.filterText || root.placeholder
color: root.foreground
opacity: root.filterText ? 1 : 0.5
font.family: root.fontFamily
font.pixelSize: 18
elide: Text.ElideRight
}
}
Item {
width: parent.width
height: parent.height - root.searchHeight - root.contentSpacing
ListView {
id: resultList
anchors.fill: parent
model: displayModel
clip: true
spacing: 0
boundsBehavior: Flickable.StopAtBounds
delegate: Rectangle {
id: row
required property int index
required property string name
required property string subtext
required property string icon
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
width: ListView.view.width
height: root.rowHeight
radius: 0
color: row.hasCursor ? root.withAlpha(root.foreground, 0.07) : "transparent"
Item {
id: iconSlot
anchors.left: parent.left
anchors.leftMargin: 14
anchors.verticalCenter: parent.verticalCenter
width: root.iconSlotWidth
height: parent.height
IconImage {
id: appIcon
anchors.centerIn: parent
implicitSize: root.iconSize
width: root.iconSize
height: root.iconSize
source: root.iconSource(row.icon)
asynchronous: true
mipmap: true
}
Text {
anchors.centerIn: parent
visible: appIcon.status === Image.Error
text: "?"
color: row.hasCursor ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: 18
}
}
Text {
anchors.left: iconSlot.right
anchors.leftMargin: 14
anchors.right: parent.right
anchors.rightMargin: 14
anchors.verticalCenter: parent.verticalCenter
text: row.name
color: row.hasCursor ? root.accent : root.foreground
font.family: root.fontFamily
font.pixelSize: 18
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPositionChanged: function(mouse) {
root.hoverArmed = true
root.cursorActive = true
root.selectedIndex = row.index
}
onContainsMouseChanged: if (containsMouse && root.hoverArmed) {
root.cursorActive = true
root.selectedIndex = row.index
}
onClicked: {
root.cursorActive = true
root.selectedIndex = row.index
root.activateIndex(row.index)
}
}
}
}
Text {
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: 14
visible: displayModel.count === 0
text: "No Results"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 18
}
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"id": "omarchy.app-launcher",
"name": "App launcher",
"version": "1.0.0",
"author": "Omarchy",
"description": "A Quickshell-powered application launcher",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "AppLauncher.qml"
}
}
@@ -7,12 +7,15 @@ import qs.Commons
Item {
id: root
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
property bool opened: false
property string filterText: ""
property int selectedIndex: 0
property bool cursorActive: false
property var items: []
property var history: []
property string historyPath: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-history.json"
property string captureScript: root.omarchyPath + "/shell/scripts/clipboard-capture.sh"
property color accent: Color.menu.selected
property color background: Color.menu.background
property color foreground: Color.menu.text
@@ -31,12 +34,7 @@ Item {
root.filterText = ""
root.selectedIndex = 0
root.cursorActive = false
// Trigger fetch
fetchProc.collected = ""
fetchProc.command = ["bash", "-lc", "elephant query --json 'clipboard;;100'"]
fetchProc.running = true
root.rebuildDisplay()
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
@@ -53,35 +51,111 @@ Item {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function shellQuote(value) {
return "'" + String(value || "").replace(/'/g, "'\\''") + "'"
}
function fileUrl(path) {
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
}
function normalizeEntry(value) {
if (typeof value === "string") {
return value.length > 0 ? { type: "text", text: value } : null
}
if (!value || typeof value !== "object") return null
var type = String(value.type || value.kind || "")
if (type === "text") {
var text = String(value.text || "")
return text.length > 0 ? { type: "text", text: text } : null
}
if (type === "image") {
var path = String(value.path || "")
if (!path) return null
return {
type: "image",
path: path,
mime: String(value.mime || "image/png")
}
}
return null
}
function entryKey(entry) {
if (!entry) return ""
if (entry.type === "image") return "image:" + String(entry.path || "")
return "text:" + String(entry.text || "")
}
function loadHistory(raw) {
try {
var parsed = JSON.parse(String(raw || "[]"))
var next = []
if (Array.isArray(parsed)) {
for (var i = 0; i < parsed.length; i++) {
var entry = root.normalizeEntry(parsed[i])
if (entry) next.push(entry)
}
}
root.history = next
} catch (e) {
root.history = []
}
if (root.opened) root.rebuildDisplay()
}
function saveHistory() {
historyFile.setText(JSON.stringify(root.history.slice(0, 100), null, 2) + "\n")
}
function addClipboardEntry(entry) {
var normalized = root.normalizeEntry(entry)
if (!normalized) return
var key = root.entryKey(normalized)
var next = [normalized]
for (var i = 0; i < root.history.length && next.length < 100; i++) {
var existing = root.normalizeEntry(root.history[i])
if (!existing || root.entryKey(existing) === key) continue
next.push(existing)
}
root.history = next
root.saveHistory()
if (root.opened) root.rebuildDisplay()
}
function addClipboardJson(line) {
var raw = String(line || "").trim()
if (!raw) return
try { root.addClipboardEntry(JSON.parse(raw)) } catch (e) {}
}
function rebuildDisplay() {
var query = root.filterText.trim().toLowerCase()
displayModel.clear()
var outCount = 0
for (var i = 0; i < root.items.length; i++) {
var entry = root.items[i]
var isPassword = (entry.meta === "password" || entry.preview_type === "password")
var textMatch = false
if (isPassword) {
textMatch = false // Passwords shouldn't match plain text search queries
} else {
textMatch = (entry.preview && entry.preview.toLowerCase().indexOf(query) >= 0)
}
if (!query || textMatch) {
displayModel.append({
identifier: entry.identifier,
previewText: entry.preview_type === "text" ? entry.preview.replace(/\n/g, " ") : "",
previewImage: entry.preview_type === "file" ? ("file://" + entry.preview) : "",
previewType: entry.preview_type || "text",
isPassword: isPassword,
index: outCount
})
outCount++
if (outCount >= 50) break
}
for (var i = 0; i < root.history.length; i++) {
var entry = root.normalizeEntry(root.history[i])
if (!entry) continue
var isImage = entry.type === "image"
var searchable = isImage ? ("image " + String(entry.mime || "")) : String(entry.text || "")
if (query && searchable.toLowerCase().indexOf(query) < 0) continue
displayModel.append({
entryType: entry.type,
fullText: isImage ? "" : String(entry.text || ""),
previewText: isImage ? "Image" : String(entry.text || "").replace(/\s+/g, " "),
previewImage: isImage ? root.fileUrl(entry.path) : "",
path: isImage ? String(entry.path || "") : "",
mime: isImage ? String(entry.mime || "image/png") : "text/plain",
index: outCount
})
outCount++
if (outCount >= 50) break
}
if (displayModel.count === 0) selectedIndex = 0
@@ -114,40 +188,60 @@ Item {
function activateIndex(index) {
if (index < 0 || index >= displayModel.count) return
var row = displayModel.get(index)
root.applySelected(row.identifier)
root.applySelected(row)
}
function applySelected(identifier) {
if (!identifier) return
function applySelected(row) {
if (!row) return
root.opened = false
var escId = identifier.replace(/'/g, "'\\''")
Quickshell.execDetached(["bash", "-lc", "elephant activate 'clipboard;" + escId + ";copy;;'; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
if (row.entryType === "image") {
Quickshell.execDetached(["bash", "-lc", "wl-copy --type " + root.shellQuote(row.mime) + " < " + root.shellQuote(row.path) + "; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
} else if (row.fullText) {
Quickshell.execDetached(["bash", "-lc", "printf %s " + root.shellQuote(row.fullText) + " | wl-copy; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
}
}
Component.onCompleted: initProc.running = true
ListModel { id: displayModel }
FileView {
id: historyFile
path: root.historyPath
watchChanges: true
atomicWrites: true
printErrors: false
onLoaded: root.loadHistory(text())
onLoadFailed: root.loadHistory("[]")
onFileChanged: reload()
}
Process {
id: fetchProc
property string collected: ""
stdout: SplitParser {
onRead: function(data) { fetchProc.collected += data + "\n" }
}
id: initProc
command: ["bash", "-lc", "mkdir -p ~/.local/state/omarchy"]
onExited: {
var lines = fetchProc.collected.split("\n")
var newItems = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (!line) continue
try {
var parsed = JSON.parse(line)
if (parsed && parsed.item) {
newItems.push(parsed.item)
}
} catch(e) {}
}
root.items = newItems
root.rebuildDisplay()
currentProc.command = [root.captureScript]
currentProc.running = true
watchProc.command = ["wl-paste", "--watch", root.captureScript]
watchProc.running = true
}
}
Process {
id: currentProc
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.addClipboardJson(text)
}
}
Process {
id: watchProc
stdout: SplitParser {
onRead: function(data) { root.addClipboardJson(data) }
}
}
PanelWindow {
id: panel
visible: root.opened
@@ -261,10 +355,10 @@ Item {
delegate: Rectangle {
required property int index
required property string identifier
required property string entryType
required property string previewText
required property string previewType
required property bool isPassword
required property string fullText
required property string previewImage
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
@@ -275,33 +369,33 @@ Item {
border.color: hasCursor ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: hasCursor ? Style.hoverBorderWidth : 0
Rectangle {
visible: false
width: Style.space(4)
height: parent.height - Style.space(18)
radius: Math.min(root.cornerRadius, Style.space(4))
color: root.accent
anchors.left: parent.left
anchors.leftMargin: Style.space(8)
anchors.verticalCenter: parent.verticalCenter
}
Item {
Row {
anchors.fill: parent
anchors.leftMargin: Style.space(12)
anchors.rightMargin: Style.space(12)
anchors.topMargin: Style.space(8)
anchors.bottomMargin: Style.space(8)
spacing: Style.space(10)
Image {
visible: parent.parent.entryType === "image"
width: visible ? parent.height : 0
height: parent.height
source: parent.parent.previewImage
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
}
Text {
width: parent.width
width: parent.width - (parent.parent.entryType === "image" ? parent.height + parent.spacing : 0)
height: parent.height
text: parent.parent.isPassword ? "••••••••" : (parent.parent.previewType === "text" ? parent.parent.previewText : "Image")
text: parent.parent.previewText
color: parent.parent.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.title
font.italic: parent.parent.previewType === "file" || parent.parent.isPassword
opacity: (parent.parent.previewType === "file" || parent.parent.isPassword) ? 0.6 : 1.0
font.italic: parent.parent.entryType === "image"
opacity: parent.parent.entryType === "image" ? 0.72 : 1.0
elide: Text.ElideRight
wrapMode: Text.NoWrap
verticalAlignment: Text.AlignVCenter
@@ -309,7 +403,6 @@ Item {
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -335,13 +428,13 @@ Item {
border.width: Style.normalBorderWidth
clip: true
property var activeRow: root.cursorActive && displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
property var activeRow: displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
Text {
visible: parent.activeRow && parent.activeRow.previewType === "text"
visible: parent.activeRow && parent.activeRow.entryType === "text"
anchors.fill: parent
anchors.margins: Style.space(16)
text: parent.activeRow ? (parent.activeRow.isPassword ? "••••••••" : parent.activeRow.previewText) : ""
text: parent.activeRow ? parent.activeRow.fullText : ""
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.title
@@ -351,11 +444,13 @@ Item {
}
Image {
visible: parent.activeRow && parent.activeRow.previewType === "file"
visible: parent.activeRow && parent.activeRow.entryType === "image"
anchors.fill: parent
anchors.margins: Style.space(16)
source: parent.activeRow ? parent.activeRow.previewImage : ""
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
}
}
}
@@ -376,7 +471,7 @@ Item {
}
Text {
text: root.items.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
color: root.foreground
opacity: 0.7
font.family: root.fontFamily
+161 -9
View File
@@ -18,10 +18,14 @@ Item {
var payload = ({})
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
root.pendingInitialMenu = payload.initialMenu || payload.menu || "root"
if (payload.fontFamily) root.fontFamily = payload.fontFamily
root.openExistingMenu(root.pendingInitialMenu)
if (payload.mode === "select" || payload.mode === "input") {
root.openDmenu(payload)
} else {
root.pendingInitialMenu = payload.initialMenu || payload.menu || "root"
root.openExistingMenu(root.pendingInitialMenu)
}
}
function close() {
@@ -37,6 +41,15 @@ Item {
property var defaultMenuItems: []
property var userMenuItems: []
property bool opened: false
property string mode: "menu"
readonly property bool dmenuActive: mode === "select" || mode === "input"
property string dmenuPrompt: ""
property var dmenuOptions: []
property string selectionFile: ""
property string doneFile: ""
property int dmenuWidth: 300
property int dmenuMaxHeight: 0
property bool requestActive: false
property bool rowsLoaded: false
property string activeMenu: "root"
property string filterText: ""
@@ -64,14 +77,40 @@ Item {
property int dividerHeight: Style.space(17)
property bool searchDivider: false
property int layoutSerial: 0
property int cardWidth: Math.min((root.activeMenu === "trigger.capture.screenrecord" || root.activeMenu === "style.font") ? Style.space(520) : Style.space(300), panel.width - Style.gapsOut * 2)
property int visibleRowsHeight: rowListHeight(layoutSerial, displayModel.count, filterText, searchDivider)
property int cardHeight: Math.min(Math.max(Style.space(220), contentMargin * 2 + headerHeight + contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
property int cardWidth: Math.min(root.dmenuActive ? Style.space(root.dmenuWidth) : ((root.activeMenu === "trigger.capture.screenrecord" || root.activeMenu === "style.font") ? Style.space(520) : Style.space(300)), panel.width - Style.gapsOut * 2)
property int visibleRowsHeight: root.dmenuActive ? dmenuRowListHeight(layoutSerial, displayModel.count, filterText) : rowListHeight(layoutSerial, displayModel.count, filterText, searchDivider)
property int cardHeight: root.dmenuActive
? Math.min(contentMargin * 2 + headerHeight + (mode === "input" ? 0 : contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
: Math.min(Math.max(Style.space(220), contentMargin * 2 + headerHeight + contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
function withAlpha(color, alpha) {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function shellQuote(value) {
return "'" + String(value || "").replace(/'/g, "'\\''") + "'"
}
function finishRequest(selection) {
if (!root.requestActive || !root.doneFile) {
root.opened = false
return
}
var activeSelectionFile = root.selectionFile
var activeDoneFile = root.doneFile
root.requestActive = false
root.selectionFile = ""
root.doneFile = ""
if (selection === null || selection === undefined) {
resultProc.command = ["bash", "-lc", ": > " + root.shellQuote(activeDoneFile)]
} else {
resultProc.command = ["bash", "-lc", "printf '%s\\n' " + root.shellQuote(selection) + " > " + root.shellQuote(activeSelectionFile) + "; : > " + root.shellQuote(activeDoneFile)]
}
resultProc.running = true
}
function rowHeightForDetail(detail) {
return root.filterText && detail ? root.detailRowHeight : root.baseRowHeight
}
@@ -94,6 +133,20 @@ Item {
return total
}
function dmenuRowListHeight(_serial, _count, _filter) {
if (root.mode === "input") return 0
if (displayModel.count === 0) return root.baseRowHeight
var count = Math.min(displayModel.count, 10)
var total = 0
for (var i = 0; i < count; i++) {
if (i > 0) total += root.rowSpacing
total += root.baseRowHeight
}
return root.dmenuMaxHeight > 0 ? Math.min(total, Style.space(root.dmenuMaxHeight)) : total
}
function item(id) {
return root.items[id] || null
}
@@ -458,7 +511,52 @@ Item {
}
}
function rebuildDmenuDisplay() {
displayModel.clear()
root.searchDivider = false
if (root.mode === "input") {
layoutSerial += 1
return
}
var query = root.filterText.trim().toLowerCase()
for (var i = 0; i < root.dmenuOptions.length; i++) {
var label = String(root.dmenuOptions[i] || "")
if (query && label.toLowerCase().indexOf(query) < 0) continue
displayModel.append({
itemId: "dmenu." + i,
kind: "dmenu",
icon: "",
label: label,
target: "",
detail: "",
path: "",
childCount: 0,
action: "",
provider: "",
score: i,
section: ""
})
}
layoutSerial += 1
if (displayModel.count === 0) selectedIndex = 0
else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1
else if (selectedIndex < 0) selectedIndex = 0
Qt.callLater(function() {
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
})
}
function rebuildDisplay() {
if (root.dmenuActive) {
root.rebuildDmenuDisplay()
return
}
displayModel.clear()
if (!root.rowsLoaded) return
@@ -540,7 +638,7 @@ Item {
root.filterText = nextFilter
root.selectedIndex = 0
root.cursorActive = false
if (root.filterText.trim()) root.loadProvidersForSearch()
if (!root.dmenuActive && root.filterText.trim()) root.loadProvidersForSearch()
root.rebuildDisplay()
}
@@ -571,6 +669,16 @@ Item {
}
function activateIndex(index) {
if (root.dmenuActive) {
if (root.mode === "input") {
root.applyDmenuSelection(root.filterText)
return
}
if (index < 0 || index >= displayModel.count) return
root.applyDmenuSelection(displayModel.get(index).label)
return
}
if (index < 0 || index >= displayModel.count) return
var row = displayModel.get(index)
@@ -583,6 +691,13 @@ Item {
}
}
function applyDmenuSelection(value) {
applySerial = requestSerial
opened = false
filterText = ""
root.finishRequest(value)
}
function applySelected(id, action) {
if (!id) { cancel(); return }
applySerial = requestSerial
@@ -592,12 +707,17 @@ Item {
}
function cancel() {
if (root.dmenuActive) root.finishRequest(null)
opened = false
filterText = ""
}
function openExistingMenu(initialMenu) {
requestSerial += 1
mode = "menu"
requestActive = false
selectionFile = ""
doneFile = ""
activeMenu = root.item(initialMenu) ? initialMenu : "root"
navStack = []
filterText = ""
@@ -610,6 +730,27 @@ Item {
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
function openDmenu(payload) {
requestSerial += 1
mode = payload.mode === "input" ? "input" : "select"
dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select"))
dmenuOptions = Array.isArray(payload.options) ? payload.options : []
selectionFile = String(payload.selectionFile || "")
doneFile = String(payload.doneFile || "")
requestActive = !!doneFile
dmenuWidth = Math.max(1, Number(payload.width || 300))
dmenuMaxHeight = Math.max(0, Number(payload.maxHeight || 0))
activeMenu = "root"
navStack = []
filterText = ""
selectedIndex = 0
cursorActive = false
opened = true
rebuildDisplay()
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
ListModel { id: displayModel }
// ----------------------------------------------------------- IPC surface
@@ -697,6 +838,14 @@ Item {
}
}
Process {
id: resultProc
onExited: {
if (root.applySerial === root.requestSerial)
root.opened = false
}
}
// The JSONC sources are watched so live edits to the default file (or the
// user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take
// effect without restarting the shell.
@@ -840,7 +989,10 @@ Item {
if (!root.filterText) root.goBack()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Right) {
if (root.cursorActive) root.activateIndex(root.selectedIndex)
if (root.dmenuActive) {
if (root.mode === "input") root.applyDmenuSelection(root.filterText)
else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0)
} else if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = 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)) {
@@ -866,7 +1018,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.filterText || ((root.item(root.activeMenu) ? root.item(root.activeMenu).label : "Go") + "…")
text: root.filterText || (root.dmenuActive ? (root.dmenuPrompt + "…") : ((root.item(root.activeMenu) ? root.item(root.activeMenu).label : "Go") + "…"))
color: root.foreground
opacity: root.filterText ? 1 : 0.58
font.family: root.fontFamily
@@ -1034,7 +1186,7 @@ Item {
Column {
anchors.centerIn: parent
spacing: Style.space(8)
visible: displayModel.count === 0
visible: displayModel.count === 0 && root.mode !== "input"
Text {
text: "󰈉"
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
set -o pipefail
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
IMAGE_DIR="$STATE_DIR/clipboard-images"
mkdir -p "$IMAGE_DIR"
types=$(wl-paste --list-types 2>/dev/null || true)
emit_image() {
local mime="$1"
local ext="$2"
local tmp=""
local hash=""
local file=""
tmp=$(mktemp --tmpdir="$IMAGE_DIR" clipboard.XXXXXX) || return 0
if ! wl-paste --type "$mime" > "$tmp" 2>/dev/null || [[ ! -s $tmp ]]; then
rm -f "$tmp"
return 0
fi
hash=$(sha256sum "$tmp" | awk '{print $1}')
file="$IMAGE_DIR/$hash.$ext"
if [[ -e $file ]]; then
rm -f "$tmp"
else
mv "$tmp" "$file"
fi
jq -cn --arg mime "$mime" --arg path "$file" '{type:"image", mime:$mime, path:$path}'
}
if grep -qx 'image/png' <<<"$types"; then
emit_image 'image/png' 'png'
elif grep -qx 'image/jpeg' <<<"$types"; then
emit_image 'image/jpeg' 'jpg'
elif grep -qx 'image/webp' <<<"$types"; then
emit_image 'image/webp' 'webp'
elif grep -qx 'image/gif' <<<"$types"; then
emit_image 'image/gif' 'gif'
elif grep -qx 'image/bmp' <<<"$types"; then
emit_image 'image/bmp' 'bmp'
elif grep -qx 'image/tiff' <<<"$types"; then
emit_image 'image/tiff' 'tiff'
elif grep -q '^text/' <<<"$types" || grep -qx 'UTF8_STRING' <<<"$types" || grep -qx 'STRING' <<<"$types"; then
wl-paste --type text --no-newline 2>/dev/null | jq -Rs 'select(length > 0) | {type:"text", text:.}'
fi