Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4930677
Give foot its own touchpad scroll factor (#9793)
dhh Sep 4, 2026
7004795
Harden lock authentication command lookup
mdisec Sep 4, 2026
8f15549
Ask, default no, before Remove Hermes deletes the user's data
spencerbull Sep 5, 2026
e8e92c5
Register the Chromium native messaging hosts for Brave Origin (#10292)
dhh Sep 5, 2026
110cb8f
Add original vi as a standard terminal editor (#10307)
dhh Sep 5, 2026
5345673
Add OpenClaw to Install > AI as a web app on its own gateway
spencerbull Sep 4, 2026
eb56446
Merge pull request #10246 from omacom/add-openclaw-ai-app
spencerbull Sep 5, 2026
fa60a8b
Add the Perplexity desktop app to Install > AI
spencerbull Sep 1, 2026
36e56f4
Drop tests for shipped one-shot migrations (#10318)
dhh Sep 5, 2026
eea678b
Ask, default no, before Remove Perplexity deletes the user's data
spencerbull Sep 5, 2026
a1095af
Ask about the user's data whenever it exists, not only behind the boo…
spencerbull Sep 5, 2026
959e49d
Merge pull request #10271 from spencerbull/hermes-remove-ask-user-data
spencerbull Sep 6, 2026
f1b065c
Merge pull request #9625 from spencerbull/t3code/add-perplexity-ai-app
spencerbull Sep 6, 2026
6baae0f
Make Hermes follow the Omarchy theme as a skin
spencerbull Sep 5, 2026
8569d1c
Harden the Hermes skin hand-over
spencerbull Sep 6, 2026
988da12
Merge pull request #10381 from spencerbull/hermes-system-theme
spencerbull Sep 6, 2026
adcc96a
Install libfprint-git for every fingerprint reader (#10442)
powderluv Sep 6, 2026
41b6cc6
Add native video wallpaper support (#6792)
gardensazurescens Sep 6, 2026
a62e34e
Add Cursor CLI as a coding agent choice (#10465)
dhh Sep 6, 2026
a9eaf79
Point the shipped agent skills at omacom/omarchy (#10532)
steelcityappliance Sep 6, 2026
1d466c4
Add o.rebind for replacing Hyprland keybindings
ryanrhughes Sep 6, 2026
f4a462e
Merge pull request #10537 from omacom/add-hyprland-rebind
ryanrhughes Sep 6, 2026
0d223fe
Add Muse Code as a default coding agent (#9915)
jp30566347 Sep 6, 2026
97a86af
Nix ideas
dhh Aug 29, 2026
3f91f06
Merge pull request #10225 from mdisec/security/harden-quattro-fprintd…
ryanrhughes Sep 6, 2026
a51b229
Merge upstream quattro into the fork.
cursoragent Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Suggestion
url: https://github.com/basecamp/omarchy/discussions/categories/suggestions
url: https://github.com/omacom/omarchy/discussions/categories/suggestions
about: Suggest a new feature, change to existing feature, or other ideas in Discussions.
- name: Support
url: https://omarchy.org/discord
Expand Down
18 changes: 13 additions & 5 deletions bin/omarchy-agent
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ crush)
command=(crush --yolo)
fi
;;
cursor-agent)
# --yolo skips command approvals; --trust skips the workspace trust prompt.
command=(cursor-agent --yolo --trust)
[[ -n ${prompt:-} ]] && command+=("$prompt")
;;
claude)
command=(claude --permission-mode auto)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
Expand All @@ -100,13 +95,26 @@ codex)
command=(codex --approve-for-me)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
cursor-agent)
# --yolo covers commands only; the workspace trust dialog has its own flag.
# A one-word prompt naming a subcommand (update, login, help) still runs that
# subcommand after a bare --, so the agent subcommand is named outright, and
# -- after it keeps a prompt starting with a dash from being read as an option.
command=(cursor-agent --yolo --trust)
[[ -n ${prompt:-} ]] && command+=(agent -- "$prompt")
;;
hermes)
if [[ -n ${prompt:-} ]]; then
command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt")
else
command=(hermes --yolo)
fi
;;
muse)
# --approval-mode never skips the tool prompts but keeps Muse's own sandbox.
command=(muse --approval-mode never)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
omp)
command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
Expand Down
9 changes: 8 additions & 1 deletion bin/omarchy-apply-lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

set -e

# Install and upgrade callers can start this helper as root. Ignore their PATH
# so optional commands never fall through to a user-writable directory.
if (( EUID == 0 )); then
export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin
fi

target_user=${OMARCHY_INSTALL_USER:-${SUDO_USER:-}}
if [[ -z $target_user && -n ${PKEXEC_UID:-} ]]; then
target_user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1)
Expand Down Expand Up @@ -34,7 +40,8 @@ auth required pam_faillock.so authsucc
account include system-local-login
EOF

