Compare commits
103
Commits
china-region
...
v4.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0534987009 | ||
|
|
b9657b6313 | ||
|
|
15b4928e11 | ||
|
|
4ea7ca2b8b | ||
|
|
f4d156c09a | ||
|
|
6ba4ceef57 | ||
|
|
3248aa55c2 | ||
|
|
7cc9057413 | ||
|
|
d57cec42d4 | ||
|
|
c5b59bcfff | ||
|
|
a9e1c01ee9 | ||
|
|
4064436eea | ||
|
|
2cdcafa5b5 | ||
|
|
461cbe9b9c | ||
|
|
256bfeaa75 | ||
|
|
36695fbe84 | ||
|
|
9d95fc7f01 | ||
|
|
e919a64014 | ||
|
|
866d994dcf | ||
|
|
cf459ba752 | ||
|
|
1fe83ff129 | ||
|
|
6a72d4236b | ||
|
|
7a35a8ee9b | ||
|
|
6d327ce83c | ||
|
|
f94c6db34d | ||
|
|
8d1a7ef00c | ||
|
|
6cc561b8d3 | ||
|
|
6cb5396dc7 | ||
|
|
27594ba3f5 | ||
|
|
be6c401200 | ||
|
|
d49dd939b7 | ||
|
|
68e3d7a2ec | ||
|
|
15d64a7025 | ||
|
|
82de446713 | ||
|
|
ebaad12947 | ||
|
|
cf6fedc9f5 | ||
|
|
fb257bf872 | ||
|
|
1cbe72e5dd | ||
|
|
b1cdec9e47 | ||
|
|
af6f64fa3a | ||
|
|
346e69e1ce | ||
|
|
0e1ce609d8 | ||
|
|
3cb3b2d20b | ||
|
|
f8e35ec654 | ||
|
|
f802ae107d | ||
|
|
6f8b62f04c | ||
|
|
9aa0545c2a | ||
|
|
ebd6480387 | ||
|
|
07579f5e94 | ||
|
|
ff4cf8afbc | ||
|
|
cf3d69c38d | ||
|
|
47ce81ebca | ||
|
|
2421e76fb8 | ||
|
|
bffc6a2a5d | ||
|
|
7678e5b0d8 | ||
|
|
be730884f6 | ||
|
|
a73a312e1b | ||
|
|
159163bb16 | ||
|
|
521779b114 | ||
|
|
51af7bf794 | ||
|
|
92c8f87730 | ||
|
|
3bb9867245 | ||
|
|
da0fe6d89f | ||
|
|
2645b82bcd | ||
|
|
9c1ec524cd | ||
|
|
880605f22b | ||
|
|
294c38a2f0 | ||
|
|
1082b7864d | ||
|
|
13f18b2cb7 | ||
|
|
2e989e35e5 | ||
|
|
c6d676f23c | ||
|
|
0f836c0deb | ||
|
|
07adef8a53 | ||
|
|
e713ff3166 | ||
|
|
c7af36d0aa | ||
|
|
b3028f9bd9 | ||
|
|
0f4abeec8e | ||
|
|
7b89780810 | ||
|
|
c0b593b349 | ||
|
|
7fa32bb98c | ||
|
|
2b1d3c4606 | ||
|
|
286b8c2b1c | ||
|
|
f2a2973de8 | ||
|
|
64c5c02f55 | ||
|
|
d0aefc1f79 | ||
|
|
1626405638 | ||
|
|
9b192256e7 | ||
|
|
2487fbcce1 | ||
|
|
9434f220f0 | ||
|
|
9e79b24342 | ||
|
|
6245965cb6 | ||
|
|
f6b4183e4f | ||
|
|
39c2797d3b | ||
|
|
8b979e0c4c | ||
|
|
5c2fb68c0a | ||
|
|
2d3826aff3 | ||
|
|
eb201d0bd3 | ||
|
|
ac1bdbfbf5 | ||
|
|
9f7cdf6983 | ||
|
|
1867dcaa50 | ||
|
|
33d7363c33 | ||
|
|
84336759cc | ||
|
|
5f0704a0f8 |
No files matched your search
@@ -61,19 +61,18 @@ its screenshots are also hosted.
|
||||
- [Prompt](manual/40-prompt.md)
|
||||
- [Branding](manual/41-branding.md)
|
||||
- [Common tweaks](manual/42-common-tweaks.md)
|
||||
- [Extra themes](manual/43-extra-themes.md)
|
||||
- [Making your own theme](manual/44-making-your-own-theme.md)
|
||||
- [Making your own theme](manual/43-making-your-own-theme.md)
|
||||
|
||||
**The Rest**
|
||||
|
||||
- [Mac support](manual/45-mac-support.md)
|
||||
- [Troubleshooting](manual/46-troubleshooting.md)
|
||||
- [FAQ](manual/47-faq.md)
|
||||
- [System snapshots](manual/48-system-snapshots.md)
|
||||
- [Security](manual/49-security.md)
|
||||
- [Omarchy on...](manual/50-omarchy-on.md)
|
||||
- [Dual Boot Install](manual/51-dual-boot-install.md)
|
||||
- [Unattended Installs](manual/52-unattended-installs.md)
|
||||
- [Mac support](manual/44-mac-support.md)
|
||||
- [Troubleshooting](manual/45-troubleshooting.md)
|
||||
- [FAQ](manual/46-faq.md)
|
||||
- [System snapshots](manual/47-system-snapshots.md)
|
||||
- [Security](manual/48-security.md)
|
||||
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -70,10 +70,6 @@ magick -background white -fill black -font default/fonts/omarchy/omarchy.ttf \
|
||||
-pointsize 110 label:@/tmp/row.txt /tmp/font-row.png
|
||||
```
|
||||
|
||||
Then confirm it in the running menu per
|
||||
[`visual-verification.md`](visual-verification.md). Fontconfig prefers the
|
||||
packaged font over a copy in `~/.local/share/fonts` for the same family, so a
|
||||
preview needs either the real file replaced or a `<rejectfont>` rule in
|
||||
`~/.config/fontconfig/conf.d/` pointing fontconfig away from the packaged one.
|
||||
Restart the shell afterwards — Qt reads the font database at startup, so
|
||||
`omarchy menu refresh` alone will not pick up a changed font.
|
||||
Then confirm it in the running menu per [`visual-verification.md`](visual-verification.md). Avoid leaving two fonts with the `omarchy` family registered: Qt can use an old copy in `~/.local/share/fonts` even when `fc-match omarchy` reports the packaged font. For a preview, either replace the packaged file in the disposable VM or temporarily exclude it with a `<rejectfont>` rule in `~/.config/fontconfig/conf.d/` before loading the candidate.
|
||||
|
||||
Refresh the font cache and restart the shell afterwards — Qt reads the font database at startup, so `omarchy menu refresh` alone will not pick up a changed font. Remove temporary fonts and rules after verification.
|
||||
@@ -20,9 +20,7 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
||||
[`docs/omarchy-shell.md`](../../docs/omarchy-shell.md) and
|
||||
`shell/services/PluginRegistry.qml` for the current contract; fields such as
|
||||
`activation` are optional.
|
||||
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the
|
||||
shell-injected properties `omarchyPath`, `shell`, `manifest`, and
|
||||
`pluginRegistry` / `barWidgetRegistry` as appropriate.
|
||||
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the shell-injected properties `omarchyPath`, `shell`, `manifest`, and `pluginRegistry` / `barWidgetRegistry` as appropriate. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state; full-bar plugins additionally receive detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities must be stamped from trusted first-party manifests, and third-party registry views and bar configuration must be detached snapshots rather than shared objects. These facades reduce accidental authority but are not a same-process QML sandbox: a visual bar widget can walk its parent hierarchy to ordinary host objects. Authentication services must therefore remain outside both `ShellRoot._services` and the host QObject tree. Do not expose authentication services through new third-party-facing properties.
|
||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Docker
|
||||
Exec=xdg-terminal-exec --app-id=TUI.tile -e lazydocker
|
||||
Exec=xdg-terminal-exec --app-id=TUI.tile -e omarchy-launch-docker-tui
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=docker
|
||||
|
||||
@@ -63,6 +63,7 @@ GROUP_DESCRIPTIONS[monitor]="Monitor status helpers"
|
||||
GROUP_DESCRIPTIONS[network]="Network status helpers"
|
||||
GROUP_DESCRIPTIONS[notification]="Notification helpers"
|
||||
GROUP_DESCRIPTIONS[mise]="Mise tool wrappers"
|
||||
GROUP_DESCRIPTIONS[openclaw]="OpenClaw agent platform setup"
|
||||
GROUP_DESCRIPTIONS[osd]="On-screen display status helpers"
|
||||
GROUP_DESCRIPTIONS[pkg]="Package management helpers"
|
||||
GROUP_DESCRIPTIONS[plugin]="Omarchy shell plugin and bar widget management"
|
||||
|
||||
+36
-3
@@ -74,12 +74,45 @@ crush)
|
||||
command=(crush --yolo)
|
||||
fi
|
||||
;;
|
||||
claude | grok)
|
||||
command=("$agent" --permission-mode bypassPermissions)
|
||||
claude)
|
||||
command=(claude --permission-mode auto)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
grok)
|
||||
command=(grok --permission-mode bypassPermissions)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
openclaw)
|
||||
# The launcher owns onboarding and the gateway dance: OpenClaw's terminal UI
|
||||
# must attach to the running gateway (the embedded `openclaw chat` refuses to
|
||||
# start while the gateway owns the state directory). It has no permission
|
||||
# prompts to skip, and --message seeds the session while keeping it
|
||||
# interactive.
|
||||
command=(omarchy-launch-openclaw --tui)
|
||||
[[ -n ${prompt:-} ]] && command+=(--message "$prompt")
|
||||
;;
|
||||
codex)
|
||||
command=(codex --dangerously-bypass-approvals-and-sandbox)
|
||||
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)
|
||||
|
||||
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
|
||||
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
# omarchy:args=[--no-osd] [+N%|N%-|N%]
|
||||
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
|
||||
|
||||
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device"
|
||||
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
|
||||
# caching (detect every run) rather than fall back to a predictable, world-writable
|
||||
# /tmp path another user could pre-create.
|
||||
device_cache=""
|
||||
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
|
||||
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
|
||||
fi
|
||||
no_osd=0
|
||||
if [[ ${1:-} == "--no-osd" ]]; then
|
||||
no_osd=1
|
||||
@@ -28,9 +34,14 @@ find_apple_display_device() {
|
||||
local cached=""
|
||||
local device=""
|
||||
|
||||
if [[ -r $device_cache ]]; then
|
||||
if [[ -n $device_cache && -r $device_cache ]]; then
|
||||
read -r cached <"$device_cache" || true
|
||||
if [[ -n $cached && -e $cached ]]; then
|
||||
# Trust a cached value only if it still names a hiddev character device. A
|
||||
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
|
||||
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
|
||||
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
|
||||
# and quoting them would turn the match into a literal string comparison.
|
||||
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
|
||||
printf '%s\n' "$cached"
|
||||
return 0
|
||||
fi
|
||||
@@ -39,7 +50,9 @@ find_apple_display_device() {
|
||||
device="$(detect_apple_display_device)" || return 1
|
||||
[[ -n $device ]] || return 1
|
||||
|
||||
printf '%s\n' "$device" >"$device_cache"
|
||||
if [[ -n $device_cache ]]; then
|
||||
printf '%s\n' "$device" >"$device_cache"
|
||||
fi
|
||||
printf '%s\n' "$device"
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ stop_screenrecording() {
|
||||
|
||||
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
||||
-t 10000 --image "${preview:-$filename}" \
|
||||
--exec "$(printf 'mpv %q' "$filename")"
|
||||
--exec mpv -- "$filename"
|
||||
|
||||
# The shell loads the thumbnail into memory when the toast appears and never
|
||||
# re-reads the file, so the preview only has to outlive that load -- not the
|
||||
|
||||
@@ -70,7 +70,7 @@ case "$PROCESSING" in
|
||||
# notification outage must not report the capture itself as failed.
|
||||
omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" \
|
||||
--image "$FILEPATH" \
|
||||
--exec "$(printf '%q %q' "$SCREENSHOT_EDITOR" "$FILEPATH")" || true
|
||||
--exec "$SCREENSHOT_EDITOR" "$FILEPATH" || true
|
||||
;;
|
||||
copy)
|
||||
grim -g "$SELECTION" - | wl-copy --type image/png
|
||||
|
||||
@@ -25,6 +25,51 @@ valid_url() {
|
||||
[[ $1 =~ ^https?:// ]]
|
||||
}
|
||||
|
||||
# A printed path is only usable if it is a regular file inside DOWNLOAD_DIR.
|
||||
# Forged records (leading-dash mpv options, paths with control chars, or
|
||||
# anything that escaped the download directory) must not reach the click command.
|
||||
resolve_download_file() {
|
||||
local candidate=$1 file_real dir_real
|
||||
|
||||
[[ -n $candidate ]] || return 1
|
||||
[[ $candidate != *$'\n'* && $candidate != *$'\r'* && $candidate != *$'\t'* ]] || return 1
|
||||
[[ -f $candidate ]] || return 1
|
||||
|
||||
# Read to a NUL: command substitution strips trailing newlines, which would
|
||||
# resolve a name ending in one to a different file that may well exist.
|
||||
IFS= read -r -d '' file_real < <(realpath -ze -- "$candidate") || return 1
|
||||
IFS= read -r -d '' dir_real < <(realpath -ze -- "$DOWNLOAD_DIR") || return 1
|
||||
|
||||
[[ $file_real != *$'\n'* && $file_real != *$'\r'* && $file_real != *$'\t'* ]] || return 1
|
||||
# Trim the slash so a download directory of "/" still leaves a usable prefix.
|
||||
[[ $file_real == "${dir_real%/}"/* ]] || return 1
|
||||
|
||||
printf '%s' "$file_real"
|
||||
}
|
||||
|
||||
# yt-dlp prints the title JSON-encoded, so a newline or tab in page metadata is an
|
||||
# escape sequence rather than a record boundary. This is toast text, never a command.
|
||||
decode_title() {
|
||||
local decoded
|
||||
|
||||
decoded=$(jq -r 'if type == "string" then . else empty end' <<<"$1" 2>/dev/null) || return 1
|
||||
decoded=${decoded%%[[:cntrl:]]*} # keep what a person would read, drop the forgery
|
||||
[[ -n $decoded && $decoded != -* ]] || return 1
|
||||
|
||||
printf '%s' "$decoded"
|
||||
}
|
||||
|
||||
title_from_file() {
|
||||
local name=${1##*/}
|
||||
name=${name%.*}
|
||||
name=${name//[$'\n\r\t']/}
|
||||
if [[ -z $name || $name == -* ]]; then
|
||||
printf '%s' "Video"
|
||||
else
|
||||
printf '%s' "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
# Drive the Quickshell OSD — a single overlay that updates in place (like the
|
||||
# volume/brightness bar), so download progress never stacks like notifications.
|
||||
osd_progress() {
|
||||
@@ -41,16 +86,19 @@ download_url() {
|
||||
mkdir -p "$DOWNLOAD_DIR"
|
||||
|
||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings "$url" >/dev/null 2>&1; then
|
||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
||||
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
osd_progress 0
|
||||
|
||||
# Stream the download: OMARCHY_PROG carries the percent (drives the OSD),
|
||||
# OMARCHY_FILE (printed only after a successful move) carries title + path.
|
||||
local line rest pct intpct last="" er nowms lastms=0 title="" filepath=""
|
||||
# Stream the download: OMARCHY_PROG carries the percent (drives the OSD), and
|
||||
# OMARCHY_FILE and OMARCHY_TITLE (printed only after a successful move) carry the
|
||||
# path and the title. The title is JSON-encoded so metadata cannot forge a record,
|
||||
# and the file is named after it: yt-dlp strips control characters from a filename
|
||||
# with or without --restrict-filenames, so a record is still only ever one line.
|
||||
local line pct intpct last="" er nowms lastms=0 title="" filepath="" resolved
|
||||
while IFS= read -r line; do
|
||||
case $line in
|
||||
OMARCHY_PROG*)
|
||||
@@ -67,22 +115,26 @@ download_url() {
|
||||
osd_progress "$intpct"
|
||||
;;
|
||||
OMARCHY_FILE*)
|
||||
rest=${line#OMARCHY_FILE$'\t'}
|
||||
title=${rest%%$'\t'*}
|
||||
filepath=${rest#*$'\t'}
|
||||
resolved=$(resolve_download_file "${line#OMARCHY_FILE$'\t'}") || continue
|
||||
filepath=$resolved
|
||||
;;
|
||||
OMARCHY_TITLE*)
|
||||
title=$(decode_title "${line#OMARCHY_TITLE$'\t'}") || title=""
|
||||
;;
|
||||
esac
|
||||
done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --restrict-filenames --no-simulate \
|
||||
--quiet --no-warnings --progress --newline \
|
||||
done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --no-simulate \
|
||||
--quiet --no-warnings --no-exec --no-exec-before-download --progress --newline \
|
||||
--progress-template $'download:OMARCHY_PROG\t%(progress._percent_str)s' \
|
||||
--paths "$DOWNLOAD_DIR" -o '%(title)s [%(id)s].%(ext)s' \
|
||||
--print $'after_move:OMARCHY_FILE\t%(title)s\t%(filepath)s' \
|
||||
"$url" 2>&1)
|
||||
--paths "$DOWNLOAD_DIR" -o '%(title)s.%(ext)s' \
|
||||
--print $'after_move:OMARCHY_FILE\t%(filepath)s' \
|
||||
--print $'after_move:OMARCHY_TITLE\t%(title)j' \
|
||||
-- "$url" 2>&1)
|
||||
|
||||
osd_close
|
||||
|
||||
# after_move only prints on a successful download+move, so a captured path == success.
|
||||
if [[ -n $filepath ]]; then
|
||||
[[ -n $title ]] || title=$(title_from_file "$filepath")
|
||||
((${#title} > 50)) && title="${title:0:50}…" # keep the toast compact
|
||||
|
||||
# Square, center-cropped thumbnail so the notification preview isn't stretched.
|
||||
@@ -94,9 +146,11 @@ download_url() {
|
||||
|
||||
# Best-effort: the download already succeeded, and under `set -e` a failed
|
||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||
# is one discrete argument, so it never reaches a shell.
|
||||
omarchy-notification-send -g "Download complete" "$title" \
|
||||
-t 10000 --image "${preview:-$filepath}" \
|
||||
--exec "$(printf 'mpv %q' "$filepath")" || true
|
||||
--exec mpv -- "$filepath" || true
|
||||
|
||||
# The shell loads the thumbnail into memory when the toast appears and never
|
||||
# re-reads the file, so the preview only has to outlive that load, not the
|
||||
|
||||
@@ -24,9 +24,7 @@ readonly ignore_pattern=${OMARCHY_CRASH_IGNORE:-}
|
||||
declare -A last_notified
|
||||
|
||||
announce() {
|
||||
local comm=$1 pid=$2 exe=$3 signal=$4 exec_command
|
||||
|
||||
exec_command=$(printf 'omarchy-agent-crash %q %q %q %q' "$pid" "$comm" "$exe" "$signal")
|
||||
local comm=$1 pid=$2 exe=$3 signal=$4
|
||||
|
||||
# The shell owns org.freedesktop.Notifications, so a shell crash takes the
|
||||
# notification server down with it and a toast sent into that gap is lost.
|
||||
@@ -35,14 +33,16 @@ announce() {
|
||||
omarchy-notification-wait || return 1
|
||||
|
||||
# --exec rather than a libnotify action: the shell runs clicks from its own
|
||||
# omarchy-exec hint and never emits ActionInvoked. Keeps the default
|
||||
# "omarchy-action" app name too, the only one shouldBypassDnd() lets through.
|
||||
# hint and never emits ActionInvoked. Keeps the default "omarchy-action" app
|
||||
# name, the only one shouldBypassDnd() lets through. Crash details ride as
|
||||
# discrete argv words, so a hostile process name can't be reparsed as a
|
||||
# command. --exec consumes the rest of the line, so it comes last.
|
||||
omarchy-notification-send \
|
||||
--urgency critical \
|
||||
--glyph "$CRASH_GLYPH" \
|
||||
--exec "$exec_command" \
|
||||
"Process crashed: $comm" \
|
||||
"Click to diagnose with AI"
|
||||
"Click to diagnose with AI" \
|
||||
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
||||
}
|
||||
|
||||
# -n 0 so a restart does not re-announce crashes already dealt with.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set and launch the default coding agent
|
||||
# omarchy:args=[pi|omp|opencode|claude|codex|grok|gemini|copilot|crush]
|
||||
# omarchy:args=[pi|omp|opencode|claude|codex|grok|gemini|openclaw|hermes|copilot|crush|cursor-agent|muse]
|
||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
||||
|
||||
installing=false
|
||||
@@ -32,20 +32,51 @@ codex) agent="codex"; name="Codex" ;;
|
||||
crush) agent="crush"; name="Crush" ;;
|
||||
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
|
||||
gemini | gemini-cli) agent="gemini"; name="Gemini" ;;
|
||||
openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-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]"
|
||||
;;
|
||||
cursor | cursor-agent) agent="cursor-agent"; name="Cursor CLI" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|claude|codex|grok|gemini|copilot|crush>"
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|claude|codex|grok|gemini|openclaw|hermes|copilot|crush|cursor-agent|muse>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
agent_package=${agent_package:-$agent}
|
||||
|
||||
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then
|
||||
# Hermes reaches mise through its own installer rather than straight from
|
||||
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
|
||||
# say so. See omarchy-install-hermes-cli. OpenClaw comes from its pacman
|
||||
# package the same way; see omarchy-install-openclaw-cli.
|
||||
if [[ -n ${agent_installer:-} ]]; then
|
||||
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
|
||||
# nothing about whether Hermes is installed behind it. Treating a cold stub
|
||||
# as installed skips the floating terminal and runs the minute-long install
|
||||
# inside the menu action instead.
|
||||
agent_present() { "$agent_installer" --check; }
|
||||
agent_install() { "$agent_installer" --now; }
|
||||
else
|
||||
# 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"; }
|
||||
fi
|
||||
|
||||
if [[ $installing == "false" ]] && ! agent_present; then
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
|
||||
fi
|
||||
|
||||
if ! mise use -g "$agent_package"; then
|
||||
if ! agent_install; then
|
||||
if [[ $installing == "true" ]]; then
|
||||
echo "Could not install $name with mise" >&2
|
||||
else
|
||||
|
||||
@@ -34,8 +34,12 @@ systemd, shell, or app-launcher environment; reboot to make every layer agree.
|
||||
Affects only \$OMARCHY_PATH-resolved trees: bin/, default/, shell/,
|
||||
themes/, applications/, config/. Files installed at fixed system paths
|
||||
(/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user
|
||||
creation, /usr/share/plymouth) are NOT covered — for those, use
|
||||
omarchy-dev-pkg-test to build and install the package from the checkout.
|
||||
creation) are NOT covered — for those, use omarchy-dev-pkg-test to build
|
||||
and install the package from the checkout.
|
||||
|
||||
The Plymouth and SDDM themes under /usr/share are the exception: omarchy
|
||||
plymouth set and omarchy refresh plymouth republish them from the checkout,
|
||||
reading this link's authorization out of the root-owned /etc/omarchy.conf.
|
||||
|
||||
Also writes $sudoers_file so sudo resolves omarchy-*
|
||||
from the checkout instead of the packaged copies. That part takes effect
|
||||
|
||||
@@ -129,7 +129,8 @@ term_pt_for() {
|
||||
'BEGIN { printf "%d", int(s * p / b + 0.5) }'
|
||||
}
|
||||
|
||||
# Set the font point size in every terminal config that exists. Family is left
|
||||
# Set the font point size in terminal configs, creating Kitty overrides when
|
||||
# it inherits its size from the system config. Family is left
|
||||
# untouched — that is omarchy-font-set's job. Live-reload signals mirror
|
||||
# omarchy-font-set; foot has no reload signal, so running instances are nudged.
|
||||
set_terminal_size() {
|
||||
@@ -139,8 +140,13 @@ set_terminal_size() {
|
||||
sed -i -E "s/^size[[:space:]]*=.*/size = $pt/" ~/.config/alacritty/alacritty.toml
|
||||
fi
|
||||
|
||||
if [[ -f ~/.config/kitty/kitty.conf ]]; then
|
||||
sed -i -E "s/^font_size[[:space:]]+.*/font_size $pt.0/" ~/.config/kitty/kitty.conf
|
||||
if [[ -f ~/.config/kitty/kitty.conf ]] || omarchy-cmd-present kitty; then
|
||||
mkdir -p ~/.config/kitty
|
||||
if grep -qE '^[[:space:]]*font_size[[:space:]]+' ~/.config/kitty/kitty.conf 2>/dev/null; then
|
||||
sed --follow-symlinks -i -E "s/^[[:space:]]*font_size[[:space:]]+.*/font_size $pt.0/" ~/.config/kitty/kitty.conf
|
||||
else
|
||||
printf '\nfont_size %s.0\n' "$pt" >>~/.config/kitty/kitty.conf
|
||||
fi
|
||||
pkill -USR1 kitty 2>/dev/null || true
|
||||
fi
|
||||
|
||||
@@ -177,9 +183,13 @@ term_current_pt() {
|
||||
elif [[ -f ~/.config/alacritty/alacritty.toml ]]; then
|
||||
grep -oP '^size[[:space:]]*=[[:space:]]*\K[0-9.]+' ~/.config/alacritty/alacritty.toml | head -1
|
||||
elif [[ -f ~/.config/kitty/kitty.conf ]]; then
|
||||
grep -oP '^font_size[[:space:]]+\K[0-9.]+' ~/.config/kitty/kitty.conf | head -1
|
||||
local pt
|
||||
pt=$(grep -oP '^[[:space:]]*font_size[[:space:]]+\K[0-9.]+' ~/.config/kitty/kitty.conf | tail -1)
|
||||
echo "${pt:-$TERM_DEFAULT_PT}"
|
||||
elif [[ -f ~/.config/foot/foot.ini ]]; then
|
||||
grep -oP ':size=\K[0-9.]+' ~/.config/foot/foot.ini | head -1
|
||||
elif omarchy-cmd-present kitty; then
|
||||
echo "$TERM_DEFAULT_PT"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
+38
-7
@@ -6,6 +6,18 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Whenever this runs as root — invoked directly through the passwordless
|
||||
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
|
||||
# tool, never an omarchy-* command, so pin PATH to trusted system directories
|
||||
# and keep root from resolving one out of that checkout. The unprivileged
|
||||
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||
fi
|
||||
|
||||
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
||||
|
||||
provider_from_arg() {
|
||||
@@ -28,19 +40,32 @@ provider_from_arg() {
|
||||
esac
|
||||
}
|
||||
|
||||
self_path() {
|
||||
readlink -f "$OMARCHY_PATH/bin/${BASH_SOURCE[0]##*/}"
|
||||
# The path etc/sudoers.d/omarchy-dns names. The privileged half always runs from
|
||||
# there rather than from whichever copy was invoked, so the rule matches even
|
||||
# where $OMARCHY_PATH points at a checkout.
|
||||
PACKAGED_PATH=/usr/bin/omarchy-dns
|
||||
|
||||
# True when sudo would run this exact command without stopping for a password.
|
||||
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||
# entry's tags, so !authenticate is the grant in etc/sudoers.d/omarchy-dns and
|
||||
# nothing else. Listing runs nothing and, under -n, prompts for nothing, so a
|
||||
# machine whose omarchy-settings predates that file falls through to polkit
|
||||
# instead of dying on a password prompt it has no terminal to show.
|
||||
sudo_grants_passwordless() {
|
||||
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if (( EUID == 0 )); then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
exec sudo "$(self_path)" "$@"
|
||||
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||
# A terminal can carry sudo's own password prompt. Without one, sudo is
|
||||
# right only where the grant reaches; polkit can at least put a prompt on
|
||||
# screen, and offer to authenticate as someone else.
|
||||
exec sudo "$PACKAGED_PATH" "$@"
|
||||
else
|
||||
exec pkexec "$(self_path)" "$@"
|
||||
exec pkexec "$PACKAGED_PATH" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -124,6 +149,9 @@ write_networkmanager_dns() {
|
||||
local servers="$1"
|
||||
|
||||
install -d -m 0755 "$(dirname "$NM_DNS_CONF")"
|
||||
# omarchy:heredoc-expands paths=none -- $servers is a normalized, single-line
|
||||
# DNS server list written as data, not a path or command; nothing user-writable
|
||||
# is resolved or executed from the root-owned drop-in.
|
||||
cat >"$NM_DNS_CONF" <<EOF
|
||||
# Managed by omarchy-dns. Remove this file or run omarchy dns DHCP to use DHCP DNS again.
|
||||
[global-dns]
|
||||
@@ -277,6 +305,9 @@ Custom)
|
||||
split_dns_servers "$dns_servers"
|
||||
write_networkmanager_dns "$dns_servers"
|
||||
set_connection_dns "$ipv4_dns" "$ipv6_dns"
|
||||
# omarchy:heredoc-expands paths=none -- $dns_servers is a normalized,
|
||||
# single-line DNS server list; the //,/ turns its comma separators into the
|
||||
# spaces resolved.conf wants. It is written as data, not a path or command.
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<EOF
|
||||
[Resolve]
|
||||
DNS=${dns_servers//,/ }
|
||||
|
||||
@@ -30,8 +30,14 @@ if [[ -f ~/.config/alacritty/alacritty.toml ]]; then
|
||||
sed -i "s/family = \".*\"/family = \"$font_name\"/g" ~/.config/alacritty/alacritty.toml
|
||||
fi
|
||||
|
||||
if [[ -f ~/.config/kitty/kitty.conf ]]; then
|
||||
sed -i "s/^font_family .*/font_family $font_name/g" ~/.config/kitty/kitty.conf
|
||||
if [[ -f ~/.config/kitty/kitty.conf ]] || omarchy-cmd-present kitty; then
|
||||
mkdir -p ~/.config/kitty
|
||||
if grep -qE '^[[:space:]]*font_family[[:space:]]+' ~/.config/kitty/kitty.conf 2>/dev/null; then
|
||||
kitty_font_name=$(printf '%s' "$font_name" | sed 's/[\\&/]/\\&/g')
|
||||
sed --follow-symlinks -i -E "s/^[[:space:]]*font_family[[:space:]]+.*/font_family $kitty_font_name/" ~/.config/kitty/kitty.conf
|
||||
else
|
||||
printf '\nfont_family %s\n' "$font_name" >>~/.config/kitty/kitty.conf
|
||||
fi
|
||||
pkill -USR1 kitty
|
||||
fi
|
||||
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Check that a git URL names a repository, not a transport helper
|
||||
# omarchy:args=<git-url>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
|
||||
# in exactly two shapes, and no others: `<helper>::<address>`, and
|
||||
# `<scheme>://<address>` for any scheme git does not handle itself. A single
|
||||
# colon is always scp-style ssh, and a bare path is always a path; neither can
|
||||
# reach a helper. So constraining those two shapes covers the whole surface.
|
||||
#
|
||||
# The `::` shape is refused outright, because no helper reachable that way is one
|
||||
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
|
||||
# The `://` shape cannot be refused the same way, since it is also how every
|
||||
# legitimate URL arrives -- so it is allowlisted instead. The list is the
|
||||
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
|
||||
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
|
||||
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
|
||||
# command the URL carries.
|
||||
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
|
||||
|
||||
fail() {
|
||||
echo "omarchy-git-url-check: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
url="${1-}"
|
||||
|
||||
if [[ -z $url ]]; then
|
||||
fail "a git URL is required"
|
||||
fi
|
||||
|
||||
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
fail "'$url' names a git option or transport helper, not a repository."
|
||||
fi
|
||||
|
||||
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
|
||||
scheme="${BASH_REMATCH[1]}"
|
||||
|
||||
for transport in "${TRANSPORTS[@]}"; do
|
||||
if [[ $scheme == "$transport" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
|
||||
fi
|
||||
@@ -30,6 +30,35 @@ MKINITCPIO_CONF="/etc/mkinitcpio.conf.d/omarchy_resume.conf"
|
||||
SWAP_FILE="/swap/swapfile"
|
||||
RESUME_DROP_IN="/etc/limine-entry-tool.d/resume.conf"
|
||||
|
||||
install_root_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="$3"
|
||||
local stage
|
||||
|
||||
stage=$(sudo /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1
|
||||
safe_stage_path "$stage" "$destination" || return 1
|
||||
|
||||
if sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" &&
|
||||
sudo /usr/bin/mv -Tf -- "$stage" "$destination"; then
|
||||
return 0
|
||||
else
|
||||
safe_stage_path "$stage" "$destination" && sudo /usr/bin/rm -f -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
safe_stage_path() {
|
||||
local stage="$1"
|
||||
local destination="$2"
|
||||
local prefix suffix
|
||||
|
||||
prefix="${destination%/*}/.${destination##*/}.omarchy."
|
||||
[[ $stage == "$prefix"* ]] || return 1
|
||||
suffix=${stage#"$prefix"}
|
||||
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||
}
|
||||
|
||||
# Check if hibernation is already configured
|
||||
if [[ -f $MKINITCPIO_CONF ]] && grep -q "^HOOKS+=(resume)$" "$MKINITCPIO_CONF"; then
|
||||
# Fix empty resume_offset if btrfs map-swapfile failed during initial setup
|
||||
@@ -83,14 +112,20 @@ if ! swapon --show | grep -q "$SWAP_FILE"; then
|
||||
sudo swapon -p 0 "$SWAP_FILE"
|
||||
fi
|
||||
|
||||
# Ensure keyboard backlight doesn't prevent sleep
|
||||
# Install this before writing the resume marker so a failed install remains
|
||||
# retryable through the normal setup command.
|
||||
if ! install_root_file "$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight" \
|
||||
/usr/lib/systemd/system-sleep/keyboard-backlight 0755; then
|
||||
echo "Could not install the keyboard-backlight system-sleep hook" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add resume hook to mkinitcpio
|
||||
sudo mkdir -p /etc/mkinitcpio.conf.d
|
||||
echo "Adding resume hook to $MKINITCPIO_CONF"
|
||||
echo "HOOKS+=(resume)" | sudo tee "$MKINITCPIO_CONF" >/dev/null
|
||||
|
||||
# Ensure keyboard backlight doesn't prevent sleep
|
||||
sudo cp -p "$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight" /usr/lib/systemd/system-sleep/
|
||||
|
||||
# Add resume= kernel parameters so the initramfs resume hook knows where to find the
|
||||
# hibernation image. Without these, resume happens late (after GPU drivers load) and fails.
|
||||
if [[ ! -f $RESUME_DROP_IN ]]; then
|
||||
|
||||
@@ -11,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
|
||||
|
||||
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
||||
|
||||
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
|
||||
# name that is not a plain connector string could execute on the next reload.
|
||||
# Names come from hyprctl; a user-created headless output can carry anything.
|
||||
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Refusing unsafe internal monitor name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
valid_scale() {
|
||||
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
||||
}
|
||||
@@ -118,9 +126,17 @@ remember_internal_scale() {
|
||||
store_internal_scale "$scale"
|
||||
}
|
||||
|
||||
# $1 is the configured scale when the caller has already read it, so one sync
|
||||
# does not parse the config twice.
|
||||
read_monitor_scale() {
|
||||
local scale
|
||||
scale=$(configured_monitor_scale)
|
||||
|
||||
if (( $# )); then
|
||||
scale="$1"
|
||||
else
|
||||
scale=$(configured_monitor_scale)
|
||||
fi
|
||||
|
||||
if valid_scale "$scale"; then
|
||||
echo "$scale"
|
||||
return
|
||||
@@ -159,11 +175,25 @@ enable_internal_output() {
|
||||
|
||||
sync_internal_scale() {
|
||||
[[ -n $INTERNAL ]] || return 0
|
||||
local configured_scale
|
||||
local desired_scale
|
||||
local active_scale
|
||||
|
||||
desired_scale=$(read_monitor_scale)
|
||||
configured_scale=$(configured_monitor_scale)
|
||||
active_scale=$(current_internal_scale)
|
||||
|
||||
# A config without a usable number -- the default "auto", or an expression
|
||||
# only Hyprland's Lua can evaluate -- delegates the scale to the compositor,
|
||||
# so whatever it resolved for the enabled panel IS the configured scale.
|
||||
# There is no number to correct it toward: substituting one makes the scale
|
||||
# flap between that number and the compositor's own value on every idle-wake.
|
||||
# Only a panel that is off altogether still gets a hand below, from the
|
||||
# remembered scale.
|
||||
if ! valid_scale "$configured_scale" && valid_scale "$active_scale"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
desired_scale=$(read_monitor_scale "$configured_scale")
|
||||
scales_match "$active_scale" "$desired_scale" && return 0
|
||||
|
||||
enable_internal_output "$desired_scale"
|
||||
|
||||
@@ -28,6 +28,13 @@ off() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The name is written into generated Lua below, so only a plain connector
|
||||
# name may pass; anything else could execute on the next reload.
|
||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! omarchy-hyprland-monitor-external-active; then
|
||||
omarchy-notification-send -g "Can't disable the only active display"
|
||||
exit 1
|
||||
|
||||
@@ -22,6 +22,15 @@ on() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Both names are written into generated Lua below, so only plain connector
|
||||
# names may pass; a user-created headless output can carry any name.
|
||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||
|
||||
@@ -80,6 +80,14 @@ set_scale() {
|
||||
local width="$(echo "$monitor_info" | jq -r '.width')"
|
||||
local height="$(echo "$monitor_info" | jq -r '.height')"
|
||||
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
||||
|
||||
# active_monitor is written into the Lua string eval'd below, so only a plain
|
||||
# connector name may pass; a hostile output name could execute otherwise.
|
||||
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Refusing unsafe monitor name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
||||
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
||||
# factor even when the monitor scale itself is fractional.
|
||||
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Hermes desktop app
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
echo "Run this command as your desktop user, without sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Hermes Desktop..."
|
||||
omarchy-pkg-add hermes-desktop
|
||||
|
||||
if [[ ! -r /usr/share/hermes-desktop/install.sh || ! -r /usr/share/hermes-desktop/runtime.patch ]] ||
|
||||
! release_commit=$(jq -er 'select(.branch == "main") | .commit | select(test("^[0-9a-f]{40}$"))' /opt/hermes-desktop/resources/install-stamp.json 2>/dev/null); then
|
||||
echo "The installed Hermes package cannot prepare in-app updates. Run 'omarchy update', then try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If Hermes was already installed for the terminal, the app supersedes it: one
|
||||
# machine, one Hermes. This drops that copy so the terminal, the default agent
|
||||
# and the app all end up on the app's installation.
|
||||
omarchy-install-hermes-cli || true
|
||||
|
||||
# Keep the runtime at the root even when invoked from a Hermes profile.
|
||||
HERMES_HOME=$(realpath -ms -- "${HERMES_HOME:-$HOME/.hermes}")
|
||||
home_parent=$(dirname -- "$HERMES_HOME")
|
||||
if [[ ${home_parent##*/} == [Pp][Rr][Oo][Ff][Ii][Ll][Ee][Ss] ]]; then
|
||||
HERMES_HOME=$(dirname -- "$home_parent")
|
||||
fi
|
||||
export HERMES_HOME
|
||||
|
||||
runtime="$HERMES_HOME/hermes-agent"
|
||||
native_app="$runtime/apps/desktop/release/linux-unpacked"
|
||||
|
||||
runtime_ready() {
|
||||
[[ -f $runtime/.hermes-bootstrap-complete && -f $runtime/venv/bin/hermes && -x $runtime/venv/bin/hermes && -f $runtime/venv/bin/python && -x $runtime/venv/bin/python ]] &&
|
||||
timeout 15 "$runtime/venv/bin/hermes" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_main() {
|
||||
local main_commit
|
||||
main_commit=$(git -C "$runtime" rev-parse --verify refs/heads/main 2>/dev/null || true)
|
||||
if [[ -n $main_commit && $main_commit != "$release_commit" && $main_commit != "$(git -C "$runtime" rev-parse --verify refs/remotes/origin/main 2>/dev/null)" ]]; then
|
||||
echo "Hermes main has local commits. Keep that work and prepare the desktop with 'hermes desktop --build-only'." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if ! runtime_ready; then
|
||||
# The upstream installer can reset an existing checkout. Do not pin a newer
|
||||
# or modified runtime back to the package release while repairing setup.
|
||||
if [[ -e $runtime || -L $runtime ]]; then
|
||||
if [[ $(git -C "$runtime" rev-parse HEAD 2>/dev/null) != "$release_commit" ]] ||
|
||||
[[ -n $(git -C "$runtime" status --porcelain --untracked-files=all) ]]; then
|
||||
echo "Hermes setup is incomplete at $runtime. Repair that installation before trying again; existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
check_main
|
||||
fi
|
||||
|
||||
# Upstream replaces these commands, including foreign files and symlinks.
|
||||
# Keep their original bytes/links before handing the names to the desktop.
|
||||
command_backup=""
|
||||
for command in hermes hermes-agent hermes-acp; do
|
||||
command_path="$HOME/.local/bin/$command"
|
||||
if [[ -e $command_path || -L $command_path ]]; then
|
||||
if [[ ! -f $command_path && ! -L $command_path ]]; then
|
||||
echo "Cannot replace $command_path: move it aside before installing Hermes Desktop." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z $command_backup ]]; then
|
||||
command_backup=$(mktemp -d "$HOME/.local/bin/.hermes-before-desktop.XXXXXX")
|
||||
echo "Saving existing Hermes commands in $command_backup"
|
||||
fi
|
||||
cp -a -- "$command_path" "$command_backup/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Setting up the Hermes runtime..."
|
||||
bash /usr/share/hermes-desktop/install.sh --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$HERMES_HOME"
|
||||
if ! runtime_ready; then
|
||||
echo "Hermes runtime setup did not complete. Re-run this command after resolving the installer error." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
runtime_commit=$(git -C "$runtime" rev-parse HEAD)
|
||||
if [[ $runtime_commit == "$release_commit" ]]; then
|
||||
# The updater switches to main before checking for changes. Start main at
|
||||
# the packaged release, with enough history for its first fast-forward.
|
||||
check_main
|
||||
if [[ $(git -C "$runtime" rev-parse --is-shallow-repository) == "true" ]]; then
|
||||
git -C "$runtime" fetch --unshallow origin main
|
||||
fi
|
||||
git -C "$runtime" switch -C main "$release_commit"
|
||||
|
||||
if git -C "$runtime" apply --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
||||
git -C "$runtime" apply /usr/share/hermes-desktop/runtime.patch
|
||||
elif ! git -C "$runtime" apply --reverse --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then
|
||||
echo "The Hermes Linux runtime patch conflicts with local changes. Existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e $native_app || -L $native_app ]]; then
|
||||
if [[ ! -f $native_app/Hermes || ! -x $native_app/Hermes || ! -f $native_app/resources/app.asar || ! -f $native_app/resources/install-stamp.json ]]; then
|
||||
echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [[ $runtime_commit != "$release_commit" ]]; then
|
||||
echo "The Hermes runtime has moved beyond the packaged desktop release. Run 'hermes desktop --build-only', then try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
desktop_changes=$(git -C "$runtime" status --porcelain --untracked-files=all -- apps/desktop package.json package-lock.json)
|
||||
if [[ -n $desktop_changes ]]; then
|
||||
echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p -- "${native_app%/*}"
|
||||
staging=$(mktemp -d "${native_app%/*}/.linux-unpacked.XXXXXX")
|
||||
trap 'rm -rf -- "$staging"' EXIT
|
||||
cp -a /opt/hermes-desktop/. "$staging/"
|
||||
chmod 0755 "$staging/chrome-sandbox"
|
||||
mv -T --no-clobber -- "$staging" "$native_app"
|
||||
if [[ -e $staging ]]; then
|
||||
echo "A Hermes desktop app appeared during setup. It has been kept; please try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
trap - EXIT
|
||||
|
||||
# Record this matching prebuilt app using the CLI's own content hash, so
|
||||
# subsequent menu launches do not rebuild an app that is already current.
|
||||
env -u PYTHONPATH -u PYTHONHOME "$runtime/venv/bin/python" - "$runtime" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
from hermes_cli.main import _write_desktop_build_stamp
|
||||
|
||||
_write_desktop_build_stamp(Path(sys.argv[1]), source_mode=False)
|
||||
PY
|
||||
fi
|
||||
|
||||
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; 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."
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the OpenClaw agent platform and its Control UI web app
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
|
||||
# The desktop app is OpenClaw's Control UI: a web app served by its own
|
||||
# gateway. The launcher entry goes through omarchy-launch-openclaw, which
|
||||
# onboards or starts that gateway before opening the window. The icon ships
|
||||
# inside the package, so nothing is fetched here.
|
||||
echo "Installing the OpenClaw web app..."
|
||||
omarchy-webapp-install OpenClaw "http://127.0.0.1:18789" \
|
||||
/usr/lib/node_modules/openclaw/dist/control-ui/apple-touch-icon.png \
|
||||
omarchy-launch-openclaw
|
||||
|
||||
# A first install runs onboarding right here: launching the app instead would
|
||||
# open a second floating terminal for the wizard, identical to this one.
|
||||
# omarchy-openclaw-onboard runs the wizard the way this flow needs (terminal
|
||||
# prompts, gateway as a user service, and it actually returns). A machine
|
||||
# that is already onboarded goes straight to the app.
|
||||
if [[ -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
echo "Opening OpenClaw..."
|
||||
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
|
||||
elif omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
echo "Opening OpenClaw..."
|
||||
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "OpenClaw has been installed."
|
||||
echo "If you skipped onboarding, launching OpenClaw from the app grid resumes it."
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install T3 Code and point it at the Omarchy palette
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
T3CODE_HOME="${T3CODE_HOME:-$HOME/.t3}"
|
||||
T3CODE_STATE_DIR="$T3CODE_HOME/userdata"
|
||||
|
||||
echo "Installing T3 Code..."
|
||||
omarchy-pkg-add t3code-bin
|
||||
|
||||
echo "Matching T3 Code to the current theme..."
|
||||
mkdir -p "$T3CODE_STATE_DIR"
|
||||
|
||||
# An updated user's current theme may have been staged before this template existed.
|
||||
if [[ ! -f $HOME/.local/state/omarchy/current/theme/t3code.json ]]; then
|
||||
omarchy-theme-refresh
|
||||
fi
|
||||
|
||||
omarchy-theme-set-t3code
|
||||
|
||||
# The packaged CLI selects the published palette before the app's first launch.
|
||||
t3 theme set omarchy --base-dir "$T3CODE_HOME"
|
||||
|
||||
echo "Opening T3 Code..."
|
||||
setsid uwsm-app -- gtk-launch t3code >/dev/null 2>&1 &
|
||||
|
||||
echo ""
|
||||
echo "T3 Code has been installed."
|
||||
@@ -16,6 +16,12 @@ fi
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
printf -v desktop_id_arg '%q' "$desktop_id"
|
||||
|
||||
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
|
||||
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
|
||||
read -r -d '' -a package_list <<<"$packages" || true
|
||||
printf -v packages_arg '%q ' "${package_list[@]}"
|
||||
packages_arg="${packages_arg% }"
|
||||
|
||||
# The subshell keeps & from backgrounding the package installation too.
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages_arg} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
+10
-1
@@ -12,4 +12,13 @@ if [[ -z $name || -z $packages ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation "echo 'Installing ${name}...'; omarchy-pkg-add ${packages}"
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
|
||||
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
|
||||
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
|
||||
read -r -d '' -a package_list <<<"$packages" || true
|
||||
printf -v packages_arg '%q ' "${package_list[@]}"
|
||||
packages_arg="${packages_arg% }"
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages_arg}"
|
||||
+10
-16
@@ -4,9 +4,10 @@
|
||||
# omarchy:args=<chrome|brave|brave-origin|edge|firefox|zen>
|
||||
# omarchy:examples=omarchy install browser firefox | omarchy install browser brave
|
||||
|
||||
setup_policy_directory() {
|
||||
sudo mkdir -p "$1"
|
||||
sudo chmod a+rw "$1"
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
setup_chromium_policy_directory() {
|
||||
browser_policy_setup_dir "$1"
|
||||
}
|
||||
|
||||
announce_browser_installed() {
|
||||
@@ -21,13 +22,6 @@ copy_chromium_flags() {
|
||||
omarchy-install-chromium-ytdlp
|
||||
}
|
||||
|
||||
setup_firefox_preferences() {
|
||||
local distribution_dir="$1"
|
||||
|
||||
setup_policy_directory "$distribution_dir"
|
||||
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
|
||||
}
|
||||
|
||||
setup_firefox_wayland() {
|
||||
mkdir -p ~/.config/environment.d
|
||||
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
|
||||
@@ -38,7 +32,7 @@ chrome)
|
||||
echo "Installing Chrome..."
|
||||
omarchy-pkg-aur-add google-chrome || exit 1
|
||||
|
||||
setup_policy_directory /etc/opt/chrome/policies/managed
|
||||
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
|
||||
copy_chromium_flags ~/.config/chrome-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Chrome"
|
||||
@@ -47,7 +41,7 @@ edge)
|
||||
echo "Installing Edge..."
|
||||
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/opt/edge/policies/managed
|
||||
setup_chromium_policy_directory /etc/opt/edge/policies/managed
|
||||
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Edge"
|
||||
@@ -56,7 +50,7 @@ brave)
|
||||
echo "Installing Brave..."
|
||||
omarchy-pkg-aur-add brave-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/brave/policies/managed
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave"
|
||||
@@ -65,7 +59,7 @@ brave-origin)
|
||||
echo "Installing Brave Origin..."
|
||||
omarchy-pkg-aur-add brave-origin-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/brave/policies/managed
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-origin-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave Origin"
|
||||
@@ -74,7 +68,7 @@ firefox)
|
||||
echo "Installing Firefox..."
|
||||
omarchy-pkg-add firefox || exit 1
|
||||
|
||||
setup_firefox_preferences /usr/lib/firefox/distribution
|
||||
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
|
||||
setup_firefox_wayland
|
||||
announce_browser_installed "Firefox"
|
||||
;;
|
||||
@@ -82,7 +76,7 @@ zen)
|
||||
echo "Installing Zen..."
|
||||
omarchy-pkg-aur-add zen-browser-bin || exit 1
|
||||
|
||||
setup_firefox_preferences /opt/zen-browser/distribution
|
||||
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
|
||||
setup_firefox_wayland
|
||||
announce_browser_installed "Zen"
|
||||
;;
|
||||
|
||||
@@ -13,5 +13,9 @@ if [[ -z $name || -z $package || -z $family ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
printf -v package_arg '%q' "$package"
|
||||
printf -v family_arg '%q' "$family"
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo 'Installing ${name}...'; omarchy-pkg-add ${package} && sleep 2 && omarchy-font-set '${family}'"
|
||||
"echo ${install_message}; omarchy-pkg-add ${package_arg} && sleep 2 && omarchy-font-set ${family_arg}"
|
||||
Executable
+280
@@ -0,0 +1,280 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin
|
||||
# omarchy:args=[--check|--now|--owns|--remove]
|
||||
# omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now
|
||||
|
||||
# Hermes pins every one of its dependencies exactly and declares
|
||||
# Requires-Python >=3.11,<3.14, so it can neither be built against Arch's
|
||||
# Python nor share the python-* packages. mise builds it a private environment
|
||||
# instead.
|
||||
#
|
||||
# It gets its own installer rather than a line in omarchy-mise-install because
|
||||
# of the interpreter pin. Given no compatible interpreter to hand, uv builds
|
||||
# the venv against the system Python in violation of Hermes' own bound,
|
||||
# reports success, and leaves the breakage to surface later inside a
|
||||
# dependency -- and omarchy-mise-install writes a fixed stub with nowhere to
|
||||
# say otherwise.
|
||||
#
|
||||
# There is only ever one Hermes on a machine. hermes-desktop cannot run against
|
||||
# this one -- it needs a runtime built from its own commit, and the version gap
|
||||
# fails its readiness probe -- so it installs its own under ~/.hermes and puts
|
||||
# that on PATH. When the package is present it therefore owns Hermes outright:
|
||||
# this installer stands aside and removes its own copy, so the terminal, the
|
||||
# default agent and the app are all the same installation.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
mode=${1:-}
|
||||
|
||||
tool='pipx:hermes-agent[extras=all]'
|
||||
python='3.13'
|
||||
|
||||
# The line that identifies the stub as this installer's; matched whole, so a
|
||||
# wrapper that merely mentions the command is not mistaken for ours.
|
||||
marker='# Written by omarchy-install-hermes-cli.'
|
||||
|
||||
# The package, not the runtime directory: it is installed before the app has
|
||||
# ever run, and that is exactly when we must not start building a second copy.
|
||||
desktop_owns_hermes() {
|
||||
omarchy-pkg-present hermes-desktop
|
||||
}
|
||||
|
||||
# The venv appears at the python-deps stage, several stages before the one that
|
||||
# installs the command, so its presence says nothing about being usable. The
|
||||
# marker is written last, and the command is what the agent actually runs.
|
||||
desktop_hermes_ready() {
|
||||
[[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]] || return 1
|
||||
|
||||
# An executable of that name proves nothing about whose it is; the app's own
|
||||
# points into ~/.hermes, and anything else is not the install we are asking
|
||||
# about. Matched as a plain string, because the path carries a dot and an
|
||||
# unanchored pattern would also claim a wrapper pointing at ~/xhermes.
|
||||
[[ -f $HOME/.local/bin/hermes ]] || return 1
|
||||
grep -qF "$HOME/.hermes" "$HOME/.local/bin/hermes" || return 1
|
||||
|
||||
# And a marker left behind by an install whose venv has since gone answers
|
||||
# for nothing, so the command has to run, exactly as a foreign one must.
|
||||
hermes_prompt_ready
|
||||
}
|
||||
|
||||
# Whether Hermes is really installed, not merely whether the stub exists. A
|
||||
# stub on its own is cold: running it installs Hermes, which takes minutes.
|
||||
installed() {
|
||||
[[ -d "$(mise where "$tool" 2>/dev/null)/hermes-agent/lib/python$python" ]]
|
||||
}
|
||||
|
||||
# The stub is the only thing this installer owns. Anything else at that path
|
||||
# -- Hermes' official installer, a hand-rolled wrapper, even a dangling link
|
||||
# -- was put there by the user and is never deleted or overwritten here.
|
||||
# Symlinks count as foreign even when they resolve to a marked file: the stub
|
||||
# is written as a regular file, so a link is someone else's arrangement.
|
||||
ours() {
|
||||
[[ -f $HOME/.local/bin/hermes && ! -L $HOME/.local/bin/hermes ]] &&
|
||||
grep -qxF "$marker" "$HOME/.local/bin/hermes"
|
||||
}
|
||||
|
||||
foreign_hermes() {
|
||||
[[ -e $HOME/.local/bin/hermes || -L $HOME/.local/bin/hermes ]] && ! ours
|
||||
}
|
||||
|
||||
# A hermes at that path is usable when it is a command that runs: a regular
|
||||
# executable whose --version answers. The executable bit alone proves little -- a directory
|
||||
# passes -x on search permission, and a wrapper whose interpreter or target is
|
||||
# gone passes it too. The desktop app applies the same probe with the same 15
|
||||
# second budget, so what passes here is what it will use.
|
||||
hermes_runs() {
|
||||
[[ -f $HOME/.local/bin/hermes && -x $HOME/.local/bin/hermes ]] &&
|
||||
timeout 15 "$HOME/.local/bin/hermes" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A flag counts only when the help defines it, not whenever it is mentioned:
|
||||
# what follows must be a shape argparse prints after a definition -- the usage
|
||||
# line's closing bracket, the gap before same-line help text, an uppercase
|
||||
# metavar, or the end of the line. Prose like "With --tui: run ..." stays
|
||||
# prose, and --tui-theme or --tui_mode never answers for --tui. Not probed by
|
||||
# parsing an actual invocation on purpose: a release that ignores unknown
|
||||
# arguments would turn the probe into a live session.
|
||||
help_defines_flag() {
|
||||
grep -qE -- "$1(]|[[:space:]][[:upper:]]|[[:space:]]{2}|$)" <<<"$2"
|
||||
}
|
||||
|
||||
# Probed for the flags omarchy-agent actually passes -- --query to seed the
|
||||
# session and --tui to keep it interactive -- rather than a marker standing in
|
||||
# for them: the old probe keyed on chat carrying --oneshot, which no released
|
||||
# Hermes did (it lived at the top level until v0.21 added chat's own), so
|
||||
# every release read as "not ready".
|
||||
hermes_prompt_ready() {
|
||||
local help
|
||||
hermes_runs &&
|
||||
help=$(timeout 15 "$HOME/.local/bin/hermes" chat --help 2>/dev/null) &&
|
||||
help_defines_flag '--tui' "$help" &&
|
||||
help_defines_flag '--query' "$help"
|
||||
}
|
||||
|
||||
# --owns answers whether the wrapper on PATH is the one this command wrote, so
|
||||
# the migration and Remove Preinstalls do not each carry their own copy of the
|
||||
# marker and drift from it.
|
||||
if [[ $mode == "--owns" ]]; then
|
||||
if ours; then exit 0; else exit 1; fi
|
||||
fi
|
||||
|
||||
# --remove tears down a Hermes CLI this installer put in place -- the mise tool
|
||||
# it installed and the stub it marked -- so Remove Hermes clears a CLI the app
|
||||
# never superseded (an interrupted install, or the terminal CLI from before the
|
||||
# app existed) rather than leaving it stranded on PATH. The whole teardown
|
||||
# turns on the marked stub, exactly as replacement does further down: without
|
||||
# it nothing proves the mise environment is Omarchy's rather than one the user
|
||||
# built against the same spec, and a user's stays theirs. The desktop takeover
|
||||
# removes the environment without asking, but that is its own bargain -- a
|
||||
# second Hermes has to go whoever built it, and the app still provides the
|
||||
# command afterwards; here nothing would. Idempotent: nothing owned, nothing
|
||||
# to do.
|
||||
if [[ $mode == "--remove" ]]; then
|
||||
if ours; then
|
||||
mise rm -g "$tool" >/dev/null 2>&1 || true
|
||||
mise uninstall --all "$tool" >/dev/null 2>&1 || true
|
||||
rm -f "$HOME/.local/bin/hermes" 2>/dev/null || true
|
||||
|
||||
# Every step is attempted before any is judged, and judged by what is left
|
||||
# rather than by what the commands claimed: the marked stub still answering
|
||||
# hermes, or mise still resolving the tool, is a CLI still installed no
|
||||
# matter how the removal exited.
|
||||
# Not "run --remove again": once the stub is gone nothing marks the mise
|
||||
# environment as ours, so a rerun would find nothing it owns and succeed
|
||||
# without touching what was left. Only the full commands finish the job.
|
||||
if ours || mise where "$tool" >/dev/null 2>&1; then
|
||||
echo "Could not remove the Hermes CLI Omarchy installed. Finish by hand:" >&2
|
||||
echo " rm -f ~/.local/bin/hermes" >&2
|
||||
echo " mise rm -g '$tool'" >&2
|
||||
echo " mise uninstall --all '$tool'" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --check lets callers tell a cold stub from a working one before they commit
|
||||
# to a path that assumes Hermes is ready.
|
||||
if [[ $mode == "--check" ]]; then
|
||||
if desktop_owns_hermes; then
|
||||
if desktop_hermes_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
# A foreign command is ready only when it also supports prompted sessions;
|
||||
# since it is not ours to replace, nothing this installer does will update it.
|
||||
if foreign_hermes; then
|
||||
if hermes_prompt_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
if installed && hermes_prompt_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
|
||||
# Hand Hermes over to the app rather than keeping a second copy beside it.
|
||||
if desktop_owns_hermes; then
|
||||
# Not gated on that copy being healthy: `mise up` can rebuild it against the
|
||||
# wrong interpreter and a half-finished install answers to neither test, and
|
||||
# either way it is still a second Hermes. Removing nothing is harmless.
|
||||
if mise where "$tool" >/dev/null 2>&1; then
|
||||
echo "Hermes Desktop provides Hermes; removing the separate CLI install..." >&2
|
||||
fi
|
||||
|
||||
mise rm -g "$tool" >/dev/null 2>&1 || true
|
||||
mise uninstall --all "$tool" >/dev/null 2>&1 || true
|
||||
|
||||
# Our own stub has to go with it. Left in place it still answers `hermes`
|
||||
# until the app's bootstrap overwrites it, and answering means building the
|
||||
# second Hermes this whole arrangement exists to avoid.
|
||||
if ours; then
|
||||
rm -f "$HOME/.local/bin/hermes"
|
||||
fi
|
||||
|
||||
if desktop_hermes_ready; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Hermes Desktop is installed but has not set Hermes up yet." >&2
|
||||
echo "Launch Hermes Desktop once to finish installing it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The user already has a hermes of their own. Leave it be: a working one is
|
||||
# what the default agent will run, and a broken one is theirs to fix.
|
||||
if foreign_hermes; then
|
||||
if hermes_prompt_ready; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if hermes_runs; then
|
||||
echo "~/.local/bin/hermes does not support the interactive seeded sessions Omarchy needs." >&2
|
||||
echo "Update it to a Hermes Agent release with interactive chat queries, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "~/.local/bin/hermes exists but is not runnable, and it was not installed by Omarchy." >&2
|
||||
echo "Fix or remove it, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only the marked wrapper proves the matching mise environment is ours to replace.
|
||||
if installed && ! hermes_prompt_ready; then
|
||||
if ours; then
|
||||
echo "Updating Hermes for prompted sessions..." >&2
|
||||
mise rm -g "$tool" >/dev/null 2>&1 || true
|
||||
mise uninstall --all "$tool" >/dev/null 2>&1 || true
|
||||
else
|
||||
echo "A Hermes mise environment exists without an Omarchy-owned wrapper." >&2
|
||||
echo "Update or remove it explicitly, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
rm -f "$HOME/.local/bin/hermes"
|
||||
|
||||
cat >"$HOME/.local/bin/hermes" <<EOF
|
||||
#!/bin/bash
|
||||
|
||||
$marker
|
||||
|
||||
export UV_PYTHON="$python"
|
||||
|
||||
# Exported rather than set on the install line alone, so the version resolved
|
||||
# to run agrees with the one just installed. Hermes ships several times a week
|
||||
# and mise's cooldown would otherwise hold a new release back for days.
|
||||
export MISE_MINIMUM_RELEASE_AGE=0
|
||||
|
||||
# mise up -- which omarchy update runs -- reinstalls without that pin, so this
|
||||
# asks which interpreter is actually there rather than whether anything is.
|
||||
if ! [[ -d "\$(mise where '$tool' 2>/dev/null)/hermes-agent/lib/python$python" ]]; then
|
||||
echo "Installing Hermes on Python $python (this takes a minute)..." >&2
|
||||
|
||||
# mise's pipx backend shells out to uv, which a stock Omarchy does not have.
|
||||
# It is fetched here rather than when this stub was written, so setting up a
|
||||
# machine that never runs Hermes costs nothing.
|
||||
if omarchy-cmd-missing uv && ! mise where uv >/dev/null 2>&1; then
|
||||
mise use -g --quiet uv@latest || exit 1
|
||||
fi
|
||||
|
||||
mise use -g --quiet --force '$tool' || exit 1
|
||||
fi
|
||||
|
||||
# The pin belongs to building Hermes, not to everything Hermes then runs.
|
||||
# Exported it would reach the agent and every command it shells out to, so a
|
||||
# uv in the user's own project would resolve 3.13 there too -- uv only warns
|
||||
# when that contradicts the project's requires-python, and builds it anyway.
|
||||
exec env -u UV_PYTHON mise x '$tool' -- hermes "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$HOME/.local/bin/hermes"
|
||||
|
||||
# The desktop app resolves a hermes on PATH by running `hermes --version` with
|
||||
# a 15 second budget, then falls back to cloning its own copy when that times
|
||||
# out. A first-run mise install does not fit in 15 seconds, so anything that
|
||||
# hands Hermes to the GUI has to install it here rather than leave it stubbed.
|
||||
if [[ $mode == "--now" ]]; then
|
||||
"$HOME/.local/bin/hermes" --version
|
||||
if ! hermes_prompt_ready; then
|
||||
echo "Hermes installed without the interactive seeded sessions Omarchy needs." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Ensure the OpenClaw CLI is installed for the default agent
|
||||
# omarchy:args=[--check|--now]
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# OpenClaw is not a mise tool: the openclaw pacman package is the one OpenClaw
|
||||
# installation on the machine — the CLI, the gateway, and the Install > AI web
|
||||
# app all share it, and the fast ring keeps it current. The default-agent flow
|
||||
# talks to that package through the same --check/--now contract mise-backed
|
||||
# agents get from mise itself.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
case "${1:---now}" in
|
||||
--check)
|
||||
omarchy-pkg-present openclaw
|
||||
;;
|
||||
--now)
|
||||
if ! omarchy-pkg-present openclaw; then
|
||||
echo "Installing OpenClaw..."
|
||||
omarchy-pkg-add openclaw
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-install-openclaw-cli [--check|--now]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
set -e
|
||||
|
||||
# 1Password reads the display scale itself, the way Electron apps do, and comes
|
||||
# up oversized next to every other window on a scaled monitor. The packaged
|
||||
# .desktop pins it too; this covers the hotkey, which runs the binary directly.
|
||||
if omarchy-cmd-present 1password; then
|
||||
exec setsid uwsm-app -- 1password
|
||||
exec setsid uwsm-app -- 1password --force-device-scale-factor=1
|
||||
else
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password
|
||||
fi
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open the Docker TUI (lazydocker) with access to the Docker daemon
|
||||
# omarchy:hidden=true
|
||||
|
||||
# By default the install user is NOT in the docker group: membership is
|
||||
# root-equivalent (a container can bind-mount / and rewrite the host as root),
|
||||
# so a single process running as the user could otherwise escalate to root with
|
||||
# no prompt. lazydocker needs the root-owned Docker socket, so when the group is
|
||||
# absent, gate that access behind a polkit prompt. If the user has opted into
|
||||
# sudoless Docker (omarchy-setup-security-sudoless-docker), the socket is already
|
||||
# reachable, so run lazydocker directly — omarchy-sudo-docker answers that for
|
||||
# this session, so the prompt stays until the reboot that grants the group.
|
||||
# pkexec sanitizes the environment, so carry TERM through for the TUI to render
|
||||
# and run lazydocker from root's PATH.
|
||||
if omarchy-sudo-docker; then
|
||||
exec pkexec /usr/bin/env TERM="${TERM:-xterm-256color}" lazydocker
|
||||
else
|
||||
exec lazydocker
|
||||
fi
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open the OpenClaw Control UI (or its terminal UI with --tui), onboarding or starting the gateway first when needed.
|
||||
# omarchy:args=[--tui [--message <text>]]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
tui=false
|
||||
message=()
|
||||
if [[ ${1:-} == "--tui" ]]; then
|
||||
tui=true
|
||||
shift
|
||||
if [[ ${1:-} == "--message" ]]; then
|
||||
message=(--message "${2:?--message needs a value}")
|
||||
shift 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Onboarding is OpenClaw's own interactive wizard, run through
|
||||
# omarchy-openclaw-onboard so it stays in the terminal, installs the gateway
|
||||
# as a user service, and returns (see that script for why bare `openclaw
|
||||
# onboard` does none of those as of 2026.9.1). The config check gates what
|
||||
# follows because the wizard's "Skip for now" also exits 0: only inference
|
||||
# that passed writes the config, and skipping must not loop back into the
|
||||
# wizard.
|
||||
if [[ ! -f $HOME/.openclaw/openclaw.json ]]; then
|
||||
if [[ $tui == "true" ]]; then
|
||||
# Already in a terminal, so the wizard runs right here.
|
||||
omarchy-openclaw-onboard
|
||||
[[ -f $HOME/.openclaw/openclaw.json ]] || exit 1
|
||||
else
|
||||
# The wizard needs a real terminal, and chaining the relaunch means
|
||||
# finishing it lands the user in the app. Single quotes so $HOME expands
|
||||
# in the spawned terminal.
|
||||
# shellcheck disable=SC2016
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
'omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]] && omarchy-launch-openclaw'
|
||||
fi
|
||||
fi
|
||||
|
||||
dashboard_url() {
|
||||
# browserUrl carries a single-use browser handoff; url is the shared-auth
|
||||
# fallback for gateways predating the handoff flow. The timeout keeps a
|
||||
# wedged CLI from hanging an app-grid launch that has no terminal to ^C.
|
||||
timeout 10 openclaw dashboard --json 2>/dev/null | jq -re '.browserUrl // .url // empty'
|
||||
}
|
||||
|
||||
# --json never starts the gateway, so an empty answer means it is not running.
|
||||
# Recovery goes through the gateway's own service commands. `dashboard --yes`
|
||||
# used to be the start/install-without-prompting path, but as of 2026.9.1 it
|
||||
# defers to "the owning supervisor" for both a missing and a stopped unit,
|
||||
# and once the gateway is up it copies a one-time pairing URL into the
|
||||
# clipboard, which nothing here needs. Enablement, not the unit file, decides:
|
||||
# a unit that was written but never enabled (an install that died halfway)
|
||||
# would otherwise be "started" once and stay off at every following login,
|
||||
# where --force rewrites and enables it.
|
||||
if ! url=$(dashboard_url); then
|
||||
if systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then
|
||||
timeout 60 openclaw gateway start >&2 || true
|
||||
else
|
||||
timeout 120 openclaw gateway install --force >&2 || true
|
||||
fi
|
||||
|
||||
# A first-ever service install (unit write, daemon-reload, first boot)
|
||||
# takes materially longer than starting an installed unit, so the budget
|
||||
# is sized for the slow case.
|
||||
for _ in {1..30}; do
|
||||
url=$(dashboard_url) && break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z ${url:-} ]]; then
|
||||
echo "OpenClaw's gateway did not come up. Check it with: openclaw gateway status" >&2
|
||||
echo "If onboarding never finished, rerun it with: omarchy-openclaw-onboard" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $tui == "true" ]]; then
|
||||
# Attach to the gateway rather than `openclaw chat`: chat is the embedded
|
||||
# local runtime, which refuses to start while the gateway owns ~/.openclaw's
|
||||
# state directory -- and on any machine set up through Install > AI, the
|
||||
# gateway service is running whenever the desktop session is.
|
||||
exec openclaw tui "${message[@]}"
|
||||
fi
|
||||
|
||||
# The handoff URL rides in the browser's argv, which uwsm's app daemon echoes
|
||||
# into the user journal. Accepted: the token is single-use with a ten-minute
|
||||
# expiry against a loopback-only gateway, and journal access already implies
|
||||
# access to ~/.openclaw itself.
|
||||
exec omarchy-launch-webapp "$url"
|
||||
@@ -38,7 +38,7 @@ id=$(cut -f2 <<<"$selection")
|
||||
|
||||
if [[ $1 == "clone" ]]; then
|
||||
omarchy-launch-floating-terminal-with-presentation \
|
||||
"omarchy-plugin-clone --edit $(printf '%q' "$id")"
|
||||
"omarchy-plugin-clone $(printf '%q' "$id") --edit"
|
||||
elif [[ $1 == "remove" ]]; then
|
||||
omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove $(printf '%q' "$id")"
|
||||
else
|
||||
|
||||
+3
-3
@@ -85,16 +85,16 @@ wait_for_pacman_transaction
|
||||
mkdir -p "$STATE_DIR"
|
||||
[[ -d $MIGRATIONS_DIR ]] || exit 0
|
||||
|
||||
while IFS=$'\t' read -r name file marker; do
|
||||
while IFS=$'\t' read -r name file marker <&3; do
|
||||
[[ -n $name ]] || continue
|
||||
|
||||
if [[ ! -f $marker ]]; then
|
||||
echo -e "\e[32m\nRunning migration (${name%.sh})\e[0m"
|
||||
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file"
|
||||
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" 3<&-
|
||||
mkdir -p "$(dirname "$marker")"
|
||||
touch "$marker"
|
||||
fi
|
||||
done < <(migration_entries)
|
||||
done 3< <(migration_entries)
|
||||
|
||||
# Clear a login-time notification the user left sitting there and then resolved
|
||||
# by running migrations some other way. The substring matches both the current
|
||||
|
||||
@@ -41,7 +41,7 @@ fi
|
||||
# The shell keeps the click command with the toast, so this oneshot can hand the
|
||||
# invitation over and exit instead of staying activated until it is answered.
|
||||
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
||||
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-migrate" && exit 0
|
||||
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
||||
|
||||
# Reached when the notification could not be handed off, so fall back to telling
|
||||
# the user in the terminal.
|
||||
|
||||
@@ -12,8 +12,38 @@ package=$1
|
||||
command=${2:-$1}
|
||||
bin=${3:-$command}
|
||||
|
||||
# The command name becomes a file name under ~/.local/bin, so a slash in it
|
||||
# writes the wrapper somewhere else and the rm below deletes somewhere else. A
|
||||
# leading dot hides it or walks up, and a leading dash makes a name that reads
|
||||
# as an option to whatever picks it up. Checked before anything is removed or
|
||||
# written, and kept to those shapes so package names like npm:playwright still
|
||||
# stand in for the command name.
|
||||
case "$command" in
|
||||
*/* | .* | -* | *[[:cntrl:]]*)
|
||||
echo "omarchy-mise-install: '$command' is not usable as a command name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
# The heredoc below is unquoted, so whatever these hold is written into the
|
||||
# wrapper as shell source. Quote them the way omarchy-install-and-launch does, so
|
||||
# a package name carrying shell characters stays one argument instead of running.
|
||||
printf -v package_arg '%q' "$package"
|
||||
printf -v bin_arg '%q' "$bin"
|
||||
|
||||
# Keep values that are inert inside double quotes in the form existing migrations
|
||||
# recognize, including mise backend options that %q would unnecessarily escape.
|
||||
case "$package" in
|
||||
*'$'* | *'`'* | *'"'* | *'\'* | *'!'* | *[[:cntrl:]]*) ;;
|
||||
*) package_arg="\"$package\"" ;;
|
||||
esac
|
||||
case "$bin" in
|
||||
*'$'* | *'`'* | *'"'* | *'\'* | *'!'* | *[[:cntrl:]]*) ;;
|
||||
*) bin_arg="\"$bin\"" ;;
|
||||
esac
|
||||
|
||||
# These tools install and upgrade on first run, so mise's release cooldown would
|
||||
# hold a new version back for days after it ships. Exported rather than set on
|
||||
# the install line alone, so resolving the version to execute agrees with the
|
||||
@@ -22,8 +52,8 @@ rm -f "$HOME/.local/bin/$command"
|
||||
cat >"$HOME/.local/bin/$command" <<EOF
|
||||
#!/bin/bash
|
||||
export MISE_MINIMUM_RELEASE_AGE=0
|
||||
mise use -g "$package" || exit 1
|
||||
exec mise x "$package" -- "$bin" "\$@"
|
||||
mise use -g --quiet $package_arg || exit 1
|
||||
exec mise x $package_arg -- $bin_arg "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$HOME/.local/bin/$command"
|
||||
+151
-63
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Send an Omarchy desktop notification
|
||||
# omarchy:args=[--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]
|
||||
# omarchy:args=[--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [-i <icon>] [-t <ms>] [-r <id>] [-p] [--image <path-or-uri>] <headline> [description] [--exec <program> [args...]]
|
||||
# omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g
|
||||
|
||||
set -euo pipefail
|
||||
@@ -11,61 +11,75 @@ description=""
|
||||
glyph=
|
||||
urgency="low"
|
||||
app_name="omarchy-action"
|
||||
app_icon=""
|
||||
image=
|
||||
exec_command=
|
||||
args=()
|
||||
expire_timeout=-1
|
||||
replaces_id=0
|
||||
print_id=0
|
||||
exec_args=()
|
||||
exec_present=0
|
||||
parsed_option_args=0
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-notification-send [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [-i <icon>] [-t <ms>] [-r <id>] [-p] [--image <path-or-uri>] <headline> [description] [--exec <program> [args...]]" >&2
|
||||
}
|
||||
|
||||
# Recognize a known option, in both `--flag value` and `--flag=value` forms.
|
||||
# Returns 1 for anything unrecognized so the caller can decide (headline, or a
|
||||
# hard error in option position).
|
||||
parse_omarchy_option() {
|
||||
case $1 in
|
||||
-g | --glyph)
|
||||
if (($# < 2)); then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
glyph=$2
|
||||
parsed_option_args=2
|
||||
local opt val nargs
|
||||
if [[ $1 == --?*=* ]]; then
|
||||
opt=${1%%=*}
|
||||
val=${1#*=}
|
||||
nargs=1
|
||||
else
|
||||
opt=$1
|
||||
val=${2-}
|
||||
nargs=2
|
||||
fi
|
||||
|
||||
# -p/--print-id is a flag; it takes no value.
|
||||
if [[ $opt == -p || $opt == --print-id ]]; then
|
||||
print_id=1
|
||||
parsed_option_args=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
case $opt in
|
||||
-g | --glyph | -u | --urgency | --app-name | -i | --icon | --image | -r | --replace-id | -t | --expire-time) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if ((nargs == 2)) && (($# < 2)); then
|
||||
echo "Missing value for $opt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $opt in
|
||||
-g | --glyph) glyph=$val ;;
|
||||
-u | --urgency) urgency=$val ;;
|
||||
--app-name) app_name=$val ;;
|
||||
-i | --icon) app_icon=$val ;;
|
||||
--image) image=$val ;;
|
||||
-r | --replace-id)
|
||||
[[ $val =~ ^[0-9]+$ ]] || {
|
||||
echo "Invalid $opt value (numeric id expected): $val" >&2
|
||||
exit 1
|
||||
}
|
||||
replaces_id=$val
|
||||
;;
|
||||
-u | --urgency)
|
||||
if (($# < 2)); then
|
||||
echo "Missing value for $1" >&2
|
||||
-t | --expire-time)
|
||||
[[ $val =~ ^-?[0-9]+$ ]] || {
|
||||
echo "Invalid $opt value (milliseconds expected): $val" >&2
|
||||
exit 1
|
||||
fi
|
||||
urgency="$2"
|
||||
parsed_option_args=2
|
||||
return 0
|
||||
;;
|
||||
--app-name)
|
||||
if (($# < 2)); then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
app_name=$2
|
||||
parsed_option_args=2
|
||||
return 0
|
||||
;;
|
||||
--image)
|
||||
if (($# < 2)); then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
image=$2
|
||||
parsed_option_args=2
|
||||
return 0
|
||||
;;
|
||||
--exec)
|
||||
if (($# < 2)); then
|
||||
echo "Missing value for $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
exec_command=$2
|
||||
parsed_option_args=2
|
||||
return 0
|
||||
}
|
||||
expire_timeout=$val
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
parsed_option_args=$nargs
|
||||
return 0
|
||||
}
|
||||
|
||||
while (($# > 0)); do
|
||||
@@ -77,47 +91,121 @@ while (($# > 0)); do
|
||||
done
|
||||
|
||||
if (($# < 1)); then
|
||||
echo "Usage: omarchy-notification-send [--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
headline=$1
|
||||
shift
|
||||
|
||||
if (($# > 0)) && [[ $1 != -* ]]; then
|
||||
# The description is the next positional, taken as text even when it begins with
|
||||
# a dash — a body like "-50% off" or a negative number is content, not options.
|
||||
# Only a recognized option flag or --exec in that slot is not the description.
|
||||
known_flag() {
|
||||
case $1 in
|
||||
-g | --glyph | -u | --urgency | --app-name | -i | --icon | -t | --expire-time | --image | -r | --replace-id | -p | --print-id | --exec) return 0 ;;
|
||||
--glyph=* | --urgency=* | --app-name=* | --icon=* | --expire-time=* | --image=* | --replace-id=*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
if (($# > 0)) && ! known_flag "$1"; then
|
||||
description=$1
|
||||
shift
|
||||
fi
|
||||
|
||||
while (($# > 0)); do
|
||||
if parse_omarchy_option "$@"; then
|
||||
if [[ $1 == "--exec" ]]; then
|
||||
# --exec consumes the rest of the line as the click command's argv. The
|
||||
# caller's shell already tokenized those words into discrete arguments, and
|
||||
# the shell runs them as-is (never re-parsed), so untrusted data in an
|
||||
# argument is only ever one argument and can never become a command.
|
||||
# Detected only here, after the headline/description positionals are
|
||||
# captured, so an untrusted headline that is literally "--exec" is taken as
|
||||
# text and can't be mistaken for the delimiter. --exec therefore comes last.
|
||||
shift
|
||||
exec_args=("$@")
|
||||
exec_present=1
|
||||
break
|
||||
elif parse_omarchy_option "$@"; then
|
||||
shift "$parsed_option_args"
|
||||
else
|
||||
args+=("$1")
|
||||
shift
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Tag as a user-action toast so it pops through DND.
|
||||
args+=("-a" "$app_name" "-u" "$urgency")
|
||||
case $urgency in
|
||||
low) urgency_byte=0 ;;
|
||||
normal) urgency_byte=1 ;;
|
||||
critical) urgency_byte=2 ;;
|
||||
*)
|
||||
echo "Unknown urgency: $urgency (use low, normal, or critical)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# a{sv} hints, as busctl triples (key, variant type, value). urgency is a byte;
|
||||
# the rest are strings. The click command rides here as omarchy-exec-argv, built
|
||||
# only from --exec below.
|
||||
hints=(urgency y "$urgency_byte")
|
||||
|
||||
if [[ -n $glyph ]]; then
|
||||
args+=("--hint=string:omarchy-glyph:$glyph")
|
||||
hints+=(omarchy-glyph s "$glyph")
|
||||
fi
|
||||
|
||||
if [[ -n $image ]]; then
|
||||
args+=("--hint=string:image-path:$image")
|
||||
hints+=(image-path s "$image")
|
||||
fi
|
||||
|
||||
# The shell runs the click command itself, from a copy it keeps alongside the
|
||||
# on-screen popup. A libnotify action would instead keep this process blocked
|
||||
# until the click, and die unanswered whenever the shell restarts underneath it.
|
||||
if [[ -n $exec_command ]]; then
|
||||
args+=("--hint=string:omarchy-exec:$exec_command")
|
||||
if ((exec_present)); then
|
||||
if ((${#exec_args[@]} == 0)) || [[ -z ${exec_args[0]} ]]; then
|
||||
echo "--exec needs a command: --exec <program> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A single word with a space is almost always a whole command passed as one
|
||||
# quoted string — which would run a program literally named that. Splitting it
|
||||
# ourselves is exactly the injection we avoid, so reject it and point at the
|
||||
# unquoted form instead.
|
||||
if ((${#exec_args[@]} == 1)) && [[ ${exec_args[0]} == *[[:space:]]* ]]; then
|
||||
echo "--exec takes the command as separate words, not one quoted string." >&2
|
||||
echo "Write: --exec ${exec_args[0]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
# NUL-delimit into jq so every byte survives as data: jq's own --args would eat
|
||||
# a bare "--", and a newline in an arg must not split the vector.
|
||||
exec_argv_json=$(printf '%s\0' "${exec_args[@]}" | jq -Rsc 'split("\u0000")[:-1]')
|
||||
hints+=(omarchy-exec-argv s "$exec_argv_json")
|
||||
fi
|
||||
|
||||
if [[ -n $description ]]; then
|
||||
notify-send "${args[@]}" "$headline" "$description"
|
||||
hint_count=$((${#hints[@]} / 3))
|
||||
|
||||
# Call org.freedesktop.Notifications.Notify directly — never notify-send. Its
|
||||
# argv parsing is the surface that reinterprets a relayed headline like
|
||||
# `--hint=…` or `-rf` as options or hints; busctl takes each value as one typed
|
||||
# D-Bus parameter instead, and the leading `--` keeps a dash-leading value
|
||||
# (headline, description, a negative timeout) positional rather than a busctl
|
||||
# option. So the summary and body are strings that can never become a hint, and
|
||||
# omarchy-exec-argv is set only from --exec.
|
||||
#
|
||||
# Signature susssasa{sv}i: app_name, replaces_id, app_icon, summary, body,
|
||||
# actions (empty), hints, expire_timeout. replaces_id (from -r) updates a toast
|
||||
# in place; -p prints the returned id so a caller can reuse it.
|
||||
notify_cmd=(
|
||||
busctl --user -- call
|
||||
org.freedesktop.Notifications /org/freedesktop/Notifications
|
||||
org.freedesktop.Notifications Notify susssasa{sv}i
|
||||
"$app_name" "$replaces_id" "$app_icon" "$headline" "$description"
|
||||
0
|
||||
"$hint_count" "${hints[@]}"
|
||||
"$expire_timeout"
|
||||
)
|
||||
|
||||
if ((print_id)); then
|
||||
# busctl prints the UINT32 return as "u <id>"; emit just the id.
|
||||
out=$("${notify_cmd[@]}")
|
||||
printf '%s\n' "${out##* }"
|
||||
else
|
||||
notify-send "${args[@]}" "$headline"
|
||||
"${notify_cmd[@]}" >/dev/null
|
||||
fi
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Run OpenClaw's setup wizard the way Omarchy needs it: in the terminal, installing the gateway as a user service, and returning when it is done.
|
||||
|
||||
# Not bare `openclaw onboard`: as of 2026.9.1 that is the guided flow, which
|
||||
# ends by running a foreground gateway and handing off to a browser tab, and
|
||||
# never returns. --install-daemon keeps it to the classic wizard (minimal
|
||||
# prompts with --flow quickstart) that installs the gateway service, and
|
||||
# --skip-ui drops its closing Control UI/TUI prompt since whoever called this
|
||||
# opens one next.
|
||||
#
|
||||
# That classic wizard has one wrinkle: with --skip-ui it prints "Onboarding
|
||||
# complete" and then never exits. Upstream only calls exit(0) when it launched
|
||||
# the TUI, and the model sign-in leaves an open socket that keeps the process
|
||||
# alive otherwise. So this script watches for the gateway to answer and then
|
||||
# stops the wizard. That is safe because every prompt in the quickstart flow
|
||||
# runs before the gateway service is installed and reachable: by the time the
|
||||
# dashboard answers, only the closing notes are left.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
wizard=(openclaw onboard --flow quickstart --install-daemon --skip-ui)
|
||||
config=$HOME/.openclaw/openclaw.json
|
||||
settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3}
|
||||
# Counted from the moment the config exists, i.e. once the wizard has applied
|
||||
# setup; the prompts before that take as long as the user takes.
|
||||
gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180}
|
||||
|
||||
gateway_answers() {
|
||||
timeout 10 openclaw dashboard --json 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A gateway already answering means OpenClaw is set up, and this run must not
|
||||
# read its own success off state that predates it: the wizard's repair pass
|
||||
# would be stopped mid-prompt the moment the watcher looked. Nothing to do.
|
||||
if [[ -f $config ]] && gateway_answers; then
|
||||
echo "OpenClaw is already set up and its gateway is running." >&2
|
||||
echo "To change providers or settings, run: openclaw onboard --classic" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Marks when this run began, so a config left behind by an earlier, incomplete
|
||||
# setup is not mistaken for this run having applied its own: the gateway
|
||||
# deadline below must not start ticking while the user is still at prompts.
|
||||
started=$(mktemp)
|
||||
trap 'rm -f "$started"' EXIT
|
||||
|
||||
# Backgrounded so this script can watch it, but with the terminal kept as its
|
||||
# stdin (bash would otherwise hand a background job /dev/null). Job control is
|
||||
# off in a script, so it stays in the terminal's foreground process group and
|
||||
# reads from it freely. Ctrl-C does not reach it directly, though: bash starts
|
||||
# async children with SIGINT ignored when job control is off, so the INT trap
|
||||
# below is what turns Ctrl-C into the wizard's exit.
|
||||
"${wizard[@]}" <&0 &
|
||||
wizard_pid=$!
|
||||
|
||||
stop_wizard() {
|
||||
kill -TERM "$wizard_pid" 2>/dev/null || true
|
||||
}
|
||||
# Any signal at this script, whether Ctrl-C from the terminal or a kill aimed
|
||||
# at its pid alone, takes the wizard down with it rather than leaving it
|
||||
# running unwatched.
|
||||
trap 'stop_wizard' INT TERM HUP
|
||||
|
||||
# Whether this run has applied setup: the config exists and is not older than
|
||||
# the run itself.
|
||||
config_applied() {
|
||||
[[ -f $config && ! $started -nt $config ]]
|
||||
}
|
||||
|
||||
# Whether this run's gateway is up: setup applied, and the process answering
|
||||
# on the gateway's port is the main process of the service the wizard
|
||||
# installs. Not the dashboard alone: with no config on disk `openclaw
|
||||
# dashboard --json` still probes the default loopback port, so a gateway left
|
||||
# behind by something else (an earlier guided onboarding's foreground
|
||||
# gateway, say) would read as this run's success while the user is still at
|
||||
# the first prompt. And not the unit being active either: its Type=simple
|
||||
# counts it active from the fork, before it has found the port taken by such
|
||||
# an orphan, and the app would then open on the orphan rather than the
|
||||
# service this run installed.
|
||||
gateway_ready() {
|
||||
config_applied || return 1
|
||||
local json port listener main_pid
|
||||
json=$(timeout 10 openclaw dashboard --json 2>/dev/null) || return 1
|
||||
jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1
|
||||
port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null)
|
||||
[[ -n $port ]] || return 1
|
||||
listener=$(ss -ltnpH "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
||||
main_pid=$(systemctl --user show -p MainPID --value openclaw-gateway.service 2>/dev/null)
|
||||
[[ -n $listener && -n $main_pid && $main_pid != 0 && $listener == "$main_pid" ]]
|
||||
}
|
||||
|
||||
stopped=false
|
||||
timed_out=false
|
||||
config_seen_at=
|
||||
while kill -0 "$wizard_pid" 2>/dev/null; do
|
||||
sleep 2
|
||||
if gateway_ready; then
|
||||
# Let the outro finish printing, then end the process the wizard leaves
|
||||
# running.
|
||||
sleep "$settle_seconds"
|
||||
stop_wizard
|
||||
stopped=true
|
||||
break
|
||||
fi
|
||||
config_applied || continue
|
||||
: "${config_seen_at:=$SECONDS}"
|
||||
if (( SECONDS - config_seen_at >= gateway_timeout )); then
|
||||
# Setup was applied but the service never came up (port taken, unit
|
||||
# failing, ...): the wizard would sit in its never-exiting state forever,
|
||||
# and so would whoever is waiting on this script.
|
||||
stop_wizard
|
||||
timed_out=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
wait "$wizard_pid"
|
||||
rc=$?
|
||||
|
||||
if [[ $timed_out == true ]]; then
|
||||
echo "OpenClaw's gateway did not come up within ${gateway_timeout}s of setup finishing." >&2
|
||||
echo "Check it with: openclaw gateway status" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A wizard stopped here after the gateway came up did its job. One that
|
||||
# exited on its own, including a user who chose "Skip for now" (no config,
|
||||
# non-zero), keeps its own exit code.
|
||||
[[ $stopped == true ]] && rc=0
|
||||
exit "$rc"
|
||||
@@ -24,6 +24,6 @@ if [[ -n $pkg_names ]]; then
|
||||
source omarchy-sudo-keepalive
|
||||
|
||||
echo "$pkg_names" | sed 's/^/aur\//' | tr '\n' ' ' | xargs yay -S --noconfirm
|
||||
sudo updatedb
|
||||
sudo updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots
|
||||
omarchy-show-done
|
||||
fi
|
||||
@@ -93,6 +93,13 @@ if [[ -z $url ]]; then
|
||||
[[ -n $url ]] || fail "a git URL is required"
|
||||
fi
|
||||
|
||||
# Refuse a URL that names a git option or a transport helper before cloning, so
|
||||
# an untrusted URL cannot run a command before the plugin is validated or
|
||||
# enabled. The check is shared with omarchy-theme-install and explains itself; a
|
||||
# missing checker leaves this non-zero, which refuses the URL rather than
|
||||
# cloning it.
|
||||
omarchy-git-url-check "$url" || exit 1
|
||||
|
||||
if (( ! ASSUME_YES )); then
|
||||
cat >&2 <<WARN
|
||||
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
omarchy-refresh-plymouth
|
||||
omarchy-refresh-sddm
|
||||
set -euo pipefail
|
||||
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-plymouth"
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-sddm"
|
||||
+365
-67
@@ -5,79 +5,377 @@
|
||||
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# Configure the Plymouth boot theme with a custom background color, text color, and logo.
|
||||
# Stages the change in a temp dir, then commits the staged files to /usr/share and
|
||||
# rebuilds the initramfs. Also syncs the SDDM login screen (the post-logout
|
||||
# screen) with the same colors and logo so boot/login stay visually unified.
|
||||
set -euo pipefail
|
||||
|
||||
if (( $# != 3 )); then
|
||||
# Build the authoritative theme in a root-owned directory, then publish each
|
||||
# fixed destination atomically. The caller opens the selected logo before sudo,
|
||||
# so the privileged process never resolves a user-controlled input path.
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
bg_hex="${1#\#}"
|
||||
text_hex="${2#\#}"
|
||||
logo_path="$3"
|
||||
|
||||
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $logo_path ]]; then
|
||||
echo "Logo file not found: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
|
||||
theme_dir="/usr/share/plymouth/themes/omarchy"
|
||||
staging_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$staging_dir"' EXIT
|
||||
|
||||
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
|
||||
cp "$logo_path" "$staging_dir/logo.png"
|
||||
|
||||
sed -i \
|
||||
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
||||
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
||||
"$staging_dir/omarchy.script"
|
||||
|
||||
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
||||
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
|
||||
done
|
||||
|
||||
sudo cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
if (( $# == 3 )); then
|
||||
mode=set
|
||||
elif (( $# == 1 )); then
|
||||
case "$1" in
|
||||
--refresh-default)
|
||||
mode=refresh-plymouth
|
||||
;;
|
||||
--refresh-sddm-default)
|
||||
mode=refresh-sddm
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
else
|
||||
sudo mkinitcpio -P
|
||||
usage
|
||||
fi
|
||||
|
||||
# Sync the SDDM login screen with the same colors and logo.
|
||||
sddm_dir="/usr/share/sddm/themes/omarchy"
|
||||
sddm_template="$OMARCHY_PATH/default/sddm/omarchy/Main.qml"
|
||||
if (( EUID == 0 )); then
|
||||
echo "Error: run omarchy-plymouth-set as your user, not under sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s/#1a1b26/#$bg_hex/g" \
|
||||
-e "s/#ffffff/#$text_hex/g" \
|
||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
||||
logo_fd=
|
||||
if [[ $mode != "set" ]]; then
|
||||
bg_hex=
|
||||
text_hex=
|
||||
else
|
||||
bg_hex="${1#\#}"
|
||||
text_hex="${2#\#}"
|
||||
logo_path="$3"
|
||||
|
||||
sudo cp "$logo_path" "$sddm_dir/logo.png"
|
||||
for asset in bullet.png entry.png lock.png; do
|
||||
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
|
||||
done
|
||||
for asset in entry lock; do
|
||||
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
|
||||
sudo cp "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png"
|
||||
done
|
||||
sudo rm -f "$sddm_dir/logo.svg"
|
||||
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $logo_path ]]; then
|
||||
echo "Logo file not found: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -L $logo_path ]]; then
|
||||
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Open the logo while still unprivileged. A replacement symlink to a root-only
|
||||
# file therefore fails here instead of being followed after sudo starts.
|
||||
if ! exec {logo_fd}<"$logo_path"; then
|
||||
echo "Unable to open logo file as the current user: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f /proc/$$/fd/$logo_fd ]]; then
|
||||
echo "Logo input is no longer a regular file: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
run_root_transaction() {
|
||||
sudo /bin/bash -c '
|
||||
set -eEuo pipefail
|
||||
PATH=/usr/bin:/bin
|
||||
export PATH
|
||||
umask 077
|
||||
|
||||
# Every check below is a bare assertion that aborts under set -e. Name the
|
||||
# subject of each one so a refusal reaches the user instead of exiting mute.
|
||||
failure_context="the privileged Plymouth transaction"
|
||||
failure_reported=
|
||||
report_failure() {
|
||||
[[ -z $failure_reported ]] || return 0
|
||||
failure_reported=1
|
||||
printf "omarchy-plymouth-set: refusing to publish: %s failed validation\n" "$failure_context" >&2
|
||||
if [[ -n ${failure_hint:-} ]]; then
|
||||
printf "omarchy-plymouth-set: %s\n" "$failure_hint" >&2
|
||||
fi
|
||||
}
|
||||
trap report_failure ERR
|
||||
|
||||
mode=$1
|
||||
source_root=$2
|
||||
bg_hex=$3
|
||||
text_hex=$4
|
||||
max_asset_size=$5
|
||||
|
||||
failure_context="the arguments of the privileged transaction"
|
||||
[[ $mode == "set" || $mode == "refresh-plymouth" || $mode == "refresh-sddm" ]]
|
||||
[[ $source_root == /* ]]
|
||||
[[ $max_asset_size =~ ^[0-9]+$ ]]
|
||||
(( max_asset_size > 0 ))
|
||||
|
||||
failure_context="the Omarchy source tree $source_root"
|
||||
canonical_source_root=$(realpath -e -- "$source_root")
|
||||
[[ $canonical_source_root == "$source_root" ]]
|
||||
|
||||
validate_trusted_directory() {
|
||||
local directory=$1 canonical uid directory_mode
|
||||
|
||||
failure_context="directory $directory"
|
||||
canonical=$(realpath -e -- "$directory")
|
||||
[[ $canonical == "$directory" && -d $directory && ! -L $directory ]]
|
||||
|
||||
while :; do
|
||||
failure_context="directory $directory (must be root-owned and not group- or world-writable)"
|
||||
uid=$(stat -c %u -- "$directory")
|
||||
directory_mode=$(stat -c %a -- "$directory")
|
||||
(( uid == 0 ))
|
||||
(( (8#$directory_mode & 0022) == 0 ))
|
||||
[[ $directory == "/" ]] && break
|
||||
directory=${directory%/*}
|
||||
[[ -n $directory ]] || directory=/
|
||||
done
|
||||
}
|
||||
|
||||
validate_trusted_configuration_file() {
|
||||
local configuration=$1 canonical uid configuration_mode size
|
||||
|
||||
failure_context="root configuration $configuration"
|
||||
[[ -f $configuration && ! -L $configuration ]]
|
||||
canonical=$(realpath -e -- "$configuration")
|
||||
[[ $canonical == "$configuration" ]]
|
||||
validate_trusted_directory "${configuration%/*}"
|
||||
uid=$(stat -c %u -- "$configuration")
|
||||
configuration_mode=$(stat -c %a -- "$configuration")
|
||||
size=$(stat -c %s -- "$configuration")
|
||||
(( uid == 0 ))
|
||||
(( (8#$configuration_mode & 0022) == 0 ))
|
||||
(( size > 0 && size <= 4096 ))
|
||||
}
|
||||
|
||||
# A packaged tree must be root-owned. A development checkout is the one
|
||||
# deliberate exception: omarchy dev link records its canonical path in a
|
||||
# root-owned /etc/omarchy.conf. That is already an explicit decision to run
|
||||
# privileged Omarchy commands from user-editable code in the checkout, so
|
||||
# reading its packaged assets does not widen the development trust boundary.
|
||||
development_source=false
|
||||
source_root_uid=$(stat -c %u -- "$source_root")
|
||||
if (( source_root_uid != 0 )); then
|
||||
omarchy_conf=/etc/omarchy.conf
|
||||
failure_context="$source_root is user-owned and $omarchy_conf must contain its trusted dev-link authorization; run omarchy dev link to authorize it"
|
||||
failure_hint="$source_root is user-owned; run omarchy dev link to authorize this development checkout, or omarchy dev unlink to use the packaged tree"
|
||||
validate_trusted_configuration_file "$omarchy_conf"
|
||||
|
||||
# validate_trusted_configuration_file walks /etc up to / and leaves its own
|
||||
# subject behind in failure_context. Without restoring ours, a checkout
|
||||
# that simply is not the authorized one refuses with "directory / must be
|
||||
# root-owned and not group- or world-writable" -- naming a directory that
|
||||
# passed, and sending the reader after a filesystem problem that is not
|
||||
# there.
|
||||
failure_context="the dev-link authorization in $omarchy_conf, which must name $source_root"
|
||||
|
||||
quoted_source_root=$source_root
|
||||
quoted_source_root=${quoted_source_root//\\/\\\\}
|
||||
quoted_source_root=${quoted_source_root//\"/\\\"}
|
||||
quoted_source_root=${quoted_source_root//\$/\\\$}
|
||||
quoted_source_root=${quoted_source_root//\`/\\\`}
|
||||
expected_config_line="export OMARCHY_PATH=\"$quoted_source_root\""
|
||||
mapfile -t omarchy_config_lines <"$omarchy_conf"
|
||||
(( ${#omarchy_config_lines[@]} == 1 ))
|
||||
[[ ${omarchy_config_lines[0]} == "$expected_config_line" ]]
|
||||
development_source=true
|
||||
failure_hint=
|
||||
fi
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
[[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]
|
||||
[[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]
|
||||
fi
|
||||
|
||||
theme_dir=/usr/share/plymouth/themes/omarchy
|
||||
sddm_dir=/usr/share/sddm/themes/omarchy
|
||||
plymouth_theme_assets=(
|
||||
bullet.png
|
||||
entry.png
|
||||
lock.png
|
||||
logo.png
|
||||
omarchy.plymouth
|
||||
omarchy.script
|
||||
preview-unlock.png
|
||||
progress_bar.png
|
||||
progress_box.png
|
||||
)
|
||||
plymouth_default_assets=("${plymouth_theme_assets[@]}" logos/oma.png)
|
||||
sddm_theme_assets=(Main.qml bullet.png entry-failed.png entry.png lock-failed.png lock.png logo.png)
|
||||
sddm_default_assets=("${sddm_theme_assets[@]}" metadata.desktop theme.conf)
|
||||
|
||||
plymouth_assets=()
|
||||
sddm_assets=()
|
||||
case "$mode" in
|
||||
set)
|
||||
plymouth_assets=("${plymouth_theme_assets[@]}")
|
||||
sddm_assets=("${sddm_theme_assets[@]}")
|
||||
;;
|
||||
refresh-plymouth)
|
||||
plymouth_assets=("${plymouth_default_assets[@]}")
|
||||
;;
|
||||
refresh-sddm)
|
||||
sddm_assets=("${sddm_default_assets[@]}")
|
||||
;;
|
||||
esac
|
||||
|
||||
validate_trusted_file() {
|
||||
local source=$1 canonical uid file_mode size
|
||||
|
||||
failure_context="packaged source file $source"
|
||||
[[ -f $source && ! -L $source ]]
|
||||
canonical=$(realpath -e -- "$source")
|
||||
[[ $canonical == "$source" ]]
|
||||
file_mode=$(stat -c %a -- "$source")
|
||||
size=$(stat -c %s -- "$source")
|
||||
(( size > 0 && size <= max_asset_size ))
|
||||
|
||||
if ! $development_source; then
|
||||
validate_trusted_directory "${source%/*}"
|
||||
uid=$(stat -c %u -- "$source")
|
||||
(( uid == 0 ))
|
||||
(( (8#$file_mode & 0022) == 0 ))
|
||||
fi
|
||||
}
|
||||
|
||||
copy_trusted_file() {
|
||||
local source=$1 destination=$2
|
||||
|
||||
validate_trusted_file "$source"
|
||||
install -o 0 -g 0 -m 0600 -- "$source" "$destination"
|
||||
}
|
||||
|
||||
staging_dir=$(mktemp -d /tmp/omarchy-plymouth.XXXXXXXX)
|
||||
temporary=
|
||||
cleanup() {
|
||||
[[ -z $temporary ]] || rm -f -- "$temporary"
|
||||
rm -rf -- "$staging_dir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
chown 0:0 -- "$staging_dir"
|
||||
chmod 0700 -- "$staging_dir"
|
||||
|
||||
plymouth_stage=$staging_dir/plymouth
|
||||
sddm_stage=$staging_dir/sddm
|
||||
mkdir -m 0700 -p -- "$plymouth_stage/logos" "$sddm_stage"
|
||||
|
||||
for asset in "${plymouth_assets[@]}"; do
|
||||
copy_trusted_file "$source_root/default/plymouth/$asset" "$plymouth_stage/$asset"
|
||||
done
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
# stdin was opened by the unprivileged caller. Read no more than the
|
||||
# documented limit into the root-owned stage before doing other work.
|
||||
failure_context="the selected logo (expected 1 to $max_asset_size bytes)"
|
||||
head -c "$((max_asset_size + 1))" >"$plymouth_stage/logo.png"
|
||||
logo_size=$(stat -c %s -- "$plymouth_stage/logo.png")
|
||||
(( logo_size > 0 && logo_size <= max_asset_size ))
|
||||
chown 0:0 -- "$plymouth_stage/logo.png"
|
||||
chmod 0600 -- "$plymouth_stage/logo.png"
|
||||
cp --reflink=never -- "$plymouth_stage/logo.png" "$sddm_stage/logo.png"
|
||||
|
||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
|
||||
sed -i \
|
||||
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
||||
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
||||
"$plymouth_stage/omarchy.script"
|
||||
|
||||
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
||||
magick "$plymouth_stage/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$plymouth_stage/$asset"
|
||||
done
|
||||
|
||||
copy_trusted_file "$source_root/default/sddm/omarchy/Main.qml" "$sddm_stage/Main.qml"
|
||||
sed -i \
|
||||
-e "s/#1a1b26/#__OMARCHY_SDDM_BG__/g" \
|
||||
-e "s/#ffffff/#__OMARCHY_SDDM_TEXT__/g" \
|
||||
-e "s/#__OMARCHY_SDDM_BG__/#$bg_hex/g" \
|
||||
-e "s/#__OMARCHY_SDDM_TEXT__/#$text_hex/g" \
|
||||
"$sddm_stage/Main.qml"
|
||||
|
||||
for asset in bullet.png entry.png lock.png; do
|
||||
cp --reflink=never -- "$plymouth_stage/$asset" "$sddm_stage/$asset"
|
||||
done
|
||||
for asset in entry lock; do
|
||||
magick "$plymouth_stage/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$sddm_stage/$asset-failed.png"
|
||||
done
|
||||
chown -R 0:0 -- "$staging_dir"
|
||||
find "$staging_dir" -type f -exec chmod 0600 -- {} +
|
||||
elif (( ${#sddm_assets[@]} )); then
|
||||
for asset in "${sddm_assets[@]}"; do
|
||||
copy_trusted_file "$source_root/default/sddm/omarchy/$asset" "$sddm_stage/$asset"
|
||||
done
|
||||
fi
|
||||
|
||||
if (( ${#plymouth_assets[@]} )); then
|
||||
validate_trusted_directory "$theme_dir"
|
||||
if [[ $mode == "refresh-plymouth" ]]; then
|
||||
validate_trusted_directory "$theme_dir/logos"
|
||||
fi
|
||||
fi
|
||||
if (( ${#sddm_assets[@]} )); then
|
||||
validate_trusted_directory "$sddm_dir"
|
||||
fi
|
||||
|
||||
publish_asset() {
|
||||
local source=$1 destination=$2 parent filename source_size copied_size
|
||||
|
||||
failure_context="destination $destination"
|
||||
[[ -f $source && ! -L $source ]]
|
||||
(( $(stat -c %u -- "$source") == 0 ))
|
||||
source_size=$(stat -c %s -- "$source")
|
||||
(( source_size > 0 && source_size <= max_asset_size ))
|
||||
|
||||
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
|
||||
parent=${destination%/*}
|
||||
filename=${destination##*/}
|
||||
[[ -n $parent && -n $filename && $filename != "." && $filename != ".." ]]
|
||||
validate_trusted_directory "$parent"
|
||||
|
||||
temporary=$(mktemp --tmpdir="$parent" ".$filename.omarchy-new.XXXXXXXX")
|
||||
install -o 0 -g 0 -m 0644 -- "$source" "$temporary"
|
||||
copied_size=$(stat -c %s -- "$temporary")
|
||||
(( copied_size == source_size ))
|
||||
cmp -s -- "$source" "$temporary"
|
||||
sync -f -- "$temporary"
|
||||
mv --no-copy -fT -- "$temporary" "$destination"
|
||||
temporary=
|
||||
}
|
||||
|
||||
if (( ${#plymouth_assets[@]} )); then
|
||||
for asset in "${plymouth_assets[@]}"; do
|
||||
publish_asset "$plymouth_stage/$asset" "$theme_dir/$asset"
|
||||
done
|
||||
fi
|
||||
|
||||
if (( ${#sddm_assets[@]} )); then
|
||||
for asset in "${sddm_assets[@]}"; do
|
||||
publish_asset "$sddm_stage/$asset" "$sddm_dir/$asset"
|
||||
done
|
||||
validate_trusted_directory "$sddm_dir"
|
||||
rm -f -- "$sddm_dir/logo.svg"
|
||||
fi
|
||||
' bash "$mode" "$OMARCHY_PATH" "$bg_hex" "$text_hex" "$((64 * 1024 * 1024))"
|
||||
}
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
run_root_transaction <&"$logo_fd"
|
||||
else
|
||||
run_root_transaction </dev/null
|
||||
fi
|
||||
|
||||
if [[ $mode != "refresh-sddm" ]]; then
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
else
|
||||
sudo mkinitcpio -P
|
||||
fi
|
||||
fi
|
||||
@@ -141,7 +141,8 @@ say() {
|
||||
notice() {
|
||||
clear_logo
|
||||
echo
|
||||
gum spin --spinner "pulse" --title "$1" -- sleep "${2:-2}"
|
||||
printf '%*s\033[32m● \033[0m%s\n' "$PADDING_LEFT" '' "$1"
|
||||
sleep "${2:-2}"
|
||||
echo
|
||||
}
|
||||
|
||||
@@ -676,6 +677,15 @@ user_groups() {
|
||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||
while IFS= read -r group; do
|
||||
[[ -n $group ]] || continue
|
||||
# Never replay old privileged group defaults. Docker is always opt-in.
|
||||
# Input is only retained when the factory image has one of the features
|
||||
# whose installer deliberately grants access to raw input devices.
|
||||
[[ $group == "docker" ]] && continue
|
||||
if [[ $group == "input" ]] &&
|
||||
! pacman -Qq xpadneo-dkms &>/dev/null &&
|
||||
! pacman -Qq ydotool &>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
getent group "$group" >/dev/null || continue
|
||||
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
||||
done <"$PROVISIONING_DIR/groups"
|
||||
@@ -736,6 +746,12 @@ create_user() {
|
||||
# for specific commands), and a duplicate grant is harmless.
|
||||
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
||||
chmod 440 /etc/sudoers.d/00-omarchy-wheel
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||
[[ -d $dir || -L $dir ]] || continue
|
||||
browser_policy_setup_dir "$dir"
|
||||
done
|
||||
}
|
||||
|
||||
install_authorized_keys() {
|
||||
@@ -776,7 +792,7 @@ configure_login() {
|
||||
# After=) is what makes it deterministic — no sleep/race against SDDM's startup.
|
||||
install_autologin_once_cleanup() {
|
||||
local unit=omarchy-provision-autologin-once.service
|
||||
cat >"/etc/systemd/system/$unit" <<UNIT
|
||||
sed "s|@UNIT@|$unit|g" >"/etc/systemd/system/$unit" <<'UNIT'
|
||||
[Unit]
|
||||
Description=Drop the first-boot autologin before the next login
|
||||
Before=display-manager.service
|
||||
@@ -785,7 +801,7 @@ ConditionPathExists=/etc/sddm.conf.d/autologin.conf
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/rm -f /etc/sddm.conf.d/autologin.conf
|
||||
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/$unit /etc/systemd/system/$unit
|
||||
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/@UNIT@ /etc/systemd/system/@UNIT@
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
|
||||
@@ -84,7 +84,7 @@ fi
|
||||
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
|
||||
# point at a dev checkout (omarchy dev link) where the target differs.
|
||||
# Loops every skill directory, so shipping a new one needs no edit here.
|
||||
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills
|
||||
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.hermes/skills
|
||||
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
|
||||
skill=${skill%/}
|
||||
name=${skill##*/}
|
||||
@@ -92,6 +92,14 @@ for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
|
||||
ln -sfn "$skill" ~/.claude/skills/"$name"
|
||||
ln -sfn "$skill" ~/.codex/skills/"$name"
|
||||
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
|
||||
ln -sfn "$skill" ~/.hermes/skills/"$name"
|
||||
if [[ -d ~/.hermes/profiles ]]; then
|
||||
for profile in ~/.hermes/profiles/*/; do
|
||||
[[ -d $profile ]] || continue
|
||||
mkdir -p "$profile/skills"
|
||||
ln -sfn "$skill" "$profile/skills/$name"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
sudo cp -r "$OMARCHY_PATH/default/plymouth/." /usr/share/plymouth/themes/omarchy/
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
else
|
||||
sudo mkinitcpio -P
|
||||
fi
|
||||
# Reuse the fixed-file publisher so root never resolves the source checkout or
|
||||
# follows a destination symlink while restoring the packaged assets.
|
||||
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-default
|
||||
@@ -3,5 +3,6 @@
|
||||
# omarchy:summary=Refresh the SDDM theme from default
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
sudo rm -rf /usr/share/sddm/themes/omarchy
|
||||
sudo cp -r "$OMARCHY_PATH/default/sddm/omarchy" /usr/share/sddm/themes/omarchy
|
||||
# Reuse the fixed-file publisher so root never resolves an untrusted source or
|
||||
# follows a destination symlink while restoring the packaged SDDM theme.
|
||||
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-sddm-default
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
ensure_hermes_stopped() {
|
||||
python3 - "$HOME" <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
home = Path(sys.argv[1])
|
||||
roots = [str((home / relative).resolve()) for relative in ('.hermes', '.config/Hermes')]
|
||||
roots.append('/opt/hermes-desktop')
|
||||
|
||||
def belongs_to_hermes(target):
|
||||
target = target.removesuffix(' (deleted)')
|
||||
return any(target == root or target.startswith(root + '/') for root in roots)
|
||||
|
||||
holders = []
|
||||
for process in Path('/proc').iterdir():
|
||||
if not process.name.isdigit() or int(process.name) == os.getpid():
|
||||
continue
|
||||
try:
|
||||
if process.stat().st_uid != os.getuid():
|
||||
continue
|
||||
targets = [os.fsdecode(arg) for arg in (process / 'cmdline').read_bytes().split(b'\0')]
|
||||
entries = [process / 'exe', process / 'cwd']
|
||||
try:
|
||||
entries.extend((process / 'fd').iterdir())
|
||||
except PermissionError:
|
||||
pass
|
||||
for entry in entries:
|
||||
try:
|
||||
targets.append(os.readlink(entry))
|
||||
except OSError:
|
||||
pass
|
||||
if any(belongs_to_hermes(target) for target in targets):
|
||||
holders.append(process.name)
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
||||
continue
|
||||
|
||||
if holders:
|
||||
print('Close Hermes and processes using its files before removing it (PIDs: '
|
||||
+ ', '.join(holders) + '). Then try again.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
# Removing an open SQLite WAL leaves a live writer on a deleted generation.
|
||||
ensure_hermes_stopped
|
||||
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
|
||||
# before the app existed. Remove Hermes clears that too, scoped by the installer
|
||||
# to what Omarchy owns so a hermes the user set up themselves is left alone.
|
||||
# Tolerated here rather than fatal, so the ~/.hermes handling below still runs;
|
||||
# the failure is answered for at the end instead of being swallowed.
|
||||
cli_removed=true
|
||||
ensure_hermes_stopped
|
||||
omarchy-install-hermes-cli --remove || cli_removed=false
|
||||
|
||||
# The app writes this when the runtime it provisions under ~/.hermes has landed,
|
||||
# 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 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, 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" \
|
||||
"$HOME/.hermes/bin" \
|
||||
"$HOME/.hermes/node"
|
||||
|
||||
# Only the wrappers pointing into ~/.hermes, matched as a plain string: the
|
||||
# path carries a dot, so an unanchored pattern would also claim a wrapper
|
||||
# pointing at a sibling like ~/xhermes.
|
||||
for command in hermes hermes-agent hermes-acp; do
|
||||
wrapper="$HOME/.local/bin/$command"
|
||||
|
||||
if [[ -f $wrapper && ! -L $wrapper ]] && grep -qF "$HOME/.hermes" "$wrapper"; then
|
||||
rm -f "$wrapper"
|
||||
fi
|
||||
done
|
||||
|
||||
# When Hermes brought its own Node it symlinked these next to its own commands,
|
||||
# and they point at what we just deleted. Only the links into ~/.hermes: a
|
||||
# system Node, or someone else's, lives somewhere else entirely.
|
||||
for command in node npm npx; do
|
||||
link="$HOME/.local/bin/$command"
|
||||
|
||||
if [[ -L $link && $(readlink "$link") == "$HOME/.hermes"/* ]]; then
|
||||
rm -f "$link"
|
||||
fi
|
||||
done
|
||||
|
||||
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 ]] && omarchy-cmd-present gum; 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
|
||||
ensure_hermes_stopped
|
||||
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."
|
||||
fi
|
||||
|
||||
# The messages above still hold -- the app and its runtime are gone -- but a CLI
|
||||
# teardown that failed already said so on stderr, and that stands.
|
||||
if [[ $cli_removed == "false" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the OpenClaw agent platform along with its gateway service and web app.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
unit_dir="$HOME/.config/systemd/user"
|
||||
|
||||
# Only systemd's own word counts as "stopped": a non-zero exit from is-active
|
||||
# also covers an unreachable user manager, which says nothing about whether
|
||||
# the process is alive.
|
||||
unit_stopped() {
|
||||
local state
|
||||
state=$(systemctl --user is-active "$1" 2>/dev/null) || true
|
||||
[[ $state == inactive || $state == failed ]]
|
||||
}
|
||||
|
||||
# The user services OpenClaw installs for itself: the gateway through
|
||||
# onboarding, and the node host if the user ever paired this machine to
|
||||
# another gateway. OpenClaw writes them to $HOME/.config regardless of
|
||||
# XDG_CONFIG_HOME, so look exactly there; no unit means nothing registered.
|
||||
# Upstream's own teardown knows every piece its install wrote (unit file, the
|
||||
# default.target.wants enablement symlink, the reload), so prefer it while the
|
||||
# binary is still installed and fall back to doing the same by hand. A service
|
||||
# that will not stop aborts the removal: dropping the package would strand the
|
||||
# live process on deleted code with no way to restart it cleanly.
|
||||
for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node.service; do
|
||||
[[ -f $unit_file ]] || continue
|
||||
unit=${unit_file##*/}
|
||||
role=${unit#openclaw-}
|
||||
role=${role%.service}
|
||||
if openclaw "$role" uninstall >/dev/null 2>&1 ||
|
||||
systemctl --user disable --now "$unit" 2>/dev/null ||
|
||||
unit_stopped "$unit"; then
|
||||
# .bak is what `gateway install --force` leaves behind when it rewrites a
|
||||
# unit, so it goes with the unit.
|
||||
rm -f "$unit_file" "$unit_file.bak" "$unit_dir/default.target.wants/$unit"
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
# A unit that had been failing stays listed as "not-found failed" after
|
||||
# its file is gone until its failed state is reset.
|
||||
systemctl --user reset-failed "$unit" 2>/dev/null || true
|
||||
else
|
||||
echo "Could not stop $unit; OpenClaw was not removed." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
omarchy-pkg-drop openclaw
|
||||
|
||||
# The web app launcher and icon omarchy-install-ai-openclaw created.
|
||||
rm -f "$HOME/.local/share/applications/OpenClaw.desktop"
|
||||
rm -f "$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
|
||||
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
|
||||
|
||||
# ~/.openclaw stays unless asked: the chats, memories, and credentials in
|
||||
# there are the user's agent, and finding them intact after a reinstall is the
|
||||
# better surprise. But it also holds the plugin runtimes and caches OpenClaw
|
||||
# downloads for itself, easily hundreds of megabytes, so the choice is put in
|
||||
# front of the user with the size rather than left silent. Without a terminal
|
||||
# to ask in, keeping it is the answer.
|
||||
state_removed=false
|
||||
if [[ -d $HOME/.openclaw && -t 0 ]] && omarchy-cmd-present gum; then
|
||||
size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1)
|
||||
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then
|
||||
rm -rf "$HOME/.openclaw"
|
||||
state_removed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "OpenClaw has been removed."
|
||||
if [[ $state_removed == true ]]; then
|
||||
echo "Its chats, memories, and settings in ~/.openclaw are gone too."
|
||||
elif [[ -d $HOME/.openclaw ]]; then
|
||||
echo "Your agent's chats, memories, and settings are still in ~/.openclaw."
|
||||
fi
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the Perplexity desktop app along with its runtime caches.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
omarchy-pkg-drop perplexity
|
||||
|
||||
# The runtime the app downloads for itself: llama.cpp builds and local models
|
||||
# under ~/.local/share, the older download location under ~/.cache. Of the
|
||||
# perplexity-* dirs these are the only ones the desktop app owns; the rest
|
||||
# belong to Perplexity products that outlive it.
|
||||
rm -rf \
|
||||
"$HOME/.cache/Perplexity" \
|
||||
"$HOME/.cache/perplexity-rpc-server" \
|
||||
"$HOME/.local/share/perplexity-rpc-server"
|
||||
|
||||
# What is left is the user's: the logins and session in ~/.config/Perplexity,
|
||||
# the secret vault and device identity in ~/.local/state/perplexity, and the
|
||||
# launcher flags. Keeping them stays the default -- they are small, and being
|
||||
# signed in 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, default no. Without a terminal to ask in, keeping it
|
||||
# is the answer -- and gum draws the prompt on stderr, so a redirected stderr
|
||||
# would block on a question nobody can see.
|
||||
data_removed=false
|
||||
if [[ -t 0 && -t 2 ]]; then
|
||||
# du answers non-zero when a directory is missing, and pipefail would turn
|
||||
# that into an aborted removal; the size is worth no such thing.
|
||||
size=$(du -shc "$HOME/.config/Perplexity" "$HOME/.local/state/perplexity" 2>/dev/null | tail -1 | cut -f1 || true)
|
||||
if gum confirm --default=false "Also delete your Perplexity data ($size: logins, settings, secret vault and device identity)?"; then
|
||||
rm -rf \
|
||||
"$HOME/.config/Perplexity" \
|
||||
"$HOME/.local/state/perplexity"
|
||||
rm -f "$HOME/.config/perplexity-flags.conf"
|
||||
data_removed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Perplexity has been removed."
|
||||
if [[ $data_removed == "true" ]]; then
|
||||
echo "Its logins, settings, secret vault, and device identity are gone too."
|
||||
else
|
||||
echo "Nothing of yours was touched: not the logins in ~/.config/Perplexity,"
|
||||
echo "nor the secret vault and device identity in ~/.local/state/perplexity."
|
||||
fi
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove T3 Code along with its configuration and workspaces.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
omarchy-pkg-drop t3code-bin
|
||||
|
||||
# T3 Code drives agents that keep their own state -- ~/.claude.json, ~/.grok,
|
||||
# ~/.local/share/opencode -- and those outlive it, so only its own goes.
|
||||
rm -rf \
|
||||
"$HOME/.config/t3code" \
|
||||
"$HOME/.t3"
|
||||
rm -f "$HOME/.config/t3code-flags.conf"
|
||||
|
||||
echo ""
|
||||
echo "T3 Code has been removed."
|
||||
@@ -17,6 +17,27 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
||||
~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \
|
||||
~/.local/bin/omp ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
|
||||
|
||||
# Cursor's own installer links ~/.local/bin/cursor-agent as well, so only
|
||||
# the mise wrapper omarchy-mise-install wrote is a preinstall.
|
||||
if [[ -f ~/.local/bin/cursor-agent && ! -L ~/.local/bin/cursor-agent ]] &&
|
||||
grep -Eq '^mise use -g .*"cursor-agent"' ~/.local/bin/cursor-agent; then
|
||||
rm -f ~/.local/bin/cursor-agent
|
||||
fi
|
||||
|
||||
# Preserve a user-managed Muse launcher at the same path.
|
||||
if [[ -f ~/.local/bin/muse && ! -L ~/.local/bin/muse ]] &&
|
||||
grep -Eq '^mise use -g .*"http:muse\[' ~/.local/bin/muse; then
|
||||
rm -f ~/.local/bin/muse
|
||||
fi
|
||||
|
||||
# Only the wrapper omarchy-install-hermes-cli wrote is a preinstall. Hermes
|
||||
# Desktop's command, an official install, or anything else at that path is
|
||||
# the user's, so it is the installer that decides whether the wrapper is its
|
||||
# own, rather than a copy of its marker kept here.
|
||||
if omarchy-install-hermes-cli --owns; then
|
||||
rm -f ~/.local/bin/hermes
|
||||
fi
|
||||
|
||||
omarchy-pkg-drop \
|
||||
aether \
|
||||
cliamp \
|
||||
|
||||
@@ -24,9 +24,14 @@ echo -e "\e[32mRemoving FIDO2 device from authentication.\n\e[0m"
|
||||
|
||||
remove_pam_config
|
||||
|
||||
if [[ -d /etc/fido2 ]]; then
|
||||
authdir=/etc/fido2
|
||||
|
||||
# -d follows symlinks, so a dangling link at /etc/fido2 would survive this and
|
||||
# a later setup would install the authfile through it. rm -rf on a symlink
|
||||
# removes the link itself, never the directory it points at.
|
||||
if [[ -e $authdir || -L $authdir ]]; then
|
||||
echo "Removing FIDO2 configuration..."
|
||||
sudo rm -rf /etc/fido2
|
||||
sudo rm -rf "$authdir"
|
||||
fi
|
||||
|
||||
echo "Removing FIDO2 packages..."
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Disable sudoless Docker by removing your user from the docker group
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
# Ask about the configured groups, not this session's: right after enabling,
|
||||
# sudoless Docker is on for the account even though the running session still
|
||||
# needs a prompt, and this command is what turns it back off.
|
||||
if omarchy-sudo-docker --configured; then
|
||||
echo "Sudoless Docker is not enabled: $USER is not in the docker group."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Removing $USER from the docker group..."
|
||||
sudo gpasswd -d "$USER" docker >/dev/null
|
||||
|
||||
# Group membership is only re-read by a fresh session, and in practice logging
|
||||
# out or newgrp isn't enough — only a reboot reliably applies it. Record it so a
|
||||
# later `omarchy update` still prompts (omarchy-update-restart reads this), then
|
||||
# offer to do it now.
|
||||
omarchy-state set reboot-required
|
||||
|
||||
echo ""
|
||||
echo "Sudoless Docker DISABLED. Docker access goes through a polkit/sudo prompt"
|
||||
echo "again: the Docker TUI (Super + Shift + D) and the Windows VM ask when they"
|
||||
echo "need it, and the plain 'docker' CLI runs under sudo. It takes effect after a reboot."
|
||||
echo ""
|
||||
# The migration reuses this command during 'omarchy update' and defers the
|
||||
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||
omarchy-system-reboot
|
||||
fi
|
||||
@@ -63,7 +63,6 @@ relock_session() {
|
||||
|
||||
# Each kill stops the oldest matching instance and only returns once it has
|
||||
# fully exited, so the no-duplicate launch below can't race a dying shell.
|
||||
# Requires our quickshell-git build; 0.3.0's kill returns immediately.
|
||||
while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; do :; done
|
||||
|
||||
# Spawn from Hyprland so the shell inherits the canonical session environment,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
|
||||
check_fido2_hardware() {
|
||||
@@ -50,13 +51,79 @@ if ! check_fido2_hardware; then
|
||||
fi
|
||||
|
||||
# Create the pamu2fcfg file
|
||||
if [[ ! -f /etc/fido2/fido2 ]]; then
|
||||
sudo mkdir -p /etc/fido2
|
||||
authdir=/etc/fido2
|
||||
authfile=/etc/fido2/fido2
|
||||
|
||||
# install -d follows a symlink here and applies the mode and ownership to
|
||||
# whatever it points at, so the credential would be staged and published inside
|
||||
# the link target and that directory reopened to root:root 755. This is the
|
||||
# threat omarchy-remove-security-fido2 already names on its side.
|
||||
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
|
||||
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
|
||||
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -f follows symlinks, so the already-registered check below reads a symlinked
|
||||
# authfile as a registration and leaves it in place, and is false for a
|
||||
# directory, so it tries to register over one. Only a regular file is a valid
|
||||
# pam_u2f authfile.
|
||||
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
|
||||
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
|
||||
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $authfile ]]; then
|
||||
sudo install -d -m 755 -o root -g root "$authdir"
|
||||
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
||||
echo -e "Touch your FIDO2 key when it lights up...\n"
|
||||
|
||||
if pamu2fcfg >/tmp/fido2; then
|
||||
sudo mv /tmp/fido2 /etc/fido2/fido2
|
||||
# A unique sibling created by root cannot be replaced by another process
|
||||
# running as this user. Stream pamu2fcfg into it instead of asking root to
|
||||
# reopen a caller-owned path: an observed temporary name could otherwise be
|
||||
# replaced with a symlink before the privileged copy. The final rename is
|
||||
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
|
||||
# root-owned global authfile readable when pam_u2f uses openasuser; only root
|
||||
# can still rewrite it.
|
||||
stage=""
|
||||
|
||||
# mktemp's output is an operand for four privileged commands below, one of
|
||||
# them an rm. Take only the name this script asked for rather than whatever
|
||||
# came back on stdout.
|
||||
safe_stage_path() {
|
||||
local candidate=$1
|
||||
local prefix="$authfile.new."
|
||||
local suffix
|
||||
|
||||
[[ $candidate == "$prefix"* ]] || return 1
|
||||
suffix=${candidate#"$prefix"}
|
||||
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||
}
|
||||
|
||||
cleanup_stage() {
|
||||
local status=$?
|
||||
|
||||
if safe_stage_path "$stage"; then
|
||||
sudo rm -f -- "$stage" || true
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
}
|
||||
|
||||
trap cleanup_stage EXIT
|
||||
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
||||
|
||||
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
||||
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
|
||||
sudo chmod 644 "$stage"
|
||||
sudo mv -Tf "$stage" "$authfile"
|
||||
stage=""
|
||||
trap - EXIT
|
||||
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
||||
else
|
||||
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
||||
|
||||
@@ -41,6 +41,9 @@ setup_pam_config() {
|
||||
fi
|
||||
else
|
||||
echo "Creating polkit configuration with fingerprint authentication..."
|
||||
# omarchy:heredoc-expands paths=none -- $fprintd_gate is the literal PAM
|
||||
# line defined above, shared with the two sed insertions so the gate cannot
|
||||
# drift between files. The only path in it is the fixed /usr/bin one.
|
||||
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
|
||||
$fprintd_gate
|
||||
auth sufficient pam_fprintd.so
|
||||
@@ -71,18 +74,15 @@ if ! omarchy-hw-fingerprint; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install required packages
|
||||
echo "Installing required packages..."
|
||||
|
||||
# libfprint-git provides+conflicts libfprint; pacman -S --noconfirm
|
||||
# defaults the conflict prompt to N and aborts. Pre-remove it (deps-only,
|
||||
# so an installed fprintd stays put) so stock libfprint installs cleanly.
|
||||
if pacman -Q libfprint-git &>/dev/null; then
|
||||
sudo pacman -Rdd --noconfirm libfprint-git
|
||||
# libfprint-git tracks upstream ahead of the Arch release, so a new reader only
|
||||
# needs a pin bump in omarchy-pkgs. It conflicts with stock libfprint, and
|
||||
# --noconfirm answers that prompt with N; --ask 4 accepts the replacement in
|
||||
# one transaction, so a failed install leaves the existing driver in place.
|
||||
if omarchy-pkg-missing libfprint-git fprintd usbutils; then
|
||||
echo "Installing required packages..."
|
||||
sudo pacman -S --needed --noconfirm --ask 4 libfprint-git fprintd usbutils
|
||||
fi
|
||||
|
||||
omarchy-pkg-add libfprint fprintd usbutils
|
||||
|
||||
# Enroll first fingerprint
|
||||
echo -e "\e[32m\nLet's setup your right index finger as the first fingerprint.\e[0m"
|
||||
echo -e "Keep moving the finger around on sensor until the process completes.\n"
|
||||
|
||||
@@ -107,6 +107,50 @@ authorize_pasted_key() {
|
||||
authorize_key "$key" || exit 1
|
||||
}
|
||||
|
||||
# Only called after a key is authorized. Disabling password authentication
|
||||
# before then could lock the owner out of the machine.
|
||||
disable_password_auth() {
|
||||
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
local effective_config
|
||||
|
||||
if [[ ! -s $AUTHORIZED_KEYS ]]; then
|
||||
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication, now that a key is authorized..."
|
||||
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
|
||||
# Validate before reloading: a config sshd rejects would otherwise take the
|
||||
# service down on its next restart, potentially stranding a remote owner.
|
||||
if ! sudo sshd -t; then
|
||||
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads for
|
||||
# these settings. An earlier administrator rule could leave passwords enabled.
|
||||
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
|
||||
# in CamelCase.
|
||||
if ! effective_config=$(sudo sshd -T) ||
|
||||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Reload rather than restart so an administrator already connected keeps
|
||||
# their session.
|
||||
sudo systemctl reload sshd.service
|
||||
}
|
||||
|
||||
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
|
||||
|
||||
setup_sshd
|
||||
@@ -123,5 +167,8 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
disable_password_auth
|
||||
|
||||
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
|
||||
echo "Password logins are off; this machine now accepts authorized keys only."
|
||||
echo "You can now connect with: ssh $USER@$(hostname)"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Enable sudoless Docker by adding your user to the docker group (root-equivalent!)
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
# Ask about the configured groups, not this session's: once enabled it stays
|
||||
# enabled for the account even before the reboot that lets this session use it.
|
||||
if ! omarchy-sudo-docker --configured; then
|
||||
echo "Sudoless Docker is already enabled: $USER is in the docker group."
|
||||
echo "To disable it again, run: omarchy-remove-security-sudoless-docker"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "⚠️ WARNING: Enabling sudoless Docker adds you to the 'docker' group."
|
||||
echo ""
|
||||
echo "The Docker daemon runs as root, so membership in the docker group is"
|
||||
echo "equivalent to passwordless root. Any process running as your user could"
|
||||
echo "then run, for example:"
|
||||
echo ""
|
||||
echo " docker run -v /:/host alpine # full read/write of the host, as root"
|
||||
echo ""
|
||||
echo "and take over the machine with no password prompt. A single rogue script,"
|
||||
echo "dependency, or plugin running as you is enough. It is convenient for"
|
||||
echo "development, but it removes the protection Omarchy keeps by default, where"
|
||||
echo "Docker access goes through a polkit/sudo prompt."
|
||||
echo ""
|
||||
|
||||
if gum confirm "Enable sudoless Docker? This gives anything running as you passwordless root."; then
|
||||
sudo usermod -aG docker "$USER"
|
||||
# A new docker group membership is only picked up by a fresh session, and in
|
||||
# practice logging out or newgrp isn't enough — only a reboot reliably applies
|
||||
# it. Record it so a later `omarchy update` still prompts
|
||||
# (omarchy-update-restart reads this), then offer to do it now.
|
||||
omarchy-state set reboot-required
|
||||
echo ""
|
||||
echo "Sudoless Docker ENABLED. It takes effect after a reboot."
|
||||
echo "To disable it again: Setup > Security > Sudoless Docker."
|
||||
echo ""
|
||||
# The migration reuses this command during 'omarchy update' and defers the
|
||||
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||
omarchy-system-reboot
|
||||
fi
|
||||
else
|
||||
echo "Aborted. No changes made. Docker access still goes through a prompt."
|
||||
fi
|
||||
+15
-3
@@ -1,6 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Display a "Done!" message with a spinner and wait for user to press any key.
|
||||
# omarchy:summary=Display a "Done!" message and wait for user to press any key.
|
||||
|
||||
echo
|
||||
gum spin --spinner "globe" --title "Done! Press any key to close..." -- bash -c 'read -n 1 -s'
|
||||
# The device node is there whether or not a terminal is behind it, so opening
|
||||
# it is the only test that means anything.
|
||||
: 2>/dev/null <>/dev/tty || exit 0
|
||||
|
||||
# gum 2.0 no longer lets a spun command read the terminal, so the wait has to
|
||||
# happen here. Its own query replies are still queued on the tty; drop those or
|
||||
# they answer the keypress for the user.
|
||||
while read -rsn 1 -t 0.1 _ </dev/tty; do :; done
|
||||
|
||||
# Prompt on the terminal rather than stdout, or a caller that redirects us
|
||||
# leaves the user waiting on a prompt they were never shown.
|
||||
printf '\n\033[32m● \033[0mDone! Press any key to close...' >/dev/tty
|
||||
read -rsn 1 </dev/tty
|
||||
echo >/dev/tty
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Succeed when Docker needs sudo, fail when it can be used directly
|
||||
# omarchy:args=[--configured]
|
||||
# omarchy:examples=omarchy-sudo-docker && echo "needs sudo" | omarchy-sudo-docker --configured
|
||||
# omarchy:hidden=true
|
||||
|
||||
# The docker group is root-equivalent, so Omarchy leaves users out of it by
|
||||
# default and reaches the daemon through a prompt instead. Everything that has
|
||||
# to make that choice asks here rather than testing group membership itself.
|
||||
#
|
||||
# Two questions, because they have different answers between toggling sudoless
|
||||
# Docker and the reboot that applies it (group membership is fixed when the
|
||||
# session is created):
|
||||
#
|
||||
# (default) Does Docker need sudo *right now*? Answered by whether this
|
||||
# process can actually reach the socket, which is what decides
|
||||
# if a command must elevate. Still true in the window after
|
||||
# sudoless Docker is enabled but before the reboot.
|
||||
# --configured Will it need sudo once the account's groups take effect?
|
||||
# Answered from the account's configured groups, so the menu
|
||||
# offers the toggle that can actually change state.
|
||||
#
|
||||
# Succeeds (exit 0) when sudo is needed, so it reads as `if omarchy-sudo-docker`.
|
||||
|
||||
DOCKER_SOCKET="${OMARCHY_DOCKER_SOCKET:-/var/run/docker.sock}"
|
||||
|
||||
case "${1:-}" in
|
||||
--configured)
|
||||
# An account in the docker group will not need sudo after the next login.
|
||||
id -nG "$USER" 2>/dev/null | grep -qw docker && exit 1
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
# A socket we can write is a daemon we can drive without elevating. A missing
|
||||
# socket counts as needing sudo: reaching it means starting it as root anyway.
|
||||
[[ -w $DOCKER_SOCKET ]] && exit 1
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-sudo-docker [--configured]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -13,6 +13,19 @@ if [[ $1 && ! $1 =~ ^[0-9]+$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
arm_expiry() {
|
||||
if sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \
|
||||
rm -f -- "$NOPASSWD_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Failed to schedule passwordless sudo expiry. Revoking access now." >&2
|
||||
if ! sudo rm -f -- "$NOPASSWD_FILE"; then
|
||||
echo "CRITICAL: Could not remove $NOPASSWD_FILE. Remove it as root immediately." >&2
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "Toggle passwordless sudo..."
|
||||
|
||||
# Safety: if the file exists but the timer doesn't (e.g. after reboot), clean up
|
||||
@@ -24,8 +37,7 @@ fi
|
||||
if sudo test -f "$NOPASSWD_FILE"; then
|
||||
if [[ $1 ]]; then
|
||||
sudo systemctl stop "${TIMER_NAME}.timer" 2>/dev/null
|
||||
sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \
|
||||
rm "$NOPASSWD_FILE"
|
||||
arm_expiry || exit 1
|
||||
echo "Passwordless sudo timer updated. It will now automatically disable in ${MINUTES} minutes."
|
||||
else
|
||||
sudo rm "$NOPASSWD_FILE"
|
||||
@@ -48,12 +60,11 @@ else
|
||||
if gum confirm "Enable passwordless sudo for ${MINUTES} minutes? This is a significant security risk!"; then
|
||||
echo "${USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee "$NOPASSWD_FILE" > /dev/null
|
||||
sudo chmod 440 "$NOPASSWD_FILE"
|
||||
sudo systemd-run --on-active=${MINUTES}m --timer-property=AccuracySec=1s --unit="$TIMER_NAME" \
|
||||
rm "$NOPASSWD_FILE"
|
||||
arm_expiry || exit 1
|
||||
|
||||
echo ""
|
||||
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${MINUTES} minutes."
|
||||
echo "Note: if you restart before then, run omarchy-sudo-passwordless again to disable it."
|
||||
echo "A restart removes the passwordless sudo rule as well."
|
||||
else
|
||||
echo "Aborted. No changes made."
|
||||
fi
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Reset the sudo lockout/faillock for the current user.
|
||||
|
||||
su -c "faillock --reset --user $USER"
|
||||
@@ -59,7 +59,7 @@ claim_path() {
|
||||
announce() {
|
||||
local path="$1"
|
||||
local name="${path##*/}"
|
||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}")
|
||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical)
|
||||
|
||||
case "${name,,}" in
|
||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||
@@ -72,7 +72,7 @@ announce() {
|
||||
|
||||
# Announcing is best-effort: the file is already delivered, and under `set -e`
|
||||
# a notification outage would otherwise kill the long-running receiver.
|
||||
omarchy-notification-send "${args[@]}" --exec "$(printf 'xdg-open %q' "$path")" || true
|
||||
omarchy-notification-send "${args[@]}" --exec xdg-open "$path" || true
|
||||
}
|
||||
|
||||
deliver() {
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=List the user-installed themes that came from a git clone
|
||||
# omarchy:examples=omarchy theme extras
|
||||
|
||||
# Exits nonzero when there are none, so a caller can ask whether any exist
|
||||
# without reading the list. A symlinked theme is someone's working copy and a
|
||||
# `.git` file is a worktree pointing elsewhere; neither is ours to pull.
|
||||
status=1
|
||||
|
||||
for theme in ~/.config/omarchy/themes/*; do
|
||||
[[ ! -L $theme && -d $theme/.git ]] || continue
|
||||
echo "$theme"
|
||||
status=0
|
||||
done
|
||||
|
||||
exit $status
|
||||
@@ -16,24 +16,48 @@ if [[ -z $REPO_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Refuse a URL that names a git option or a transport helper before cloning. The
|
||||
# check is shared with omarchy-plugin-add and explains itself; a missing checker
|
||||
# leaves this non-zero, which refuses the URL rather than cloning it.
|
||||
omarchy-git-url-check "$REPO_URL" || exit 1
|
||||
|
||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||
|
||||
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path
|
||||
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path.
|
||||
# git reads a URL as scp-style when a colon appears before any slash, so the path
|
||||
# after it need not hold one: `git@host:omarchy-blue-theme.git` is a repo in that
|
||||
# user's home, and leaving its prefix on names the theme after the whole URL.
|
||||
REPO_PATH="$REPO_URL"
|
||||
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}"
|
||||
THEME_NAME=$(basename "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
||||
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:* && ${REPO_PATH%%:*} != */* ]] && REPO_PATH="${REPO_PATH#*:}"
|
||||
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||
|
||||
# The name comes from the URL, is joined into a path that is about to be
|
||||
# removed, and then names a directory the rest of Omarchy passes around by
|
||||
# name: Style > Unlock builds a command line out of the one the picker
|
||||
# returned. So it is held to the characters a theme name needs rather than
|
||||
# screened for the harm of the day -- a repo called `..` would take
|
||||
# ~/.config/omarchy with it, and one called `a';'id` would carry its own
|
||||
# command into that picker. The leading character is kept out of `.` and `-`,
|
||||
# which also covers `host:-s/foo.git` leaving basename with `.git`.
|
||||
# A bracket range follows the locale's collation, not ASCII: `[a-z]` takes in
|
||||
# `é` under en_US.UTF-8. Pin the locale so the set is the one written here.
|
||||
if ! (LC_ALL=C; [[ $THEME_NAME =~ ^[a-z0-9_][a-z0-9._+-]*$ ]]); then
|
||||
echo "Error: '$REPO_URL' does not give a usable theme name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove existing theme if present
|
||||
if [[ -d $THEME_PATH ]]; then
|
||||
rm -rf "$THEME_PATH"
|
||||
fi
|
||||
|
||||
# Clone the repo directly to ~/.config/omarchy/themes
|
||||
if ! git clone "$REPO_URL" "$THEME_PATH"; then
|
||||
if ! git clone -- "$REPO_URL" "$THEME_PATH"; then
|
||||
echo "Error: Failed to clone theme repo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Apply the new theme with omarchy-theme-set
|
||||
omarchy-theme-set $THEME_NAME
|
||||
# Apply the new theme with omarchy-theme-set, which stages only the files an
|
||||
# extra theme is allowed to contribute and names anything it dropped.
|
||||
omarchy-theme-set "$THEME_NAME"
|
||||
@@ -21,8 +21,9 @@ fi
|
||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||
|
||||
# Ensure a theme was set
|
||||
if [[ -z $THEME_NAME ]]; then
|
||||
# Ensure a theme was set, and that the name cannot climb out of THEMES_DIR
|
||||
# on its way into the rm below.
|
||||
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+132
-2
@@ -17,6 +17,19 @@ THEME_SET_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
|
||||
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
|
||||
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
|
||||
|
||||
# What a theme installed from a git repo may not ship, because these run code.
|
||||
# Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim
|
||||
# loads its neovim.lua at startup, so no .lua from such a theme is staged at all.
|
||||
# Each terminal config names the program the terminal launches, and vscode.json
|
||||
# names an extension omarchy-theme-set-vscode installs, a VS Code extension being
|
||||
# arbitrary JavaScript. Everything else a theme ships is colour and is kept.
|
||||
#
|
||||
# Adding a template for another terminal, or for another editor that loads Lua,
|
||||
# means adding it here. test/shell.d/theme-staging-test.sh fails on a generated
|
||||
# theme file that is neither denied here nor recorded there as colour-only.
|
||||
INSTALLED_THEME_DENIED=(alacritty.toml foot.ini ghostty.conf kitty.conf vscode.json)
|
||||
IGNORED_THEME_FILES=()
|
||||
|
||||
run_parallel() {
|
||||
local pid
|
||||
local pids=()
|
||||
@@ -122,12 +135,121 @@ set_theme_background() {
|
||||
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
|
||||
}
|
||||
|
||||
is_denied_installed_file() {
|
||||
local name="$1"
|
||||
local denied
|
||||
|
||||
[[ $name == *.lua ]] && return 0
|
||||
|
||||
for denied in "${INSTALLED_THEME_DENIED[@]}"; do
|
||||
[[ $name == "$denied" ]] && return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Themes older than colors.toml still get their palette, but their
|
||||
# alacritty.toml never reaches the staged theme: an Alacritty config names the
|
||||
# program the terminal launches.
|
||||
stage_installed_colors_from_alacritty() {
|
||||
local source="$1"
|
||||
local scratch
|
||||
|
||||
if [[ -f $NEXT_THEME_PATH/colors.toml ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -f $source/alacritty.toml || -L $source/alacritty.toml ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
cp "$source/alacritty.toml" "$scratch/alacritty.toml"
|
||||
omarchy-theme-colors-from-alacritty "$scratch"
|
||||
|
||||
if [[ -f $scratch/colors.toml ]]; then
|
||||
cp "$scratch/colors.toml" "$NEXT_THEME_PATH/colors.toml"
|
||||
fi
|
||||
|
||||
rm -rf "$scratch"
|
||||
}
|
||||
|
||||
# Copies a directory without ever following a symlink: in an installed theme one
|
||||
# points wherever the theme author chose, which is how an unlock.png becomes a
|
||||
# copy of any file the session can read.
|
||||
stage_installed_dir() {
|
||||
local source="$1"
|
||||
local dest="$2"
|
||||
local entry name
|
||||
|
||||
mkdir -p "$dest"
|
||||
|
||||
for entry in "$source"/*; do
|
||||
[[ -e $entry && ! -L $entry ]] || continue
|
||||
name=${entry##*/}
|
||||
|
||||
if [[ -d $entry ]]; then
|
||||
stage_installed_dir "$entry" "$dest/$name"
|
||||
else
|
||||
cp "$entry" "$dest/$name"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# `omarchy theme install` clones into ~/.config/omarchy/themes, so a .git
|
||||
# directory there means the contents came from a stranger and are held to the
|
||||
# list above. A directory the user wrote themselves, and a symlink to their own
|
||||
# working copy, are theirs to fill however they like -- the same distinction
|
||||
# omarchy-theme-extras draws when it decides which themes it may pull.
|
||||
theme_came_from_a_repo() {
|
||||
local source="$1"
|
||||
|
||||
[[ ! -L $source && -d $source/.git ]]
|
||||
}
|
||||
|
||||
stage_installed_theme() {
|
||||
local source="$1"
|
||||
local entry name
|
||||
|
||||
[[ -d $source ]] || return 0
|
||||
|
||||
for entry in "$source"/*; do
|
||||
[[ -e $entry ]] || continue
|
||||
name=${entry##*/}
|
||||
|
||||
if [[ -L $entry ]] || is_denied_installed_file "$name"; then
|
||||
case "${name,,}" in
|
||||
readme* | license* | changelog* | *.md | *.txt) ;;
|
||||
*) IGNORED_THEME_FILES+=("$name") ;;
|
||||
esac
|
||||
elif [[ -d $entry ]]; then
|
||||
stage_installed_dir "$entry" "$NEXT_THEME_PATH/$name"
|
||||
else
|
||||
cp "$entry" "$NEXT_THEME_PATH/$name"
|
||||
fi
|
||||
done
|
||||
|
||||
stage_installed_colors_from_alacritty "$source"
|
||||
}
|
||||
|
||||
report_ignored_theme_files() {
|
||||
(( ${#IGNORED_THEME_FILES[@]} > 0 )) || return 0
|
||||
|
||||
echo "Ignored in $USER_THEMES_PATH/$THEME_NAME: ${IGNORED_THEME_FILES[*]}" >&2
|
||||
echo "A theme installed from a git repo cannot supply Lua, a terminal config, or vscode.json." >&2
|
||||
}
|
||||
|
||||
THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||
THEME_HEADLESS=0
|
||||
if [[ ${OMARCHY_THEME_HEADLESS:-} == "1" || ${OMARCHY_THEME_OFFLINE:-} == "1" ]]; then
|
||||
THEME_HEADLESS=1
|
||||
fi
|
||||
|
||||
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||
echo "Invalid theme name: $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THEME_NAME ]]; then
|
||||
echo "Theme '$THEME_NAME' does not exist"
|
||||
exit 1
|
||||
@@ -143,9 +265,15 @@ flock 9
|
||||
rm -rf "$NEXT_THEME_PATH"
|
||||
mkdir -p "$NEXT_THEME_PATH"
|
||||
|
||||
# Copy official theme first, then overlay user customizations on top
|
||||
# Copy official theme first, then overlay the user's theme on top
|
||||
cp -r "$OMARCHY_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
||||
cp -r "$USER_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
||||
|
||||
if theme_came_from_a_repo "$USER_THEMES_PATH/$THEME_NAME"; then
|
||||
stage_installed_theme "$USER_THEMES_PATH/$THEME_NAME"
|
||||
report_ignored_theme_files
|
||||
else
|
||||
cp -r "$USER_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Generate colors.toml from alacritty.toml if theme is missing colors.toml
|
||||
if [[ ! -f $NEXT_THEME_PATH/colors.toml && -f $NEXT_THEME_PATH/alacritty.toml ]]; then
|
||||
@@ -198,6 +326,8 @@ post_theme_commands=(
|
||||
omarchy-theme-set-gnome
|
||||
omarchy-theme-set-pi
|
||||
omarchy-theme-set-claude
|
||||
omarchy-theme-set-hermes
|
||||
omarchy-theme-set-t3code
|
||||
omarchy-theme-set-browser
|
||||
omarchy-theme-set-vscode
|
||||
omarchy-theme-set-obsidian
|
||||
|
||||
@@ -3,23 +3,15 @@
|
||||
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
|
||||
# omarchy:hidden=true
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
|
||||
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
|
||||
|
||||
if [[ -f $CHROMIUM_THEME ]]; then
|
||||
THEME_RGB_COLOR=$(<$CHROMIUM_THEME)
|
||||
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
|
||||
else
|
||||
# Use a default, neutral grey if theme doesn't have a color
|
||||
THEME_HEX_COLOR="#1c2027"
|
||||
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
|
||||
fi
|
||||
|
||||
set_browser_policy() {
|
||||
local policy_dir="$1"
|
||||
|
||||
[[ -d $policy_dir ]] || return
|
||||
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
|
||||
}
|
||||
|
||||
refresh_running_browser() {
|
||||
local process="$1"
|
||||
local command="$2"
|
||||
@@ -30,17 +22,15 @@ refresh_running_browser() {
|
||||
fi
|
||||
}
|
||||
|
||||
set_browser_policy /etc/chromium/policies/managed
|
||||
failed=0
|
||||
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
|
||||
|
||||
refresh_running_browser chromium chromium
|
||||
|
||||
set_browser_policy /etc/opt/chrome/policies/managed
|
||||
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
|
||||
|
||||
set_browser_policy /etc/opt/edge/policies/managed
|
||||
refresh_running_browser msedge microsoft-edge-stable
|
||||
|
||||
set_browser_policy /etc/brave/policies/managed
|
||||
refresh_running_browser brave brave
|
||||
# Match on the binary path: the running process is named plain "brave", and a
|
||||
# bare -f brave-origin pattern would also match the installer's own terminal.
|
||||
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
|
||||
|
||||
exit "$failed"
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Write the current theme color into the browser policy directories
|
||||
# omarchy:args=<rrggbb>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Whenever this runs as root — invoked directly through the passwordless
|
||||
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
|
||||
# never an omarchy-* command, so pin PATH to trusted system directories and keep
|
||||
# root from resolving one out of that checkout. The unprivileged wrapper phase
|
||||
# keeps the caller's PATH so it can still find sudo/pkexec.
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||
fi
|
||||
|
||||
# Enterprise policy trust roots. The list is fixed here rather than taken from
|
||||
# the caller: the caller chooses a color, never a path.
|
||||
POLICY_DIRS=(
|
||||
/etc/chromium/policies/managed
|
||||
/etc/opt/chrome/policies/managed
|
||||
/etc/opt/edge/policies/managed
|
||||
/etc/brave/policies/managed
|
||||
)
|
||||
|
||||
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
|
||||
# runs from there rather than from whichever copy was invoked, so the rule
|
||||
# matches even where $OMARCHY_PATH points at a checkout.
|
||||
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
|
||||
}
|
||||
|
||||
if (( $# != 1 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
color="$1"
|
||||
|
||||
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
|
||||
# is added when the JSON is written rather than passed in: "#" opens a comment
|
||||
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
|
||||
# argument as a plain six-character glob.
|
||||
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
|
||||
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# True when sudo would run this exact command without stopping for a password.
|
||||
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||
# entry's tags, so !authenticate is the grant in
|
||||
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
|
||||
# and, under -n, prompts for nothing.
|
||||
sudo_grants_passwordless() {
|
||||
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if (( EUID == 0 )); then
|
||||
return
|
||||
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||
exec sudo "$PACKAGED_PATH" "$@"
|
||||
else
|
||||
exec pkexec "$PACKAGED_PATH" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
require_root "$color"
|
||||
|
||||
failed=0
|
||||
staged=""
|
||||
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
|
||||
# so this handler must not end on a false test. Every successful run clears
|
||||
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
|
||||
cleanup() {
|
||||
if [[ -n $staged ]]; then
|
||||
rm -f "$staged"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for policy_dir in "${POLICY_DIRS[@]}"; do
|
||||
# Only browsers Omarchy has installed have a policy directory. Creating one
|
||||
# here would hand a browser a managed-policy root it does not otherwise have.
|
||||
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
|
||||
|
||||
dest=$policy_dir/color.json
|
||||
staged=$(mktemp) || {
|
||||
failed=1
|
||||
continue
|
||||
}
|
||||
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
|
||||
|
||||
if [[ -L $dest || -d $dest ]]; then
|
||||
if ! rm -rf -- "$dest"; then
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
done
|
||||
|
||||
exit "$failed"
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Sync the generated Omarchy theme to Hermes as a skin
|
||||
# omarchy:args=[--activate] [--wait]
|
||||
# omarchy:hidden=true
|
||||
|
||||
# A skin is Hermes' one theme unit for the desktop app, the TUI and the CLI;
|
||||
# its gateway watches the active skin file and repaints every surface on change.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HERMES_SOURCE_PATH="$HOME/.local/state/omarchy/current/theme/hermes.yaml"
|
||||
HERMES_THEME_NAME_PATH="$HOME/.local/state/omarchy/current/theme.name"
|
||||
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||
HERMES_CONFIG_PATH="$HERMES_HOME/config.yaml"
|
||||
HERMES_SKIN_NAME="omarchy"
|
||||
# The command the readiness probe vets, rather than whichever hermes is on PATH.
|
||||
HERMES_COMMAND="$HOME/.local/bin/hermes"
|
||||
# Written by the desktop app once the runtime its first launch provisions is in.
|
||||
HERMES_BOOTSTRAP_MARKER="$HERMES_HOME/hermes-agent/.hermes-bootstrap-complete"
|
||||
HERMES_ACTIVATE=0
|
||||
HERMES_WAIT=0
|
||||
HERMES_WAIT_LIMIT=$((30 * 60))
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-theme-set-hermes [--activate] [--wait]"
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--activate)
|
||||
HERMES_ACTIVATE=1
|
||||
;;
|
||||
--wait)
|
||||
HERMES_ACTIVATE=1
|
||||
HERMES_WAIT=1
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# A theme switch runs this beside a dozen other hooks; only --activate explains.
|
||||
note() {
|
||||
if (( HERMES_ACTIVATE == 1 )); then
|
||||
echo "$*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# A theme applied before the template existed has no skin rendered yet.
|
||||
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
|
||||
(( HERMES_ACTIVATE == 1 )) || exit 0
|
||||
|
||||
if [[ ! -s $HERMES_THEME_NAME_PATH ]]; then
|
||||
echo "Hermes skin source missing: $HERMES_SOURCE_PATH" >&2
|
||||
echo "Select an Omarchy theme first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
omarchy-theme-refresh
|
||||
|
||||
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
|
||||
echo "Hermes skin source missing after refreshing the theme: $HERMES_SOURCE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# The first launch takes minutes and may be abandoned; the readiness probe
|
||||
# would start Hermes to answer, so poll for the marker instead.
|
||||
if (( HERMES_WAIT == 1 )); then
|
||||
waited=0
|
||||
until [[ -f $HERMES_BOOTSTRAP_MARKER && -f $HERMES_CONFIG_PATH ]]; do
|
||||
if (( waited >= HERMES_WAIT_LIMIT )); then
|
||||
echo "Hermes did not finish setting up within $((HERMES_WAIT_LIMIT / 60)) minutes; run omarchy-theme-set-hermes --activate once it has." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
waited=$((waited + 10))
|
||||
done
|
||||
fi
|
||||
|
||||
# Provisioning creates ~/.hermes on every machine for the Omarchy skill; the
|
||||
# config is what Hermes writes once it has actually run.
|
||||
if [[ ! -f $HERMES_CONFIG_PATH ]]; then
|
||||
note "Hermes is not set up yet; launch it once, then run omarchy-theme-set-hermes --activate."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Hermes parses the skin as YAML and hands its strings to every surface, so only
|
||||
# the name, a plain description and #rrggbb colours may reach it, in the order
|
||||
# YAML needs them. YAML breaks lines on bytes grep does not, so the bytes are
|
||||
# counted first, NUL included.
|
||||
skin_is_well_formed() {
|
||||
local skin="$1"
|
||||
|
||||
[[ -f $skin ]] &&
|
||||
(( $(LC_ALL=C tr -d ' -~\n' <"$skin" | wc -c) == 0 )) &&
|
||||
awk -v name="name: $HERMES_SKIN_NAME" '
|
||||
bad { next }
|
||||
/^#/ || /^[[:space:]]*$/ { next }
|
||||
!seen_name { if ($0 == name) seen_name = 1; else bad = 1; next }
|
||||
!seen_colors {
|
||||
if ($0 == "colors:") seen_colors = 1
|
||||
else if (!seen_description && $0 ~ /^description: [A-Za-z0-9 ,.()-]{0,200}$/) seen_description = 1
|
||||
else bad = 1
|
||||
next
|
||||
}
|
||||
/^ [a-z_]{1,64}: "#[0-9a-fA-F]{6}"$/ { colors++; next }
|
||||
{ bad = 1 }
|
||||
END { exit (bad || !seen_colors || colors == 0) }
|
||||
' "$skin"
|
||||
}
|
||||
|
||||
# The check has to cover the bytes that get published, and the theme can change
|
||||
# underneath between the two, so a private copy is taken and that is checked.
|
||||
snapshot_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$snapshot_dir"' EXIT
|
||||
HERMES_SNAPSHOT="$snapshot_dir/$HERMES_SKIN_NAME.yaml"
|
||||
|
||||
take_snapshot() {
|
||||
cp "$HERMES_SOURCE_PATH" "$HERMES_SNAPSHOT" 2>/dev/null && skin_is_well_formed "$HERMES_SNAPSHOT"
|
||||
}
|
||||
|
||||
if ! take_snapshot; then
|
||||
echo "Skipping Hermes skin: $(basename "$HERMES_SOURCE_PATH") is not a plain color palette." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The gateway reads the file whole on an mtime change, so the write is atomic;
|
||||
# -T so a directory at the skin's path is an error rather than a destination.
|
||||
publish_skin() {
|
||||
local skins_dir="$1"
|
||||
local tmp
|
||||
|
||||
mkdir -p "$skins_dir" 2>/dev/null || return 1
|
||||
tmp=$(mktemp "$skins_dir/$HERMES_SKIN_NAME.yaml.XXXXXX" 2>/dev/null) || return 1
|
||||
if ! cp "$HERMES_SNAPSHOT" "$tmp" 2>/dev/null || ! mv -T "$tmp" "$skins_dir/$HERMES_SKIN_NAME.yaml" 2>/dev/null; then
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# A profile is a Hermes home of its own; existing ones get the skin, none are
|
||||
# made, and one that cannot take it does not cost the others.
|
||||
publish_skin_everywhere() {
|
||||
local profile
|
||||
|
||||
publish_skin "$HERMES_HOME/skins" || {
|
||||
echo "Could not publish the Hermes skin to $HERMES_HOME/skins." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
for profile in "$HERMES_HOME"/profiles/*/; do
|
||||
[[ -d $profile ]] || continue
|
||||
publish_skin "${profile%/}/skins" || note "Could not publish the skin to the Hermes profile $(basename "$profile")."
|
||||
done
|
||||
}
|
||||
|
||||
publish_skin_everywhere
|
||||
|
||||
# Hermes reads the config of the profile named in active_profile; the profile
|
||||
# exists once its directory does, with or without a config of its own.
|
||||
active_config_path() {
|
||||
local profile
|
||||
|
||||
profile=$(cat "$HERMES_HOME/active_profile" 2>/dev/null || true)
|
||||
profile=${profile,,}
|
||||
|
||||
if [[ -n $profile && $profile != "default" && -d $HERMES_HOME/profiles/$profile ]]; then
|
||||
echo "$HERMES_HOME/profiles/$profile/config.yaml"
|
||||
else
|
||||
echo "$HERMES_CONFIG_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
# A theme switch finishes the hand-over only for the app Omarchy installed, and
|
||||
# only Hermes' default is ever replaced, so a config plainly naming another skin
|
||||
# ends it here without starting Hermes. Anything less plain is for Hermes to read.
|
||||
if (( HERMES_ACTIVATE == 0 )); then
|
||||
omarchy-pkg-present hermes-desktop || exit 0
|
||||
|
||||
skin_line=$(grep -m1 -x ' skin: .*' "$(active_config_path)" 2>/dev/null || true)
|
||||
case "${skin_line# skin: }" in
|
||||
"" | default | null | true | false | *[!A-Za-z0-9_-]*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Omarchy's cold stub installs Hermes when run, so ask the probe before running it.
|
||||
if ! omarchy-install-hermes-cli --check 2>/dev/null; then
|
||||
note "Hermes is not ready, so the Omarchy skin is published but not active."
|
||||
note "Once Hermes runs, activate it with: hermes config set display.skin $HERMES_SKIN_NAME"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only Hermes' own default is replaced, so a skin chosen in Hermes stays; an
|
||||
# answer that did not come is not a default.
|
||||
if ! current_skin=$(timeout 15 "$HERMES_COMMAND" config get display.skin 2>/dev/null); then
|
||||
note "Hermes did not say which skin it is on, so the Omarchy skin is published but not active."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n $current_skin && $current_skin != "default" && $current_skin != "$HERMES_SKIN_NAME" ]]; then
|
||||
note "Hermes is set to the '$current_skin' skin; leaving it. Switch with: hermes config set display.skin $HERMES_SKIN_NAME"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Hermes' own writer: it updates the active profile's config and touches the
|
||||
# skin file so a running gateway broadcasts the change. A refusal is cosmetic.
|
||||
if ! timeout 30 "$HERMES_COMMAND" config set display.skin "$HERMES_SKIN_NAME" >/dev/null 2>&1; then
|
||||
note "Hermes refused to switch skins, so the Omarchy skin is published but not active."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
note "Hermes is on the Omarchy skin."
|
||||
|
||||
# The desktop applies a skin only from a broadcast, and a config written before
|
||||
# the gateway seeded its watcher goes unannounced; a later write is announced.
|
||||
# The theme may have changed underneath in the meantime, so it is checked again.
|
||||
if (( HERMES_WAIT == 1 )); then
|
||||
sleep 60
|
||||
|
||||
if take_snapshot; then
|
||||
publish_skin_everywhere
|
||||
fi
|
||||
fi
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Sync the generated Omarchy theme to T3 Code
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
T3CODE_SOURCE_PATH="$HOME/.local/state/omarchy/current/theme/t3code.json"
|
||||
T3CODE_HOME="${T3CODE_HOME:-$HOME/.t3}"
|
||||
T3CODE_STATE_DIR="$T3CODE_HOME/userdata"
|
||||
# The filename is the theme id T3 Code shows and `t3 theme set` accepts, and
|
||||
# it stays stable while the colors change underneath on every theme switch.
|
||||
T3CODE_THEME_PATH="$T3CODE_STATE_DIR/themes/omarchy.json"
|
||||
|
||||
[[ -f $T3CODE_SOURCE_PATH ]] || exit 0
|
||||
|
||||
# A theme without an accent or a background leaves its placeholder unresolved.
|
||||
# Publishing that would only make T3 Code reject the file, so skip instead and
|
||||
# leave the previous palette in place.
|
||||
if grep -q '{{' "$T3CODE_SOURCE_PATH"; then
|
||||
echo "Skipping T3 Code theme: $(basename "$T3CODE_SOURCE_PATH") has unresolved colors." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only follow an install that already exists. Creating the directory here would
|
||||
# leave a stray T3 Code state dir on machines that do not run it.
|
||||
[[ -d $T3CODE_STATE_DIR ]] || exit 0
|
||||
|
||||
# T3 Code watches this file and retints every connected client on change, so
|
||||
# the write has to be atomic: a half-written file would read as invalid.
|
||||
mkdir -p "$(dirname "$T3CODE_THEME_PATH")"
|
||||
tmp=$(mktemp "$T3CODE_THEME_PATH.XXXXXX")
|
||||
cp "$T3CODE_SOURCE_PATH" "$tmp"
|
||||
mv "$tmp" "$T3CODE_THEME_PATH"
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
# omarchy:summary=Update user-installed git themes
|
||||
|
||||
for dir in ~/.config/omarchy/themes/*/; do
|
||||
if [[ -d $dir ]] && [[ ! -L ${dir%/} ]] && [[ -d $dir/.git ]]; then
|
||||
echo "Updating: $(basename "$dir")"
|
||||
git -C "$dir" pull
|
||||
fi
|
||||
mapfile -t themes < <(omarchy-theme-extras)
|
||||
|
||||
for theme in "${themes[@]}"; do
|
||||
echo "Updating: $(basename "$theme")"
|
||||
git -C "$theme" pull
|
||||
done
|
||||
@@ -5,3 +5,9 @@
|
||||
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
|
||||
|
||||
omarchy-toggle bar-off "${1:-toggle}"
|
||||
|
||||
# The shell's watch on the toggles directory can miss flag changes that land in
|
||||
# quick succession, stranding the bar off screen until the shell restarts.
|
||||
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
|
||||
# works when the shell is not up.
|
||||
omarchy-shell -q omarchy.bar syncHidden
|
||||
@@ -3,6 +3,35 @@
|
||||
# omarchy:summary=Toggle dedicated vs integrated GPU mode via supergfxd (for hybrid gpu laptops, like Asus G14).
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
install_root_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="$3"
|
||||
local stage
|
||||
|
||||
stage=$(sudo /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1
|
||||
safe_stage_path "$stage" "$destination" || return 1
|
||||
|
||||
if sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" &&
|
||||
sudo /usr/bin/mv -Tf -- "$stage" "$destination"; then
|
||||
return 0
|
||||
else
|
||||
safe_stage_path "$stage" "$destination" && sudo /usr/bin/rm -f -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
safe_stage_path() {
|
||||
local stage="$1"
|
||||
local destination="$2"
|
||||
local prefix suffix
|
||||
|
||||
prefix="${destination%/*}/.${destination##*/}.omarchy."
|
||||
[[ $stage == "$prefix"* ]] || return 1
|
||||
suffix=${stage#"$prefix"}
|
||||
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||
}
|
||||
|
||||
if omarchy-cmd-missing supergfxctl; then
|
||||
omarchy-pkg-add supergfxctl
|
||||
|
||||
@@ -54,18 +83,31 @@ case "$gpu_mode" in
|
||||
;;
|
||||
"Hybrid")
|
||||
if gum confirm "Use only integrated GPU and reboot?"; then
|
||||
# Switch to integrated mode and ensure vfio is enabled (needed for sleep/wake trick)
|
||||
sudo sed -i "s/\"mode\": \".*\"/\"mode\": \"Integrated\"/" /etc/supergfxd.conf
|
||||
sudo sed -i 's/"vfio_enable": false/"vfio_enable": true/' /etc/supergfxd.conf
|
||||
|
||||
# Force igpu mode after system sleep (or dgpu could get activated)
|
||||
sudo mkdir -p /usr/lib/systemd/system-sleep
|
||||
sudo cp -p "$OMARCHY_PATH/default/systemd/system-sleep/force-igpu" /usr/lib/systemd/system-sleep/
|
||||
|
||||
# Delay supergfxd startup to avoid race condition with display manager
|
||||
# that can cause system freeze when booting in Integrated mode
|
||||
sudo mkdir -p /etc/systemd/system/supergfxd.service.d
|
||||
sudo cp -p "$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf" /etc/systemd/system/supergfxd.service.d/
|
||||
if ! install_root_file "$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf" \
|
||||
/etc/systemd/system/supergfxd.service.d/delay-start.conf 0644; then
|
||||
echo "Could not install the supergfxd startup-delay override" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Publish the self-guarding sleep hook before enabling Integrated mode. It
|
||||
# remains inert while the config says Hybrid, so any failed step is safe to
|
||||
# retry without leaving the GPU config partially switched.
|
||||
sudo mkdir -p /usr/lib/systemd/system-sleep
|
||||
if ! install_root_file "$OMARCHY_PATH/default/systemd/system-sleep/force-igpu" \
|
||||
/usr/lib/systemd/system-sleep/force-igpu 0755; then
|
||||
echo "Could not install the force-igpu system-sleep hook" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Switch both settings in one atomic config rewrite only after every
|
||||
# supporting file has been installed successfully.
|
||||
sudo sed -i \
|
||||
-e 's/"mode": ".*"/"mode": "Integrated"/' \
|
||||
-e 's/"vfio_enable": false/"vfio_enable": true/' \
|
||||
/etc/supergfxd.conf
|
||||
|
||||
omarchy-system-reboot
|
||||
fi
|
||||
|
||||
@@ -7,44 +7,70 @@
|
||||
KIND="${1:-}"
|
||||
ACTION="${2:-toggle}"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
case "$KIND" in
|
||||
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
||||
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Hyprland sources this directory on reload, so the disabled state survives restarts
|
||||
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
|
||||
# The persisted disable is the device name stored as plain data; on every
|
||||
# reload default/hypr/disabled-input-device.lua reads it back and disables the
|
||||
# device. Names come from USB descriptors and must not be interpolated into
|
||||
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
|
||||
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
|
||||
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
|
||||
|
||||
device="$("omarchy-hw-$KIND")"
|
||||
|
||||
if [[ -z $device ]]; then
|
||||
echo "No $KIND device found" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_device() {
|
||||
if [[ -z $device ]]; then
|
||||
echo "No $KIND device found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $device == *[[:cntrl:]]* ]]; then
|
||||
echo "Invalid $KIND device name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
apply_device() {
|
||||
local enabled=$1
|
||||
local quoted=${device//\\/\\\\}
|
||||
quoted=${quoted//\"/\\\"}
|
||||
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
|
||||
}
|
||||
|
||||
enable() {
|
||||
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
|
||||
rm -f "$STATE_FILE"
|
||||
# Clear the persisted state before requiring a usable device, so a device
|
||||
# that stops reporting a valid name can never wedge the disable in place.
|
||||
rm -f "$NAME_FILE"
|
||||
require_device
|
||||
apply_device true
|
||||
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
||||
}
|
||||
|
||||
disable() {
|
||||
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
|
||||
require_device
|
||||
apply_device false
|
||||
mkdir -p "$(dirname "$NAME_FILE")"
|
||||
printf '%s\n' "$device" >"$NAME_FILE"
|
||||
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
on) enable ;;
|
||||
off) disable ;;
|
||||
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
|
||||
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
|
||||
*)
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -175,7 +175,10 @@ target_home=$(getent passwd "$target_user" | cut -d: -f6)
|
||||
[[ -n $target_home && -d $target_home ]] || fail "Home directory for '$target_user' was not found."
|
||||
target_uid=$(id -u "$target_user")
|
||||
target_runtime_dir="/run/user/$target_uid"
|
||||
package_path="/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin:$target_home/.local/bin"
|
||||
# User-local commands are needed only after dropping to the target user. Never
|
||||
# expose their search path to commands run through as_root.
|
||||
root_path=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin
|
||||
package_path="$root_path:$target_home/.local/bin"
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
@@ -597,6 +600,9 @@ preserve_kernel_cmdline_root() {
|
||||
fi
|
||||
|
||||
log "Preserving the kernel cmdline root parameters in $default_conf"
|
||||
# omarchy:heredoc-expands paths=none -- ${boot_params[*]} is the kernel
|
||||
# cmdline fragment assembled above from findmnt output (root=, rootflags=),
|
||||
# not a filesystem path this script controls.
|
||||
as_root tee -a "$default_conf" >/dev/null <<EOF
|
||||
# Written by omarchy-upgrade-to-quattro. The += drop-ins in
|
||||
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
|
||||
@@ -658,7 +664,7 @@ configure_lock_authentication() {
|
||||
as_root env \
|
||||
OMARCHY_INSTALL_USER="$target_user" \
|
||||
OMARCHY_PATH=/usr/share/omarchy \
|
||||
PATH="$package_path" \
|
||||
PATH="$root_path" \
|
||||
"$apply_lock"
|
||||
}
|
||||
|
||||
@@ -1187,11 +1193,19 @@ ensure_sleep_lock_service() {
|
||||
}
|
||||
|
||||
run_post_upgrade_migrations() {
|
||||
PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0
|
||||
local pending_status
|
||||
|
||||
log "Running Omarchy migrations"
|
||||
if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
|
||||
warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
|
||||
fail "Omarchy migrations did not complete. Fix the error above and rerun the upgrade before rebooting."
|
||||
fi
|
||||
if run_as_user_omarchy omarchy-migrate --pending >/dev/null; then
|
||||
fail "Omarchy migrations are still pending. Rerun the upgrade before rebooting."
|
||||
else
|
||||
pending_status=$?
|
||||
if (( pending_status != 1 )); then
|
||||
fail "Could not verify that Omarchy migrations completed. Rerun the upgrade before rebooting."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1276,7 +1290,7 @@ apply_firewall_defaults() {
|
||||
fi
|
||||
|
||||
log "Applying Omarchy firewall defaults"
|
||||
as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$package_path" \
|
||||
as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$root_path" \
|
||||
bash -euo pipefail "$firewall_script" ||
|
||||
warn "Could not apply firewall defaults; run 'sudo bash $firewall_script' after reboot."
|
||||
}
|
||||
@@ -1312,14 +1326,28 @@ apply_system_transition() {
|
||||
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
|
||||
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
|
||||
|
||||
as_root install -d -m 0777 /etc/chromium/policies/managed
|
||||
local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
|
||||
if ! as_root test -f "$browser_policy_helper"; then
|
||||
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
|
||||
else
|
||||
as_root env OMARCHY_PATH=/usr/share/omarchy \
|
||||
bash -euo pipefail -c '
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
browser_policy_setup_dir /etc/chromium/policies/managed
|
||||
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
|
||||
[[ -d $dir || -L $dir ]] || continue
|
||||
browser_policy_setup_dir "$dir"
|
||||
done
|
||||
'
|
||||
fi
|
||||
as_root install -d -m 0755 /usr/lib/chromium
|
||||
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||
printf '%s\n' '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
||||
|
||||
if getent group docker >/dev/null; then
|
||||
as_root usermod -aG docker "$target_user"
|
||||
fi
|
||||
# Deliberately do NOT add the user to the docker group. That group is
|
||||
# root-equivalent, so it is opt-in now (Setup > Security > Sudoless Docker);
|
||||
# re-granting it on upgrade would silently undo that default for everyone.
|
||||
|
||||
if [[ -f /usr/bin/powerprofilesctl ]]; then
|
||||
as_root sed -i '/env python3/ c\#!/bin/python3' /usr/bin/powerprofilesctl || true
|
||||
@@ -1350,7 +1378,6 @@ EOF
|
||||
as_root systemctl disable docker.service >/dev/null 2>&1 || true
|
||||
|
||||
enable_system_service cups.service
|
||||
enable_system_service cups-browsed.service
|
||||
enable_system_service avahi-daemon.service
|
||||
enable_system_service linux-modules-cleanup.service
|
||||
enable_system_service docker.socket
|
||||
@@ -1382,6 +1409,9 @@ EOF
|
||||
autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
|
||||
fi
|
||||
[[ -n ${autologin_user:-} ]] || autologin_user="$target_user"
|
||||
# omarchy:heredoc-expands paths=none -- $autologin_user is a username, read
|
||||
# back from the root-owned drop-in or falling back to $target_user. Same
|
||||
# mechanism as the old getty override: a name expands, no path does.
|
||||
cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
|
||||
[Autologin]
|
||||
User=$autologin_user
|
||||
@@ -1392,6 +1422,8 @@ EOF
|
||||
fi
|
||||
|
||||
as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
|
||||
# omarchy:heredoc-expands paths=none -- $target_user is a username, not a
|
||||
# path; SDDM's state file records who logged in last.
|
||||
cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
|
||||
[Last]
|
||||
Session=omarchy.desktop
|
||||
@@ -1537,7 +1569,6 @@ refresh xournalpp/settings.xml 1c1a9efbf1b6dc7813bf3440fd5bf8409fc283e8d0192ca29
|
||||
# fastfetch/config.jsonc -> /etc/fastfetch/config.jsonc
|
||||
# fontconfig/fonts.conf -> /usr/share/fontconfig/conf.avail/50-omarchy.conf
|
||||
# mimeapps.list -> /usr/share/applications/mimeapps.list
|
||||
# omarchy.ttf -> /usr/share/fonts/omarchy/omarchy.ttf
|
||||
# systemd/user/bt-agent.service -> /usr/lib/systemd/user/bt-agent.service
|
||||
# systemd/user/omarchy-recover-internal-monitor.service -> /usr/lib/systemd/user/omarchy-recover-internal-monitor.service
|
||||
# systemd/user/omarchy-sleep-lock.service -> /usr/lib/systemd/user/omarchy-sleep-lock.service
|
||||
@@ -1570,7 +1601,6 @@ retire fontconfig/fonts.conf 6dec98b539388b95ecfdb3c7ab951001c712820eb0581002832
|
||||
retire fontconfig/fonts.conf 3545f6c5a8c1465df7a4e251b3c2047d4ba03654c86636794f64cb0d8ea8ef63
|
||||
retire fontconfig/fonts.conf 0f085b449f1cbe8eda3b59a6235bf2a3ddb6e982ab5d22fa642248916e63bafc
|
||||
retire mimeapps.list 3b574cef135b5deb7a8a0c7e17139037cf1fe155300e3809f60bed0e04120975
|
||||
retire omarchy.ttf e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966
|
||||
retire systemd/user/bt-agent.service 0406b577a1225dc2a9f86638d3c346eb3635168576f04050be50ebcc0be6be12
|
||||
retire systemd/user/omarchy-recover-internal-monitor.service b9b92cedc44cf3cb6216948629be55b53d16746e31896dc6469fd49ba55e82f4
|
||||
retire systemd/user/omarchy-recover-internal-monitor.service e1483079b9f2aefcd43b4722c75a31643e5f3bb5a2eada5f52f1d7d201e8c289
|
||||
@@ -2306,6 +2336,11 @@ refresh_current_theme_after_upgrade() {
|
||||
# hooks because one of them runs `hyprctl reload`. Still poke terminal
|
||||
# emulators so the active upgrade terminal picks up generated theme files.
|
||||
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
|
||||
|
||||
# apply_system_transition purged user-owned color.json. Headless theme-set
|
||||
# skipped omarchy-theme-set-browser, so rewrite the colour here.
|
||||
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
|
||||
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
|
||||
}
|
||||
|
||||
# Everything below mutates the system, so a non-zero exit from here on leaves a
|
||||
@@ -2340,8 +2375,10 @@ run_as_user_omarchy omarchy-bar defaults ||
|
||||
cleanup_retired_services
|
||||
ensure_sleep_lock_service
|
||||
remove_retired_default_packages
|
||||
run_post_upgrade_migrations
|
||||
run_final_system_package_upgrade
|
||||
# This also retires the stock ~/.local/share/fonts/omarchy.ttf missed by the
|
||||
# former ~/.config retirement entry, through migration 1788848726.
|
||||
run_post_upgrade_migrations
|
||||
run_post_upgrade_update_steps
|
||||
refresh_current_theme_after_upgrade
|
||||
# Do not force-reload Hyprland in the live upgraded session. The legacy
|
||||
|
||||
@@ -14,6 +14,8 @@ fi
|
||||
|
||||
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
|
||||
pkgs="stable"
|
||||
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
|
||||
pkgs="rc"
|
||||
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
pkgs="edge"
|
||||
else
|
||||
|
||||
+98
-16
@@ -13,6 +13,18 @@ safe_icon_name() {
|
||||
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
|
||||
}
|
||||
|
||||
require_plain_name() {
|
||||
# The name becomes a filename. A slash would turn it into directory levels, so
|
||||
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
|
||||
# is stuck in the launcher; a leading ../ leaves the applications directory
|
||||
# altogether. Refuse rather than silently renaming what the user typed -- most
|
||||
# often it is a URL entered in the name field.
|
||||
if [[ $1 == */* ]]; then
|
||||
echo "App name cannot contain '/': $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
icon_name_from_ref() {
|
||||
local ref="$1"
|
||||
local name
|
||||
@@ -42,6 +54,34 @@ download_icon() {
|
||||
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
|
||||
}
|
||||
|
||||
# Chromium --app= treats javascript:, file:, and data: as a document to
|
||||
# run. Prefix schemeless input with https as before, then refuse anything
|
||||
# that is not http(s).
|
||||
normalize_webapp_url() {
|
||||
local url=$1
|
||||
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
url="https://$url"
|
||||
fi
|
||||
printf '%s' "$url"
|
||||
}
|
||||
|
||||
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
|
||||
# the desktop entry; before Exec argument quoting, it also split browser flags
|
||||
# and additional URLs into separate arguments. Schemes are case-insensitive.
|
||||
require_http_url() {
|
||||
local url=$1
|
||||
|
||||
if [[ $url =~ [[:space:]] ]]; then
|
||||
echo "Error: web app URL must not contain whitespace." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! ${url,,} =~ ^https?:// ]]; then
|
||||
echo "Error: web app URL must be http or https." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_site_icon() {
|
||||
local site_url="$1" dest="$2"
|
||||
local origin page icon_url
|
||||
@@ -65,13 +105,44 @@ fetch_site_icon() {
|
||||
download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest"
|
||||
}
|
||||
|
||||
desktop_string_escape() {
|
||||
# Desktop Entry "string" value (freedesktop Desktop Entry Spec, "Value types"):
|
||||
# a raw newline would start a new key line and let a value inject a second
|
||||
# Exec=. Escape backslash first, then tab/CR/LF and a leading space. Every value
|
||||
# written into the .desktop file passes through here.
|
||||
#
|
||||
# Parameter expansion rather than sed: GNU sed's N auto-prints the pattern space
|
||||
# and exits at end of input, so a `:a;N;$!ba` slurp skips every following s///
|
||||
# for a value with no newline in it - which is every value except the injection
|
||||
# attempt this exists to stop.
|
||||
local value="$1"
|
||||
|
||||
value=${value//\\/\\\\}
|
||||
value=${value//$'\t'/\\t}
|
||||
value=${value//$'\r'/\\r}
|
||||
value=${value//$'\n'/\\n}
|
||||
[[ $value == " "* ]] && value="\\s${value# }"
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
desktop_exec_arg() {
|
||||
# One Exec argument, double-quoted per the freedesktop Exec spec: inside quotes
|
||||
# " ` $ \ take a backslash and a literal % becomes %%. Only the default Exec's
|
||||
# URL needs this; $CUSTOM_EXEC stays a whole command line (file-syntax only).
|
||||
local escaped
|
||||
escaped=$(printf '%s' "$1" \
|
||||
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')
|
||||
printf '"%s"' "$escaped"
|
||||
}
|
||||
|
||||
if (( $# < 3 )); then
|
||||
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
|
||||
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
|
||||
require_plain_name "$APP_NAME"
|
||||
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$APP_URL")
|
||||
require_http_url "$APP_URL"
|
||||
|
||||
# Try to fetch the site's icon automatically first.
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -88,10 +159,8 @@ if (( $# < 3 )); then
|
||||
INTERACTIVE_MODE=true
|
||||
else
|
||||
APP_NAME="$1"
|
||||
APP_URL="$2"
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$2")
|
||||
require_http_url "$APP_URL"
|
||||
ICON_REF="$3"
|
||||
CUSTOM_EXEC="$4" # Optional custom exec command
|
||||
MIME_TYPES="$5" # Optional mime types
|
||||
@@ -104,6 +173,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_plain_name "$APP_NAME"
|
||||
|
||||
if [[ -z $ICON_REF ]]; then
|
||||
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -128,28 +199,39 @@ else
|
||||
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
|
||||
fi
|
||||
|
||||
# Use custom exec if provided, otherwise default behavior
|
||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||
# Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
|
||||
# the file-syntax escaping below (unescaped first at read time per spec, so the
|
||||
# layers compose). $CUSTOM_EXEC is a full command line, so it gets file-syntax only.
|
||||
if [[ -n $CUSTOM_EXEC ]]; then
|
||||
EXEC_COMMAND=$CUSTOM_EXEC
|
||||
else
|
||||
EXEC_COMMAND="omarchy-launch-webapp $(desktop_exec_arg "$APP_URL")"
|
||||
fi
|
||||
|
||||
# Create application .desktop file
|
||||
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
|
||||
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
mkdir -p "$DESKTOP_DIR"
|
||||
|
||||
name_field=$(desktop_string_escape "$APP_NAME")
|
||||
exec_field=$(desktop_string_escape "$EXEC_COMMAND")
|
||||
icon_field=$(desktop_string_escape "$ICON_VALUE")
|
||||
|
||||
cat >"$DESKTOP_FILE" <<EOF
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=$APP_NAME
|
||||
Comment=$APP_NAME
|
||||
Exec=$EXEC_COMMAND
|
||||
Name=$name_field
|
||||
Comment=$name_field
|
||||
Exec=$exec_field
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=$ICON_VALUE
|
||||
Icon=$icon_field
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Add mime types if provided
|
||||
if [[ -n $MIME_TYPES ]]; then
|
||||
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE"
|
||||
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
chmod +x "$DESKTOP_FILE"
|
||||
|
||||
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
|
||||
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications/"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
# Find all web apps
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
|
||||
# Always index the launchers, so removal deletes the file that was found rather
|
||||
# than a path rebuilt from the displayed name. Installs predating the name
|
||||
# validation could nest the launcher inside directories, and those are exactly
|
||||
# the ones a reconstructed path cannot reach.
|
||||
WEB_APP_PATHS=()
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
WEB_APP_PATHS+=("$file")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
|
||||
|
||||
# The launcher matching a chosen name, or empty when nothing was indexed under
|
||||
# it (an app removed between the scan and the pick, say).
|
||||
path_for_web_app() {
|
||||
local wanted="$1" i
|
||||
for i in "${!WEB_APPS[@]}"; do
|
||||
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
|
||||
printf '%s\n' "${WEB_APP_PATHS[$i]}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if (( $# == 0 )); then
|
||||
if ((${#WEB_APPS[@]})); then
|
||||
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
|
||||
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
|
||||
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
|
||||
fi
|
||||
|
||||
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
|
||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
desktop_file=$(path_for_web_app "$APP_NAME")
|
||||
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||
|
||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||
|
||||
+1263
-120
File diff suppressed because it is too large.
Load diff
+14
-28
@@ -1,35 +1,21 @@
|
||||
# Remove the include below to disconnect Kitty from Omarchy's theming system.
|
||||
include ~/.local/state/omarchy/current/theme/kitty.conf
|
||||
|
||||
# Settings below override Omarchy's defaults in /etc/xdg/kitty/kitty.conf.
|
||||
# Learn more: https://sw.kovidgoyal.net/kitty/conf/
|
||||
|
||||
# Font
|
||||
font_family JetBrainsMono Nerd Font
|
||||
bold_italic_font auto
|
||||
font_size 9.0
|
||||
# font_family JetBrainsMono Nerd Font
|
||||
# font_size 12
|
||||
|
||||
# Window
|
||||
window_padding_width 14
|
||||
hide_window_decorations yes
|
||||
confirm_os_window_close 0
|
||||
# Window padding
|
||||
# window_padding_width 14
|
||||
|
||||
# Keybindings
|
||||
map ctrl+insert copy_to_clipboard
|
||||
map shift+insert paste_from_clipboard
|
||||
# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter.
|
||||
map shift+enter send_text all \e[13;2u
|
||||
# Kitty legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter.
|
||||
map alt+shift+enter send_text all \e[13;4u
|
||||
# Unmap a shortcut, passing it through to the terminal application
|
||||
# map ctrl+insert
|
||||
|
||||
# Allow remote access
|
||||
allow_remote_control yes
|
||||
listen_on unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}
|
||||
# Set or replace a shortcut
|
||||
# map ctrl+shift+c copy_to_clipboard
|
||||
|
||||
# Aesthetics
|
||||
cursor_shape block
|
||||
cursor_blink_interval 0
|
||||
shell_integration no-cursor
|
||||
enable_audio_bell no
|
||||
|
||||
# Minimal Tab bar styling
|
||||
tab_bar_edge bottom
|
||||
tab_bar_style powerline
|
||||
tab_powerline_style slanted
|
||||
tab_title_template {title}{' :{}:'.format(num_windows) if num_windows > 1 else ''}
|
||||
# Remove all inherited shortcuts, including Kitty's built-in shortcuts
|
||||
# clear_all_shortcuts yes
|
||||
@@ -4,4 +4,4 @@
|
||||
# To put it into use, remove .sample from this file name.
|
||||
|
||||
# Example: Show the name of the font that was just set.
|
||||
# notify-send -u low "New font" "Your new font is $1"
|
||||
# omarchy-notification-send -u low "New font" "Your new font is $1"
|
||||
@@ -6,5 +6,5 @@
|
||||
weather=$(omarchy-weather-status 2>/dev/null) || true
|
||||
|
||||
if [[ -n $weather && $weather != "Weather unavailable" ]]; then
|
||||
notify-send -u low "$weather"
|
||||
omarchy-notification-send -u low "$weather"
|
||||
fi
|
||||
@@ -4,4 +4,4 @@
|
||||
# To put it into use, remove .sample from this file name.
|
||||
|
||||
# Example: Show notification after the system has been updated.
|
||||
# notify-send -u low "Update Performed" "Your system is now up to date"
|
||||
# omarchy-notification-send -u low "Update Performed" "Your system is now up to date"
|
||||
@@ -4,4 +4,4 @@
|
||||
# To put it into use, remove .sample from this file name.
|
||||
|
||||
# Example: Show the name of the theme that was just set.
|
||||
# notify-send -u low "New theme" "Your new theme is $1"
|
||||
# omarchy-notification-send -u low "New theme" "Your new theme is $1"
|
||||
@@ -22,11 +22,34 @@ omarchy theme install <url> # Install from git repo
|
||||
Additional user backgrounds for any theme (stock or custom) go in
|
||||
`~/.config/omarchy/backgrounds/<theme-slug>/`.
|
||||
|
||||
## What a Theme Installed From a Repo May Not Contain
|
||||
|
||||
A theme the user wrote by hand in `~/.config/omarchy/themes` is unrestricted, as
|
||||
are Omarchy's own themes. From a theme cloned by `omarchy theme install`, Omarchy
|
||||
drops only what runs code: any `*.lua` (Hyprland requires a theme's
|
||||
`hyprland.lua` and `gum_env.lua` at login, Neovim loads `neovim.lua` at startup),
|
||||
the terminal configs `alacritty.toml`, `foot.ini`, `ghostty.conf` and
|
||||
`kitty.conf` (each names the program the terminal launches), and `vscode.json`
|
||||
(names a VS Code extension to install). Those are regenerated from `colors.toml`
|
||||
through `$OMARCHY_PATH/default/themed/*.tpl`, and named on stderr.
|
||||
|
||||
Everything else a cloned theme ships is kept, including `btop.theme`,
|
||||
`chromium.theme`, `helix.toml`, `icons.theme`, `keyboard.rgb` and `shell.toml`.
|
||||
Omarchy tells a cloned theme from the user's own by the `.git` directory a clone
|
||||
leaves behind.
|
||||
|
||||
To change how Omarchy themes an app for every theme, write the template rather
|
||||
than the theme: `~/.config/omarchy/themed/<config-name>.tpl` overrides the
|
||||
built-in one. See `docs/theming.md` in the Omarchy repo.
|
||||
|
||||
## Customizing a Stock Theme
|
||||
|
||||
Never edit stock themes under `/usr/share/omarchy/themes/` — changes are lost
|
||||
on update. Two safe options:
|
||||
|
||||
Both write into `~/.config/omarchy/themes`, where a theme the user wrote is
|
||||
unrestricted — the list above applies only to a theme cloned from a repo.
|
||||
|
||||
**Overlay (preferred for small tweaks):** create a user theme directory with
|
||||
the SAME slug containing only the files you want to change. When the theme is
|
||||
applied, the stock theme is copied first and your files win on top:
|
||||
|
||||
@@ -45,8 +45,8 @@ alias ....='cd ../../..'
|
||||
# Tools
|
||||
alias a='omarchy-agent --inline'
|
||||
alias c='opencode --auto'
|
||||
alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode bypassPermissions'
|
||||
alias cy='codex -s danger-full-access -a never'
|
||||
alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode auto'
|
||||
alias cy='codex --approve-for-me'
|
||||
alias d='docker'
|
||||
alias r='rails'
|
||||
alias t='tmux attach || tmux new -s Work'
|
||||
|
||||
@@ -10,6 +10,12 @@ The private-use glyphs in `omarchy.ttf` are:
|
||||
- `U+E905` — Codex, from the OpenAI mark on <https://developers.openai.com/codex/>
|
||||
- `U+E906` — LM Studio, from <https://simpleicons.org/icons/lmstudio.svg>
|
||||
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
|
||||
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
|
||||
|
||||
- `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from <https://fontawesome.com/icons/staff-snake>, the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size
|
||||
- `U+E90B` — Perplexity, from <https://simpleicons.org/icons/perplexity.svg>
|
||||
- `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG
|
||||
- `U+E90D` — Cursor, from <https://simpleicons.org/icons/cursor.svg>
|
||||
|
||||
The agent marks are monochrome so the menu can render them using the active
|
||||
theme's foreground and selection colors.
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
-- Hermes Desktop's frameless HUD manages its own geometry.
|
||||
o.window({ class = "^Hermes$", title = "^Hermes HUD$" }, {
|
||||
tag = "-default-opacity",
|
||||
float = true,
|
||||
border_size = 0,
|
||||
opacity = "1 1",
|
||||
})
|
||||
@@ -13,7 +13,7 @@ if o.preinstalled_bindings_enabled() then
|
||||
o.bind("SUPER + CTRL + RETURN", "Herdr", { omarchy = "terminal-herdr" })
|
||||
o.bind("SUPER + SHIFT + M", "Music", { omarchy = "spotify" })
|
||||
o.bind("SUPER + SHIFT + ALT + M", "Music TUI", { tui = "cliamp", focus = true })
|
||||
o.bind("SUPER + SHIFT + D", "Docker", { tui = "lazydocker" })
|
||||
o.bind("SUPER + SHIFT + D", "Docker", { tui = "omarchy-launch-docker-tui" })
|
||||
o.bind("SUPER + SHIFT + G", "Signal", { omarchy = "signal" })
|
||||
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
|
||||
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Disable a Hyprland input device whose name was stored as data, not Lua.
|
||||
-- Device names come from USB descriptors and must never be loaded as code.
|
||||
|
||||
local paths = require("default.hypr.paths")
|
||||
|
||||
return function(kind)
|
||||
-- Hardcoded to ~/.local/state to match omarchy-toggle-input-device and the
|
||||
-- sibling bash toggle tools, which all write there regardless of
|
||||
-- XDG_STATE_HOME.
|
||||
local file = io.open(paths.home .. "/.local/state/omarchy/toggles/hypr/" .. kind .. "-disabled-name", "r")
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
|
||||
local name = file:read("*l")
|
||||
file:close()
|
||||
|
||||
if name and name ~= "" then
|
||||
hl.device({ name = name, enabled = false })
|
||||
end
|
||||
end
|
||||
@@ -18,9 +18,20 @@ local function file_exists(path)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Hyprland reaps its own children, so os.execute() can't retrieve an exit status
|
||||
-- from inside the compositor. Read a marker off stdout instead.
|
||||
function o.shell_succeeds(command)
|
||||
local ok, _, code = os.execute(command .. " >/dev/null 2>&1")
|
||||
return ok == true or ok == 0 or code == 0
|
||||
-- Subshell, so the redirection covers every command rather than binding to
|
||||
-- the last one and letting an earlier one write its own OK into the pipe.
|
||||
local pipe = io.popen("( " .. command .. " ) >/dev/null 2>&1 && echo OK")
|
||||
if not pipe then
|
||||
return false
|
||||
end
|
||||
|
||||
local output = pipe:read("*a") or ""
|
||||
pipe:close()
|
||||
|
||||
return output:find("OK", 1, true) ~= nil
|
||||
end
|
||||
|
||||
function o.cmd_present(command)
|
||||
@@ -125,7 +136,7 @@ function o.bind_toggle(keys, description, toggle, options)
|
||||
end
|
||||
|
||||
function o.notify(message)
|
||||
return "notify-send -u low " .. shell_quote(message)
|
||||
return "omarchy-notification-send -u low " .. shell_quote(message)
|
||||
end
|
||||
|
||||
function o.window(match, rules)
|
||||
|
||||
+13
-3
@@ -4,9 +4,19 @@
|
||||
|
||||
local home = os.getenv("HOME")
|
||||
|
||||
-- A variable that is set but empty means "unset" (XDG Base Directory spec);
|
||||
-- bash's ${VAR:-fallback} in the sibling tools treats it the same way.
|
||||
local function env_or(name, fallback)
|
||||
local value = os.getenv(name)
|
||||
if value == nil or value == "" then
|
||||
return fallback
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
return {
|
||||
home = home,
|
||||
config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config"),
|
||||
state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state"),
|
||||
omarchy_path = os.getenv("OMARCHY_PATH") or "/usr/share/omarchy",
|
||||
config_home = env_or("XDG_CONFIG_HOME", home .. "/.config"),
|
||||
state_home = env_or("XDG_STATE_HOME", home .. "/.local/state"),
|
||||
omarchy_path = env_or("OMARCHY_PATH", "/usr/share/omarchy"),
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
-- Pass a module prefix for normal package.path modules, e.g.
|
||||
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
|
||||
-- Pass nil as the prefix when the directory itself has been added to package.path.
|
||||
-- Pass options.exclude as a set of base names (without ".lua") to skip; a legacy
|
||||
-- file that must never be loaded as code stays on disk for a migration to remove.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -12,19 +14,23 @@ local function shell_quote(path)
|
||||
end
|
||||
|
||||
function M.files(dir, module_prefix, options)
|
||||
local exclude = options and options.exclude or {}
|
||||
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
|
||||
if handle then
|
||||
for filename in handle:lines() do
|
||||
local module = filename:gsub("%.lua$", "")
|
||||
if module_prefix then
|
||||
module = module_prefix .. "." .. module
|
||||
end
|
||||
local name = filename:gsub("%.lua$", "")
|
||||
if not exclude[name] then
|
||||
local module = name
|
||||
if module_prefix then
|
||||
module = module_prefix .. "." .. module
|
||||
end
|
||||
|
||||
if options and options.reload then
|
||||
package.loaded[module] = nil
|
||||
end
|
||||
if options and options.reload then
|
||||
package.loaded[module] = nil
|
||||
end
|
||||
|
||||
require(module)
|
||||
require(module)
|
||||
end
|
||||
end
|
||||
handle:close()
|
||||
end
|
||||
|
||||
@@ -4,6 +4,20 @@ local require_all = require("default.hypr.require_all")
|
||||
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
|
||||
package.path = toggles_dir .. "/?.lua;" .. package.path
|
||||
|
||||
require_all.files(toggles_dir, nil, { reload = true })
|
||||
-- touchpad-disabled.lua / touchscreen-disabled.lua were generated Lua in older
|
||||
-- versions and could carry an injected USB device name. They must never be loaded
|
||||
-- as code again: exclude them so a not-yet-migrated install cannot execute a
|
||||
-- leftover payload on reload. The migration recovers the name and deletes them.
|
||||
require_all.files(toggles_dir, nil, {
|
||||
reload = true,
|
||||
exclude = {
|
||||
["touchpad-disabled"] = true,
|
||||
["touchscreen-disabled"] = true,
|
||||
},
|
||||
})
|
||||
|
||||
local disabled_input_device = require("default.hypr.disabled-input-device")
|
||||
disabled_input_device("touchpad")
|
||||
disabled_input_device("touchscreen")
|
||||
|
||||
require("default.hypr.workspace-layouts")
|
||||
Loaded 100 of 382 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user