if omarchy-cmd-present fprintd-list && fprintd-list "$target_user" 2>/dev/null | grep -qi finger; then
if [[ -x /usr/bin/fprintd-list ]] &&
/usr/bin/fprintd-list "$target_user" 2>/dev/null | grep -qi finger; then
echo "Configuring lock screen fingerprint authentication..."
as_root tee /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
#%PAM-1.0
Expand Down
5 changes: 4 additions & 1 deletion bin/omarchy-bar-text-color
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ right)
;;
esac

pixel=$(magick "$background_path" -auto-orient \
# Sample the first frame only. Without the selector a video background makes
# ImageMagick decode the whole file and emit one value per frame, and the match
# below then fails into the fallback colour.
pixel=$(magick "$background_path[0]" -auto-orient \
-resize "${screen_width}x${screen_height}^" \
-gravity center -extent "${screen_width}x${screen_height}" \
-gravity NorthWest -crop "$crop" +repage \
Expand Down
20 changes: 16 additions & 4 deletions bin/omarchy-default-agent
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

# omarchy:summary=Set and launch the default coding agent
# omarchy:args=[pi|omp|opencode|ori|claude|codex|cursor|grok|openclaw|agy|hermes|copilot|crush]
# omarchy:args=[pi|omp|opencode|ori|claude|codex|cursor|grok|openclaw|agy|hermes|copilot|crush|muse]
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude

installing=false
Expand Down Expand Up @@ -37,8 +37,13 @@ openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-op
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
muse | muse-code | musecode)
agent="muse"; name="Muse Code"
# Meta's launcher verifies and updates the native binary for this platform.
agent_package="http:muse[url=https://api.meta.ai/muse-launcher.sh,bin=muse,version_list_url=https://api.meta.ai/muse-code/channels/muse-stable,version_json_path=.version]"
;;
*)
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|cursor|grok|openclaw|agy|hermes|copilot|crush>"
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|cursor|grok|openclaw|agy|hermes|copilot|crush|muse>"
exit 1
;;
esac
Expand Down Expand Up @@ -77,8 +82,15 @@ else
agent_install() { "$agent_installer" --now; }
install_failure="Could not install $name"
else
agent_present() { mise where "$agent_package" &>/dev/null; }
agent_install() { mise use -g "$agent_package"; }
# Anything at the wrapper's path other than the wrapper is the user's own
# install, such as the symlink Cursor's installer leaves. A mise copy would
# only shadow it, since the mise shims precede ~/.local/bin on PATH.
user_install() {
[[ -x $HOME/.local/bin/$agent ]] &&
{ [[ -L $HOME/.local/bin/$agent ]] || ! grep -q '^mise use -g' "$HOME/.local/bin/$agent"; }
}
agent_present() { user_install || mise where "$agent_package" &>/dev/null; }
agent_install() { user_install || mise use -g "$agent_package"; }
install_failure="Could not install $name with mise"
fi

Expand Down
6 changes: 6 additions & 0 deletions bin/omarchy-install-ai-hermes
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ omarchy-install-hermes-cli || true
echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &

# Only a running Hermes can be told which skin to show, and the first launch
# takes minutes; a unit outlives this terminal and reports to the journal.
echo "Matching Hermes to the current theme once it is set up..."
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme omarchy-theme-set-hermes --wait

echo ""
echo "Hermes Desktop has been installed."
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
89 changes: 73 additions & 16 deletions bin/omarchy-menu-images
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ fi
selection_file=$(mktemp)
done_file=$(mktemp)
pending_file=$(mktemp)
pending_video_file=$(mktemp)
rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file" "$pending_file"' EXIT
trap 'rm -f "$selection_file" "$done_file" "$pending_file" "$pending_video_file"' EXIT

image_dirs_env=""
for dir in "${image_dirs[@]}"; do
Expand Down Expand Up @@ -111,8 +112,8 @@ cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1)
rows_cache_file="$cache_dir/$cache_key.rows"
rows_signature_file="$cache_dir/$cache_key.signature"
rows_fast_signature_file="$cache_dir/$cache_key.fast-signature"
rows_signature="v3"$'\n'
rows_fast_signature="v2"$'\n'
rows_signature="v4"$'\n'
rows_fast_signature="v3"$'\n'
rows_cacheable=true
rows_cache_hit=false
image_files=()
Expand All @@ -133,17 +134,25 @@ else
image_files+=("$image")
image_signature=$(stat -Lc '%s:%Y' "$image") || continue
rows_signature+="$image:$image_signature"$'\n'
done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) -print0 2>/dev/null | sort -z)
done < <(find -L "$dir" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \
-o -iname '*.mp4' -o -iname '*.m4v' -o -iname '*.mov' -o -iname '*.webm' -o -iname '*.mkv' -o -iname '*.avi' \) \
-print0 2>/dev/null | sort -z)
fi
done
fi

is_video_path() {
[[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]]
}

generate_thumbnail() {
local image="$1"
local thumbnail="$2"
local lock="$thumbnail.lock"
local lock_fd
local tmp="$thumbnail.$$.jpg"
local thumbnail_command

# Older releases used directories as locks, which could survive a killed
# generator and block this thumbnail forever. Only reap aged ones, so a
Expand All @@ -161,13 +170,29 @@ generate_thumbnail() {

[[ -f $thumbnail ]] && return

# Callers fan out one generator per image, so keep each vips single-threaded.
# Close the lock fd for vips: an orphaned or hung vips must not keep holding
# the lock after this shell is killed.
if VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]" {lock_fd}>&-; then
# Callers fan out one generator per file, so keep each image conversion
# single-threaded. ffmpegthumbnailer provides a still preview for videos.
if is_video_path "$image"; then
# Videos are generated before the picker opens, so one unreadable or
# stalled file must not hold it shut. A failed run drops the row.
thumbnail_command=(timeout -k 5 10 ffmpegthumbnailer -i "$image" -o "$tmp" -s 1536 -q 8)
else
thumbnail_command=(env VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]")
fi

# Close the lock fd for the converter: an orphaned or hung child must not
# keep holding the lock after this shell is killed.
if "${thumbnail_command[@]}" {lock_fd}>&-; then
mv -f "$tmp" "$thumbnail"
else
status=$?
rm -f "$tmp" "$thumbnail"
# Remember a video the converter rejected, so it costs nothing on the next
# open. The key covers size and mtime, so a repaired file starts clean. A
# timeout is left to retry: the machine may only have been busy.
if is_video_path "$image" && (( status != 124 && status != 137 )); then
: >"$thumbnail.failed"
fi
fi
}

Expand All @@ -186,7 +211,17 @@ thumbnail_for() {
thumbnail="$cache_dir/$hash.jpg"

if [[ ! -f $thumbnail ]]; then
if [[ $lazy_thumbnails == true && $cache_only != true ]]; then
# A video that already failed to convert has no row to offer. Hand the
# marker back so the caller can keep the rows uncached over its absence.
if is_video_path "$image" && [[ -f $thumbnail.failed ]]; then
printf '%s' "$thumbnail.failed"
return
fi

# A lazy row stands in with the media file itself, which the picker draws
# with an Image -- fine for a picture, blank for a video. Videos take the
# queue instead, which also keeps them under its narrower fan out.
if [[ $lazy_thumbnails == true && $cache_only != true ]] && ! is_video_path "$image"; then
rows_cacheable=false

if [[ $prepare_only != true ]]; then
Expand All @@ -197,19 +232,35 @@ thumbnail_for() {
return
fi

printf '%s\0%s\0' "$image" "$thumbnail" >>"$pending_file"
if is_video_path "$image"; then
printf '%s\0%s\0' "$image" "$thumbnail" >>"$pending_video_file"
else
printf '%s\0%s\0' "$image" "$thumbnail" >>"$pending_file"
fi
fi

printf '%s' "$thumbnail"
}

# Generate every queued thumbnail at once; each vips run is single-threaded.
# Each vips run is single-threaded, so still images can fill every core.
# ffmpegthumbnailer leaves FFmpeg's automatic threading on, so a full-width fan
# out of those would put a codec thread pool on every core at once.
drain_pending_thumbnails() {
[[ -s $pending_file ]] || return 0
local video_jobs

export -f generate_thumbnail is_video_path

if [[ -s $pending_file ]]; then
xargs -a "$pending_file" -0 -n 2 -P "$(nproc)" \
bash -c 'generate_thumbnail "$1" "$2"' _ >/dev/null 2>&1 || true
fi

export -f generate_thumbnail
xargs -a "$pending_file" -0 -n 2 -P "$(nproc)" \
bash -c 'generate_thumbnail "$1" "$2"' _ >/dev/null 2>&1 || true
if [[ -s $pending_video_file ]]; then
video_jobs=$(( $(nproc) / 4 ))
(( video_jobs > 0 )) || video_jobs=1
xargs -a "$pending_video_file" -0 -n 2 -P "$video_jobs" \
bash -c 'generate_thumbnail "$1" "$2"' _ >/dev/null 2>&1 || true
fi
}

if [[ $rows_cache_hit != true && -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then
Expand All @@ -219,6 +270,12 @@ elif [[ $rows_cache_hit != true ]]; then
for image in "${image_files[@]}"; do
thumbnail=$(thumbnail_for "$image")
[[ -n $thumbnail ]] || continue
# Cached rows are trusted on the directory's mtime alone, which a file
# repaired in place never changes. Leave them uncached instead.
if [[ $thumbnail == *.failed ]]; then
rows_cacheable=false
continue
fi
if [[ $lazy_thumbnails == true && $cache_only != true && $thumbnail == $image ]]; then
rows_cacheable=false
fi
Expand All @@ -232,7 +289,7 @@ elif [[ $rows_cache_hit != true ]]; then

drain_pending_thumbnails

if [[ -s $pending_file ]]; then
if [[ -s $pending_file || -s $pending_video_file ]]; then
pruned=""
while IFS=$'\t' read -r row_image row_thumbnail; do
if [[ ! -e $row_thumbnail ]]; then
Expand Down
48 changes: 36 additions & 12 deletions bin/omarchy-remove-ai-hermes
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ set -euo pipefail

omarchy-pkg-drop hermes-desktop

# The installer leaves a unit waiting to hand the app the Omarchy theme.
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true

# The mise CLI is the app's predecessor, not the app itself: Hermes Desktop takes
# it over on install and runs its own runtime instead, so a copy still here is one
# the app never superseded -- an interrupted install, or the terminal CLI from
Expand All @@ -22,14 +25,11 @@ omarchy-install-hermes-cli --remove || cli_removed=false
# and it is the only thing that tells that runtime apart from one the user
# installed themselves -- the paths are the same either way. Without it the app
# never got that far: a machine where it was installed but never launched still
# has whatever was there before, and none of it is ours to delete.
# has whatever was there before, and none of it is ours to delete unasked.
if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then
# The checkout and venv, its own uv, its own node. None of it is any use once
# the app is gone. Not ~/.config/Hermes, which holds the gateway connections
# and their encrypted tokens, the active profile and the update settings. Not
# the rest of ~/.hermes either: the chats, memories and the skills Hermes
# wrote for itself are the user's, they are small, and finding them still
# there after a reinstall is the better surprise.
# the app is gone, so it goes without asking; what the user made with the app
# is a different question, answered below.
rm -rf \
"$HOME/.hermes/hermes-agent" \
"$HOME/.hermes/bootstrap-cache" \
Expand Down Expand Up @@ -58,14 +58,38 @@ if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then
fi
done

echo ""
echo "Hermes Desktop has been removed."
fi

# What survives to here is the user's: the chats, memories and skills in
# ~/.hermes, the connections and their encrypted tokens in ~/.config/Hermes.
# Keeping them stays the default -- they are small, and finding them intact
# after a reinstall is the better surprise -- but a removal meant to be
# complete should not leave credentials behind either, so the choice is put in
# front of the user with the size, default no. Asked whenever the directories
# exist, marker or no marker: on a machine where the marker never appeared the
# data came from the terminal CLI or an install the app never finished, and it
# is still what removal is asked to clean up. Naming the paths keeps the
# question honest there too -- ~/.hermes may still carry a runtime the app
# never owned, a yes takes that with it, and saying so is the prompt's job.
# Without a terminal to ask in, keeping everything is the answer.
data_removed=false
if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && command -v gum >/dev/null; then
# du answers non-zero when either directory is missing, and pipefail would
# turn that into an aborted removal; the size is worth no such thing.
size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true)
if gum confirm --default=false "Also delete ~/.hermes and ~/.config/Hermes ($size: chats, memories, skills, connections and tokens)?"; then
rm -rf "$HOME/.hermes" "$HOME/.config/Hermes"
data_removed=true
fi
fi

echo ""
echo "Hermes Desktop has been removed."
if [[ $data_removed == true ]]; then
echo "Its chats, memories, and settings in ~/.hermes and ~/.config/Hermes are gone too."
elif [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]]; then
echo "Your chats, memories, and skills are still in ~/.hermes,"
echo "and your connections and settings in ~/.config/Hermes."
else
echo ""
echo "Hermes Desktop has been removed."
echo "It never finished installing its own Hermes, so nothing in ~/.hermes was touched."
fi

# The messages above still hold -- the app and its runtime are gone -- but a CLI
Expand Down
Loading