Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c668141e9c | ||
|
|
8f3ab025b8 | ||
|
|
28d5251e23 | ||
|
|
cbaab83a1e | ||
|
|
dc0d894584 | ||
|
|
ec73ba5d00 | ||
|
|
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 |
No files matched your search
@@ -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.
|
||||
@@ -13,6 +13,7 @@ commands and reusable setup leaves:
|
||||
- use `$OMARCHY_INSTALL` and `$OMARCHY_PATH` instead of hard-coded Omarchy paths.
|
||||
- keep root-scoped hardware setup under `install/hardware/` and orchestrate it through `install/hardware/all.sh`.
|
||||
- keep every per-user setup leaf under `install/user/` (including `install/user/hardware/` and `install/user/first-run/`) so it is clear what must run for each user.
|
||||
- The base install supplies matching kernel headers before hardware setup. DKMS installers should install their driver packages and assume the headers exist.
|
||||
- prefer helper commands for package and command checks where available.
|
||||
|
||||
Raw `command -v`, `pacman`, and `pacman-key` are acceptable in package-helper
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -82,10 +82,39 @@ 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 --approve-for-me)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
cursor-agent)
|
||||
# --yolo covers commands only; the workspace trust dialog has its own flag.
|
||||
# A one-word prompt naming a subcommand (update, login, help) still runs that
|
||||
# subcommand after a bare --, so the agent subcommand is named outright, and
|
||||
# -- after it keeps a prompt starting with a dash from being read as an option.
|
||||
command=(cursor-agent --yolo --trust)
|
||||
[[ -n ${prompt:-} ]] && command+=(agent -- "$prompt")
|
||||
;;
|
||||
hermes)
|
||||
if [[ -n ${prompt:-} ]]; then
|
||||
command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt")
|
||||
else
|
||||
command=(hermes --yolo)
|
||||
fi
|
||||
;;
|
||||
muse)
|
||||
# --approval-mode never skips the tool prompts but keeps Muse's own sandbox.
|
||||
command=(muse --approval-mode never)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
omp)
|
||||
command=(omp --auto-approve)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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."
|
||||
@@ -10,7 +10,7 @@ set -e
|
||||
echo "Installing Xbox controller Bluetooth support..."
|
||||
|
||||
# Install xpadneo to ensure controllers work out of the box
|
||||
omarchy-pkg-add linux-headers xpadneo-dkms
|
||||
omarchy-pkg-add xpadneo-dkms
|
||||
|
||||
# Prevent xpad/xpadneo driver conflict
|
||||
echo blacklist xpad | sudo tee /etc/modprobe.d/blacklist-xpad.conf >/dev/null
|
||||
|
||||
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
+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"
|
||||
@@ -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 --quiet "$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"
|
||||
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
|
||||
@@ -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
|
||||
|
||||
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 \
|
||||
|
||||
@@ -74,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -326,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
|
||||
|
||||
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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -661,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"
|
||||
}
|
||||
|
||||
@@ -1287,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."
|
||||
}
|
||||
@@ -1566,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
|
||||
@@ -1599,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
|
||||
@@ -2375,6 +2376,8 @@ cleanup_retired_services
|
||||
ensure_sleep_lock_service
|
||||
remove_retired_default_packages
|
||||
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
|
||||
|
||||
+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
|
||||
@@ -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",
|
||||
})
|
||||
@@ -12,6 +12,7 @@ fcitx5-configtool
|
||||
fcitx5-wayland-launcher
|
||||
foot-server
|
||||
footclient
|
||||
hermes
|
||||
java-java-openjdk
|
||||
jconsole-java-openjdk
|
||||
jshell-java-openjdk
|
||||
|
||||
@@ -134,9 +134,13 @@
|
||||
"setup.default.agent.codex": {"icon":"","iconFont":"omarchy","label":"Codex","checked":"[[ \"$(omarchy-default-agent)\" == \"codex\" ]]","action":"omarchy-default-agent codex"},
|
||||
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
||||
"setup.default.agent.crush": {"icon":"","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
|
||||
"setup.default.agent.cursor-agent": {"icon":"","iconFont":"omarchy","label":"Cursor CLI","checked":"[[ \"$(omarchy-default-agent)\" == \"cursor-agent\" ]]","action":"omarchy-default-agent cursor-agent"},
|
||||
"setup.default.agent.gemini": {"icon":"","label":"Gemini","checked":"[[ \"$(omarchy-default-agent)\" == \"gemini\" ]]","action":"omarchy-default-agent gemini"},
|
||||
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
|
||||
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
|
||||
"setup.default.agent.muse": {"icon":"","label":"Muse Code","checked":"[[ \"$(omarchy-default-agent)\" == \"muse\" ]]","action":"omarchy-default-agent muse"},
|
||||
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
||||
"setup.default.agent.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","checked":"[[ \"$(omarchy-default-agent)\" == \"openclaw\" ]]","action":"omarchy-default-agent openclaw"},
|
||||
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
|
||||
"setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
|
||||
"setup.default.browser": {"icon":"","label":"Browser","title":"Default Browser"},
|
||||
@@ -155,7 +159,7 @@
|
||||
"setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"},
|
||||
"setup.default.editor.neovim": {"icon":"","label":"Neovim","when":"omarchy-cmd-present nvim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"},
|
||||
"setup.default.editor.vscode": {"icon":"","label":"VSCode","when":"omarchy-cmd-present code","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"},
|
||||
"setup.default.editor.cursor": {"icon":"","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
|
||||
"setup.default.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
|
||||
"setup.default.editor.zed": {"icon":"","label":"Zed","when":"omarchy-cmd-present zeditor","checked":"[[ \"$(omarchy-default-editor)\" == \"zeditor\" ]]","action":"omarchy-default-editor zed"},
|
||||
"setup.default.editor.sublime": {"icon":"","label":"Sublime Text","when":"omarchy-cmd-present sublime_text","checked":"[[ \"$(omarchy-default-editor)\" == \"sublime_text\" ]]","action":"omarchy-default-editor sublime_text"},
|
||||
"setup.default.editor.helix": {"icon":"","label":"Helix","when":"omarchy-cmd-present helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
||||
@@ -220,7 +224,7 @@
|
||||
"install.service.bitwarden": {"icon":"","label":"Bitwarden","when":"! omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"},
|
||||
"install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]] && ! grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"},
|
||||
"install.editor.vscode": {"icon":"","label":"VSCode","when":"! omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"},
|
||||
"install.editor.cursor": {"icon":"","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"},
|
||||
"install.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"},
|
||||
"install.editor.zed": {"icon":"","label":"Zed","when":"! omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"},
|
||||
"install.editor.sublime": {"icon":"","label":"Sublime Text","when":"! omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"},
|
||||
"install.editor.helix": {"icon":"","label":"Helix","when":"! omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"},
|
||||
@@ -235,6 +239,10 @@
|
||||
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"! omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
|
||||
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
|
||||
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"! omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
|
||||
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","when":"! omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-t3-code"},
|
||||
"install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"! omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"},
|
||||
"install.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","when":"! omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-openclaw"},
|
||||
"install.ai.perplexity": {"icon":"","iconFont":"omarchy","label":"Perplexity","when":"! omarchy-pkg-present perplexity","action":"omarchy-install-and-launch Perplexity perplexity perplexity"},
|
||||
"install.gaming.steam": {"icon":"","label":"Steam","when":"! omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
|
||||
"install.gaming.retroarch": {"icon":"","label":"RetroArch","when":"! omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
|
||||
"install.gaming.minecraft": {"icon":"","label":"Minecraft","when":"! omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
|
||||
@@ -285,6 +293,7 @@
|
||||
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
||||
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
||||
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
||||
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
||||
"remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"},
|
||||
"remove.browser.edge": {"icon":"","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"},
|
||||
"remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"},
|
||||
@@ -323,6 +332,10 @@
|
||||
"remove.development.php.symfony": {"icon":"","label":"Symfony","when":"omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env symfony'"},
|
||||
"remove.development.elixir.elixir": {"icon":"","label":"Elixir","when":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env elixir'"},
|
||||
"remove.development.elixir.phoenix": {"icon":"","label":"Phoenix","when":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-dev-env phoenix'"},
|
||||
"remove.ai": {"icon":"","label":"AI","title":"Remove"},
|
||||
"remove.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","when":"omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-t3-code"},
|
||||
"remove.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","when":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-openclaw"},
|
||||
"remove.ai.perplexity": {"icon":"","iconFont":"omarchy","label":"Perplexity","when":"omarchy-pkg-present perplexity","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-perplexity"},
|
||||
|
||||
// Update
|
||||
"update.omarchy": {"icon":"","iconFont":"omarchy","label":"Omarchy","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update"},
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode
|
||||
# without needing to restart the computer. This is needed because computers like the Asus G14
|
||||
# will wake after suspend in Hybrid mode, even if the system was in Integrated mode before
|
||||
# suspending.
|
||||
|
||||
restore_marker=/run/omarchy-force-igpu-integrated
|
||||
sleep_action=${SYSTEMD_SLEEP_ACTION:-$2}
|
||||
[[ -x /usr/bin/supergfxctl ]] || exit 0
|
||||
|
||||
switch_mode() {
|
||||
local expected="$1" current
|
||||
|
||||
if ! /usr/bin/timeout --kill-after=1s 3s /usr/bin/supergfxctl -m "$expected"; then
|
||||
echo "Could not request the GPU transition to $expected mode" >&2
|
||||
return 1
|
||||
fi
|
||||
for _ in {1..10}; do
|
||||
if current=$(/usr/bin/timeout --kill-after=1s 2s /usr/bin/supergfxctl -g 2>/dev/null) &&
|
||||
[[ $current == "$expected" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Could not confirm the GPU transition to $expected mode" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
pre)
|
||||
# Remember the mode this sleep cycle started in. supergfxctl persists the
|
||||
# temporary hibernate switch to Vfio, so post must not consult that mutable
|
||||
# value when deciding whether to restore Integrated mode.
|
||||
if [[ -L $restore_marker ]]; then
|
||||
exit 1
|
||||
elif [[ ! -f $restore_marker ]]; then
|
||||
/usr/bin/grep -Eq '"mode"[[:space:]]*:[[:space:]]*"Integrated"' /etc/supergfxd.conf 2>/dev/null || exit 0
|
||||
/usr/bin/install -m 0600 -o root -g root -T /dev/null "$restore_marker"
|
||||
fi
|
||||
|
||||
# Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU.
|
||||
# Without this, hibernate resume fails because the nvidia driver can't freeze a
|
||||
# powered-off dGPU (returns -EIO), which aborts the entire resume.
|
||||
if [[ $2 == "hibernate" ]]; then
|
||||
/usr/bin/supergfxctl -m Vfio
|
||||
sleep 1
|
||||
if [[ $sleep_action == "hibernate" ]]; then
|
||||
switch_mode Vfio
|
||||
fi
|
||||
;;
|
||||
post)
|
||||
[[ -f $restore_marker && ! -L $restore_marker ]] || exit 0
|
||||
|
||||
# small delay so the device is fully re-enumerated
|
||||
sleep 4
|
||||
|
||||
# force-bind dGPU to vfio (fully detached from nvidia)
|
||||
/usr/bin/supergfxctl -m Vfio
|
||||
sleep 1
|
||||
switch_mode Vfio
|
||||
|
||||
# then go back to Integrated, which powers it off again
|
||||
/usr/bin/supergfxctl -m Integrated
|
||||
switch_mode Integrated
|
||||
/usr/bin/rm -f -- "$restore_marker"
|
||||
;;
|
||||
esac
|
||||
@@ -3,7 +3,9 @@
|
||||
# Turn off keyboard backlight before hibernate to prevent hang on power-off.
|
||||
# The ASUS keyboard controller can block S4 shutdown if LEDs are active.
|
||||
|
||||
if [[ $1 == "pre" && $2 == "hibernate" ]]; then
|
||||
sleep_action=${SYSTEMD_SLEEP_ACTION:-$2}
|
||||
|
||||
if [[ $1 == "pre" && $sleep_action == "hibernate" ]]; then
|
||||
device=""
|
||||
for candidate in /sys/class/leds/*kbd_backlight*; do
|
||||
if [[ -e "$candidate" ]]; then
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots
|
||||
@@ -0,0 +1,47 @@
|
||||
name: omarchy
|
||||
description: Omarchy system theme
|
||||
colors:
|
||||
background: "{{ background }}"
|
||||
ui_text: "{{ foreground }}"
|
||||
ui_primary: "{{ accent }}"
|
||||
ui_accent: "{{ accent }}"
|
||||
ui_border: "{{ muted }}"
|
||||
ui_label: "{{ accent }}"
|
||||
ui_ok: "{{ green }}"
|
||||
ui_warn: "{{ yellow }}"
|
||||
ui_error: "{{ red }}"
|
||||
ui_tool: "{{ cyan }}"
|
||||
ui_thinking: "{{ dark_foreground }}"
|
||||
banner_border: "{{ muted }}"
|
||||
banner_title: "{{ accent }}"
|
||||
banner_accent: "{{ accent }}"
|
||||
banner_dim: "{{ dark_foreground }}"
|
||||
banner_text: "{{ foreground }}"
|
||||
prompt: "{{ bright_foreground }}"
|
||||
input_rule: "{{ muted }}"
|
||||
response_border: "{{ accent }}"
|
||||
shell_dollar: "{{ blue }}"
|
||||
selection_bg: "{{ selection }}"
|
||||
session_label: "{{ accent }}"
|
||||
session_border: "{{ muted }}"
|
||||
status_bar_bg: "{{ dark_background }}"
|
||||
status_bar_text: "{{ foreground }}"
|
||||
status_bar_strong: "{{ accent }}"
|
||||
status_bar_dim: "{{ dark_foreground }}"
|
||||
status_bar_good: "{{ green }}"
|
||||
status_bar_warn: "{{ yellow }}"
|
||||
status_bar_bad: "{{ red }}"
|
||||
status_bar_critical: "{{ bright_red }}"
|
||||
voice_status_bg: "{{ dark_background }}"
|
||||
completion_menu_bg: "{{ lighter_background }}"
|
||||
completion_menu_current_bg: "{{ selection }}"
|
||||
completion_menu_meta_bg: "{{ lighter_background }}"
|
||||
completion_menu_meta_current_bg: "{{ selection }}"
|
||||
diff_added: "{{ mix background green 15% }}"
|
||||
diff_removed: "{{ mix background red 15% }}"
|
||||
diff_added_word: "{{ green }}"
|
||||
diff_removed_word: "{{ red }}"
|
||||
syntax_string: "{{ green }}"
|
||||
syntax_number: "{{ yellow }}"
|
||||
syntax_keyword: "{{ magenta }}"
|
||||
syntax_comment: "{{ muted }}"
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "Omarchy",
|
||||
"appearance": "{{ mode }}",
|
||||
"canvas": "{{ background }}",
|
||||
"accent": "{{ accent }}",
|
||||
"colors": {
|
||||
"chrome": "{{ background }}",
|
||||
"toolbar": "{{ background }}",
|
||||
"toolbarForeground": "{{ foreground }}",
|
||||
"toolbarBorder": "{{ muted }}",
|
||||
|
||||
"text": "{{ foreground }}",
|
||||
"border": "{{ muted }}",
|
||||
"focus": "{{ accent }}",
|
||||
|
||||
"sidebar": "{{ dark_background }}",
|
||||
"sidebarForeground": "{{ foreground }}",
|
||||
"sidebarBorder": "{{ muted }}",
|
||||
"sidebarRowHover": "{{ mix dark_background foreground 6% }}",
|
||||
"sidebarRowActive": "{{ mix dark_background accent 18% }}",
|
||||
"sidebarRowSelected": "{{ selection }}",
|
||||
|
||||
"error": "{{ mix red foreground 35% }}",
|
||||
"warning": "{{ mix yellow foreground 35% }}",
|
||||
|
||||
"codeBackground": "{{ dark_background }}",
|
||||
"codeForeground": "{{ foreground }}",
|
||||
|
||||
"terminalBackground": "{{ background }}",
|
||||
"terminalForeground": "{{ foreground }}",
|
||||
"terminalCursor": "{{ bright_foreground }}",
|
||||
"terminalSelection": "{{ selection }}",
|
||||
"terminalScrollbar": "{{ muted }}",
|
||||
"terminalScrollbarHover": "{{ dark_foreground }}"
|
||||
}
|
||||
}
|
||||
+18
-3
@@ -72,6 +72,7 @@ version ──► omarchy /usr/share/omarchy
|
||||
config/** ──► omarchy-settings /etc/skel/.config/** (seeds new users)
|
||||
/usr/share/omarchy/config/** (resync source)
|
||||
etc/fastfetch/config.jsonc ──► omarchy-settings /etc/fastfetch/config.jsonc
|
||||
etc/xdg/kitty/kitty.conf ──► omarchy-settings /etc/xdg/kitty/kitty.conf
|
||||
|
||||
applications/*.desktop ──► omarchy-settings /etc/skel/.local/share/applications/
|
||||
/usr/share/omarchy/applications/
|
||||
@@ -123,6 +124,8 @@ logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy
|
||||
/etc/skel/.config/omarchy/branding/{about,screensaver}.txt
|
||||
```
|
||||
|
||||
The hardware-conditional `force-igpu` and `keyboard-backlight` sources also live under `default/systemd/system-sleep/`, but their setup commands publish root-owned copies only on machines that need them; they are not installed by `omarchy-settings`.
|
||||
|
||||
### Why `etc-overrides/` exists
|
||||
|
||||
Some files under `/etc/` (`.bashrc` in `/etc/skel`, `nsswitch.conf`,
|
||||
@@ -135,6 +138,14 @@ them via pacman without a file conflict. Instead they ship at
|
||||
Tradeoff: user edits to those files get clobbered on every `omarchy-settings`
|
||||
upgrade. This is documented in the PKGBUILD.
|
||||
|
||||
## Locate indexing
|
||||
|
||||
`default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf` ships through `omarchy-settings` to `/usr/lib/systemd/system/plocate-updatedb.service.d/10-omarchy.conf`. It replaces the existing service's `ExecStart` with `updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots`, keeping Btrfs subvolume mounts searchable and excluding Snapper snapshots. The upstream service retains its timer, resource limits, and sandbox; Omarchy's existing AC-power condition still applies.
|
||||
|
||||
`/etc/updatedb.conf` remains owned by plocate and is never rewritten by Omarchy. The command-line options override bind-mount pruning and add to the administrator's existing path exclusions. Installer and AUR package refreshes pass the same options directly because installation may run without systemd and an explicitly requested refresh should work on battery.
|
||||
|
||||
Arch's systemd package hook reloads units when the vendor drop-in is installed or upgraded. The settings package containing the drop-in must ship alongside the runtime package that removes the old configuration helper and migration. Pacman removes those retired files; no new state migration is needed. A running indexer finishes with its original options, and subsequent service starts use the drop-in. For an immediate local test after installing the packages, restart `plocate-updatedb.service` while connected to AC power.
|
||||
|
||||
## Env bootstrap (`default/bash/env-bootstrap`)
|
||||
|
||||
Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It:
|
||||
@@ -177,9 +188,7 @@ Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`,
|
||||
`flags.lua`, or the nautilus extensions — `/etc/skel` already seeded those.
|
||||
It only does the things `/etc/skel` can't:
|
||||
|
||||
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/omarchy` →
|
||||
`$OMARCHY_PATH/default/agents/skills/omarchy`. Symlinks (not copies) so
|
||||
`omarchy dev link` against a dev checkout repoints them correctly.
|
||||
- Skill symlinks into `~/.agents/skills/<name>`, `~/.claude/skills/<name>`, `~/.codex/skills/<name>`, `~/.pi/agent/skills/<name>`, `~/.hermes/skills/<name>`, and each existing `~/.hermes/profiles/*/skills/<name>` → `$OMARCHY_PATH/default/agents/skills/<name>`, looping over every skill directory there (currently `omarchy` and `diagnose-crash`) so new skills need no edit. Symlinks (not copies) so `omarchy dev link` against a dev checkout repoints them correctly. Hermes profile dirs are only linked when they already exist — provision does not create Hermes profiles.
|
||||
- `xdg-user-dirs-update` (Templates/Public/Desktop folded back into `$HOME`)
|
||||
and `~/.config/gtk-3.0/bookmarks` (needs `$HOME` expansion).
|
||||
- Hyprland's package-owned default input reads `XKBLAYOUT` / `XKBVARIANT`
|
||||
@@ -321,3 +330,9 @@ return to the packaged default.
|
||||
| New stock theme | `themes/<name>/` (+ matching templates under `default/themed/` if they need theme colors) |
|
||||
| User-installed theme | `~/.config/omarchy/themes/<name>/` |
|
||||
| Generated current theme/background state | `~/.local/state/omarchy/current/` |
|
||||
|
||||
## Kitty defaults and user overrides
|
||||
|
||||
Kitty loads `/etc/xdg/kitty/kitty.conf` before `~/.config/kitty/kitty.conf`. The `omarchy-settings` package owns the system file; the user template contains only the active theme include and commented examples for personal overrides. Keeping the theme include in the user file lets users remove it without changing the packaged defaults. Individual inherited keybindings can be unmapped with an empty `map <shortcut>` directive, or all inherited bindings can be cleared with `clear_all_shortcuts yes`.
|
||||
|
||||
The system default uses `allow_remote_control socket-only` so Omarchy can query the active terminal directory over its Unix socket while Kitty rejects remote-control requests arriving through terminal output. Changing this setting requires restarting Kitty. The migration refreshes the exact previous stock config with a backup; customized configs retain their settings and ordering, with only explicit unrestricted `yes`, `y`, or `true` remote-control settings commented out.
|
||||
@@ -34,9 +34,11 @@ first call.
|
||||
|
||||
Only one full bar option is active at a time. The built-in `omarchy.bar` is
|
||||
used when `bar.id` is omitted or when a selected third-party bar cannot load.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins can set
|
||||
`keepLoaded: true` to survive between summons. First-party services are
|
||||
loaded at startup.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins can set the top-level manifest key `keepLoaded: true` to survive between summons, and to keep a service mounted across plugin hot-reload (so `omarchy.lock` is not destroyed while Hyprland still holds the session lock). First-party services are loaded at startup.
|
||||
|
||||
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose `open(payloadJson)` and `close()` for summon/hide; on load the host injects `omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` / `barWidgetRegistry`) as properties. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: 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. A full-bar plugin additionally receives 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 are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry/configuration snapshots can be changed only locally without mutating host state. The facades are API boundaries, not same-process QML sandboxes: a visual widget shares the host bar's scene and can walk its parent hierarchy to ordinary host objects. Sensitive state must not rely on the facade alone for isolation.
|
||||
|
||||
A third-party replacement bar can render registered widget components, but widgets it hosts receive a service-less entry facade. Allowing the bar to manufacture an own-service facade for an arbitrary widget would also let it retrieve that plugin's live service object. Service-backed third-party widgets therefore retain their full integration only under the trusted built-in bar; a replacement bar may still provide their target-scoped lifecycle and settings operations.
|
||||
|
||||
Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegistry.qml).
|
||||
|
||||
@@ -73,11 +75,7 @@ one replaces the active bar, and it is therefore never offered under Disable.
|
||||
Bar widgets may set `barWidget.defaultSection` to `left`, `center`, or `right`;
|
||||
widgets that omit it default to `center`.
|
||||
|
||||
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you
|
||||
before cloning, plugins land disabled so you can review the code before
|
||||
`omarchy plugin enable`, and updates show a diff before touching anything.
|
||||
Commands prompt when run bare in a terminal and run unattended when given
|
||||
arguments — add `--yes` to skip every prompt (the path for scripts and agents).
|
||||
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you before cloning, plugins land disabled so you can review the code before `omarchy plugin enable`, and updates show a diff before touching anything. Commands confirm in a terminal even when given arguments; without one they refuse rather than guess. Add `--yes` to skip every prompt (the path for scripts and agents). The scoped interfaces remove direct access to authentication services and avoid handing generic cross-plugin service factories to replacement bars, but visual plugins can still traverse ordinary objects in their shared QML scene. Plugin code also has the same user-level file and process access as the shell.
|
||||
|
||||
You can still install by hand: drop a plugin into
|
||||
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
|
||||
|
||||
@@ -15,7 +15,8 @@ ENABLE_LIMINE_FALLBACK=yes
|
||||
# Find and add other bootloaders
|
||||
FIND_BOOTLOADERS=yes
|
||||
|
||||
BOOT_ORDER="*, *fallback, Snapshots"
|
||||
# Keep T2 Macs on their specialized kernel; prefer linux-omarchy elsewhere.
|
||||
BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"
|
||||
|
||||
# Snapper is configured with NUMBER_LIMIT="5" (see default/snapper/root), but
|
||||
# limine-snapper-sync can see the newly created sixth snapshot before cleanup
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Keep Cursor optional while applying these options whenever mise installs it.
|
||||
# Expose only its launcher so the bundled Node cannot shadow the user's Node.
|
||||
[tool_alias]
|
||||
cursor-agent = 'http:cursor-agent[bin_path=bin,postinstall=mkdir -p "$MISE_TOOL_INSTALL_PATH/bin" && ln -sfn ../dist-package/cursor-agent "$MISE_TOOL_INSTALL_PATH/bin/cursor-agent"]'
|
||||
@@ -0,0 +1,5 @@
|
||||
# omarchy-sudo-passwordless writes /etc/sudoers.d/99-omarchy-nopasswd-<user> and
|
||||
# arms a transient systemd-run timer to remove it again. Transient units do not
|
||||
# survive a reboot, so remove any remaining grant during early boot. Boot-only
|
||||
# (r!) ensures a later systemd-tmpfiles --remove cannot cut a live grant short.
|
||||
r! /etc/sudoers.d/99-omarchy-nopasswd-*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Omarchy defaults. Put personal overrides in ~/.config/kitty/kitty.conf.
|
||||
|
||||
# Font
|
||||
font_family JetBrainsMono Nerd Font
|
||||
bold_italic_font auto
|
||||
font_size 9.0
|
||||
|
||||
# Window
|
||||
window_padding_width 14
|
||||
hide_window_decorations yes
|
||||
confirm_os_window_close 0
|
||||
|
||||
# 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
|
||||
|
||||
# Allow local cwd lookup, but reject remote control through terminal output.
|
||||
allow_remote_control socket-only
|
||||
listen_on unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}
|
||||
|
||||
# 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 ''}
|
||||
@@ -7,6 +7,5 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/locate.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
||||
@@ -1,32 +0,0 @@
|
||||
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
|
||||
|
||||
echo "Configuring locate to skip Btrfs snapshots and index Btrfs subvolumes"
|
||||
|
||||
[[ -f $UPDATEDB_CONF_PATH ]] || exit 0
|
||||
|
||||
# updatedb refuses to run at all on a config that defines a variable twice, so
|
||||
# every setting here is rewritten where it already stands and only appended
|
||||
# when the file has no line for it.
|
||||
|
||||
# Btrfs subvolume mounts (like /home) look like bind mounts, so pruning
|
||||
# bind mounts leaves them out of the index entirely.
|
||||
if grep -qE '^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then
|
||||
sed -i -E 's|^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=.*|PRUNE_BIND_MOUNTS = "no"|' "$UPDATEDB_CONF_PATH"
|
||||
else
|
||||
printf '%s\n' 'PRUNE_BIND_MOUNTS = "no"' >>"$UPDATEDB_CONF_PATH"
|
||||
fi
|
||||
|
||||
# Snapper snapshots are nested subvolumes reached by plain directory
|
||||
# traversal, so without this updatedb indexes the system once per snapshot.
|
||||
if grep -qE '^[[:space:]]*PRUNEPATHS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then
|
||||
# updatedb only accepts quoted values and allows a comment after them. Read
|
||||
# back what the machine already prunes and write the whole setting out again
|
||||
# rather than splicing into a line of unknown shape.
|
||||
pruned=$(sed -nE 's|^[[:space:]]*PRUNEPATHS[[:space:]]*=[[:space:]]*"([^"]*)".*|\1|p' "$UPDATEDB_CONF_PATH" | tail -n 1)
|
||||
|
||||
if [[ " $pruned " != *" /.snapshots "* ]]; then
|
||||
sed -i -E "s|^[[:space:]]*PRUNEPATHS[[:space:]]*=.*|PRUNEPATHS = \"/.snapshots${pruned:+ $pruned}\"|" "$UPDATEDB_CONF_PATH"
|
||||
fi
|
||||
else
|
||||
printf '%s\n' 'PRUNEPATHS = "/.snapshots"' >>"$UPDATEDB_CONF_PATH"
|
||||
fi
|
||||
@@ -14,11 +14,6 @@ run_logged "$OMARCHY_INSTALL/hardware/vulkan.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/video-acceleration.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/lpmd.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/thermald.sh"
|
||||
# Swap in the Panther Lake kernel before anything pulls DKMS modules in.
|
||||
# intel-ipu7-camera drags in ipu7-drivers, vision-drivers and v4l2loopback,
|
||||
# and building all three against the stock kernel only to rebuild them against
|
||||
# linux-ptl and tear the first set down again cost ~25s of the install.
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/ptl-kernel.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/ipu7-camera.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh"
|
||||
|
||||
@@ -6,5 +6,5 @@ pci_info=$(lspci -nn)
|
||||
|
||||
if (echo "$pci_info" | grep -q "14e4:43a0" || echo "$pci_info" | grep -q "14e4:4331"); then
|
||||
echo "BCM4360 / BCM4331 detected"
|
||||
omarchy-pkg-add broadcom-wl dkms linux-headers
|
||||
omarchy-pkg-add broadcom-wl-dkms
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
# Install Tuxedo drivers for keyboard backlighting on Tuxedo laptops and
|
||||
# compatible devices like the Slimbook Executive (Clevo/Tuxedo chassis).
|
||||
if cat /sys/class/dmi/id/sys_vendor 2>/dev/null | grep -qi "TUXEDO\|Slimbook"; then
|
||||
omarchy-pkg-add linux-headers tuxedo-drivers-nocompatcheck-dkms
|
||||
omarchy-pkg-add tuxedo-drivers-nocompatcheck-dkms
|
||||
|
||||
# Blacklist the legacy clevo_xsm_wmi module which conflicts with the tuxedo-drivers
|
||||
# clevo_wmi module. When clevo_xsm_wmi loads first, it grabs the Clevo WMI GUIDs,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Install drivers for Motorcomm YT6801 ethernet adapter used by the Slimbook Executive
|
||||
if lspci | grep -i "YT6801\|Motorcomm.*Ethernet"; then
|
||||
omarchy-pkg-add linux-headers yt6801-dkms
|
||||
omarchy-pkg-add yt6801-dkms
|
||||
fi
|
||||
@@ -1,25 +0,0 @@
|
||||
# Install Panther Lake kernel for Dell XPS Panther Lake systems
|
||||
# The linux-ptl kernel includes audio driver patches not yet in mainline.
|
||||
|
||||
if omarchy-hw-match "XPS" && omarchy-hw-intel-ptl; then
|
||||
echo "Detected Dell XPS Panther Lake, installing PTL kernel..."
|
||||
|
||||
omarchy-pkg-add linux-ptl linux-ptl-headers
|
||||
pacman -Rdd --noconfirm linux linux-headers || true
|
||||
|
||||
# linux-ptl doesn't provide=linux, so anything depending on linux drags the
|
||||
# stock kernel back in and the boot menu grows a second, slower entry.
|
||||
if pacman -Qq linux &>/dev/null; then
|
||||
echo "WARNING: stock linux kernel still installed alongside linux-ptl:"
|
||||
pacman -Qi linux | grep -i "required by"
|
||||
fi
|
||||
|
||||
mkdir -p /etc/limine-entry-tool.d
|
||||
# Named to sort after omarchy-defaults.conf: drop-ins are read in order and
|
||||
# the last BOOT_ORDER wins, so an earlier-sorting name is a silent no-op.
|
||||
rm -f /etc/limine-entry-tool.d/dell-xps-panther-lake.conf
|
||||
cat > /etc/limine-entry-tool.d/zz-dell-xps-panther-lake.conf <<'EOF'
|
||||
# Only show Panther Lake kernel in boot menu on Dell XPS Panther Lake
|
||||
BOOT_ORDER="linux-ptl*, *fallback, Snapshots"
|
||||
EOF
|
||||
fi
|
||||
@@ -1,8 +1,4 @@
|
||||
if lspci | grep -qi 'nvidia'; then
|
||||
# Check which kernel is installed and set appropriate headers package
|
||||
KERNEL_PACKAGE=$(pacman -Qqs '^linux(-zen|-lts|-hardened|-t2|-ptl)?$' | head -1 || true)
|
||||
[[ -n $KERNEL_PACKAGE ]] && omarchy-pkg-add "$KERNEL_PACKAGE-headers"
|
||||
|
||||
if omarchy-hw-nvidia-gsp; then
|
||||
PACKAGES=(nvidia-open-dkms nvidia-utils lib32-nvidia-utils libva-nvidia-driver)
|
||||
elif omarchy-hw-nvidia-without-gsp; then
|
||||
|
||||
@@ -5,7 +5,7 @@ autoconf-archive
|
||||
asusctl
|
||||
base
|
||||
base-devel
|
||||
broadcom-wl
|
||||
broadcom-wl-dkms
|
||||
btrfs-progs
|
||||
dkms
|
||||
egl-wayland
|
||||
@@ -20,11 +20,9 @@ libva-nvidia-driver
|
||||
limine
|
||||
limine-mkinitcpio-hook
|
||||
limine-snapper-sync
|
||||
linux
|
||||
linux-firmware
|
||||
linux-headers
|
||||
linux-ptl
|
||||
linux-ptl-headers
|
||||
linux-omarchy
|
||||
linux-omarchy-headers
|
||||
macbook12-spi-driver-dkms
|
||||
nvidia-580xx-dkms
|
||||
nvidia-dkms
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# Update localdb so locate can find the installed system files immediately.
|
||||
updatedb
|
||||
# Match the scheduled service while installation runs without a system manager.
|
||||
updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots
|
||||
@@ -1,3 +1,7 @@
|
||||
# Upgrades must not delete the version a running process is executing from:
|
||||
# mise up would prune the old install dir out from under a live session.
|
||||
mise settings set upgrade.auto_prune false
|
||||
|
||||
omarchy-mise-install codex
|
||||
omarchy-mise-install claude
|
||||
omarchy-mise-install crush
|
||||
@@ -9,5 +13,16 @@ omarchy-mise-install npm:playwright playwright
|
||||
omarchy-mise-install pi
|
||||
omarchy-mise-install github:can1357/oh-my-pi omp
|
||||
omarchy-mise-install npm:@xai-official/grok grok
|
||||
# Cursor's own installer links the same path, so a re-provision keeps it.
|
||||
omarchy-cmd-missing cursor-agent && omarchy-mise-install cursor-agent
|
||||
omarchy-mise-install npm:@kitlangton/ghui ghui
|
||||
omarchy-mise-install aqua:modem-dev/hunk hunk
|
||||
# Every line above writes a stub and cannot fail. This one can: it exits
|
||||
# non-zero when Hermes Desktop owns Hermes but has not finished setting it up,
|
||||
# and this leaf is sourced under `bash -eE`, so that would abort the rest of
|
||||
# omarchy-provision-user -- the default browser, the mailto handler and the
|
||||
# finalize-user marker all come after it.
|
||||
omarchy-install-hermes-cli || true
|
||||
if omarchy-cmd-missing muse; then
|
||||
omarchy-mise-install "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]" muse
|
||||
fi
|
||||
+12
-3
@@ -13,6 +13,9 @@ Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a f
|
||||
| `grok` | Grok CLI from xAI |
|
||||
| `pi` | [Mario Zechner's Pi](https://github.com/badlogic/pi-mono) |
|
||||
| `omp` | [Oh My Pi](https://github.com/can1357/oh-my-pi) |
|
||||
| `hermes` | [Hermes](https://hermes-agent.nousresearch.com/), Nous Research's agent |
|
||||
| `muse` | [Muse Code](https://dev.meta.ai), Meta's coding agent |
|
||||
| `cursor-agent` | [Cursor CLI](https://cursor.com/cli) |
|
||||
|
||||
To wrap an additional CLI the same way, run `omarchy-mise-install <package> [command-name]`. The stubs are kept current along with everything else mise manages when you run `omarchy update` (or the `mup` alias).
|
||||
|
||||
@@ -20,9 +23,11 @@ To wrap an additional CLI the same way, run `omarchy-mise-install <package> [com
|
||||
|
||||
Pick your default agent with `omarchy default agent <name>` or under _Setup > Defaults > Agent_ in the Omarchy Menu (`Super + Space`). If the agent isn't installed yet, picking it installs it first. A fresh Omarchy will invite you to make this choice with a one-time notification.
|
||||
|
||||
[Muse Code](https://dev.meta.ai) — Meta's `muse` — uses a preinstalled mise stub like the other agents. Picking it as the default installs Meta's official launcher through mise's HTTP backend. The launcher verifies and updates the native binary for your machine.
|
||||
|
||||
Once you've chosen, `Super + Shift + Ctrl + A` launches the default agent in a dedicated terminal window (or brings up the picker if you haven't chosen yet). You can also launch it straight into a task with `omarchy agent prompt "Review this project"`. Agents launched this way run unattended in their respective don't-stop-to-ask modes, so be ready for them to actually do things! And since agents refuse to remember trust for your home directory, launches from `$HOME` start in `~/Work` instead.
|
||||
|
||||
There are terminal shortcuts too: `a` runs the default agent inline in the current terminal, while `c`, `cx`, and `cy` start OpenCode, Claude Code, and Codex directly (again in their auto-approving modes). Theme changes sync to the agents as well: Claude Code, Pi, and OpenCode all follow along when you switch the Omarchy theme.
|
||||
There are terminal shortcuts too: `a` runs the default agent inline in the current terminal, while `c`, `cx`, and `cy` start OpenCode, Claude Code, and Codex directly (again in their auto-approving modes). Theme changes sync to the agents as well: Claude Code, Pi, OpenCode, and Hermes (once Hermes Desktop is installed) all follow along when you switch the Omarchy theme.
|
||||
|
||||
### The agents panel
|
||||
|
||||
@@ -38,7 +43,11 @@ The watching is on by default. Turn it off under _Trigger > Toggle > Crash Captu
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app.
|
||||
|
||||
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too.
|
||||
|
||||
OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it.
|
||||
|
||||
### Local LLMs
|
||||
|
||||
@@ -46,6 +55,6 @@ Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. L
|
||||
|
||||
### The Omarchy Skill
|
||||
|
||||
Agent skills help AI use specific tools in a specific way, and Omarchy ships with a default skill for tailoring the system. Like tweaking your Hyprland config, adjusting the bar, or even creating a new theme from scratch. It's symlinked into the skill directories for Claude Code (`~/.claude/skills`), Codex (`~/.codex/skills`), Pi (`~/.pi/agent/skills`), and the generic `~/.agents/skills` location, so most harnesses pick it up automatically.
|
||||
Agent skills help AI use specific tools in a specific way, and Omarchy ships with a default skill for tailoring the system. Like tweaking your Hyprland config, adjusting the bar, or even creating a new theme from scratch. It's symlinked into the skill directories for Claude Code (`~/.claude/skills`), Codex (`~/.codex/skills`), Pi (`~/.pi/agent/skills`), Hermes (`~/.hermes/skills` and each `~/.hermes/profiles/*/skills`), and the generic `~/.agents/skills` location, so most harnesses pick it up automatically.
|
||||
|
||||
But you should treat this skill as experimental. Different models will use it to different effect. It's best to run in plan mode first, so you have an idea of what the agent would like to change. And then be ready to rollback changes or even invoking `omarchy reinstall configs`, if the agent makes a mess of everything.
|
||||
@@ -4,7 +4,7 @@ The Omarchy desktop runs as a single long-lived Quickshell process called `omarc
|
||||
|
||||
That's not just an implementation detail. It means you can turn pieces of the desktop off, swap them out, or write your own without touching a line of Omarchy's source.
|
||||
|
||||
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup; the only difference is where they sit on disk.
|
||||
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup, but built-ins receive trusted shell interfaces while third-party plugins receive a limited interface scoped to their own service and lifecycle. Clones of built-ins keep only the source-specific configuration and UI calls needed for the original behavior.
|
||||
|
||||
## Seeing what you have
|
||||
|
||||
@@ -37,7 +37,9 @@ A third-party plugin is just a git repo with a `manifest.json` at its root.
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
|
||||
```
|
||||
|
||||
Before it does anything, it tells you plainly that plugins run as arbitrary, unsandboxed code inside your long-lived shell process, shows you the URL, and asks you to confirm. Take that seriously. A plugin isn't a config file — it's code that runs for as long as your session does, with everything your user account can reach. Only add repos you're willing to run, and read them before you enable them.
|
||||
Before it does anything, it tells you plainly that plugins run as arbitrary, unsandboxed code inside your long-lived shell process, shows you the URL, and asks you to confirm. Take that seriously. The third-party plugin interface does not directly expose authentication services, and a replacement bar receives only limited capabilities for configured non-authentication UI. Visual plugins still share the shell's QML scene and can walk ordinary parent objects, while all plugin code runs with everything your user account can reach. Authentication state is protected separately by keeping those services outside the reachable host object graph. Only add repos you're willing to run, and read them before you enable them.
|
||||
|
||||
A replacement bar can render installed widgets, but service-backed third-party widgets may have reduced functionality there because the bar is not allowed to request another plugin's live service object. Switch back to the built-in `omarchy.bar` if such a widget needs its companion service.
|
||||
|
||||
Then it clones the repo into a staging directory, validates the manifest, refuses the install if another plugin already claims that id, and moves it into `~/.config/omarchy/plugins/<id>/`. Without `--enable` it asks whether you want it on now, and you can say no and go read the code first. It never runs anything from the plugin, never executes an install hook, and never asks for sudo — it clones files, checks the manifest, and flips a bit over IPC.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ It works by restoring the baseline snapshot the installer takes, so it's only av
|
||||
|
||||
## Passwordless sudo
|
||||
|
||||
Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ turns that off for 15 minutes and then puts it back automatically. Run it again before the timer runs out to end it early, and pass your own number of minutes with `omarchy-sudo-passwordless 30` if 15 isn't enough.
|
||||
Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ turns that off for 15 minutes and then puts it back automatically. Run it again before the timer runs out to end it early, and pass your own number of minutes with `omarchy-sudo-passwordless 30` if 15 isn't enough. A restart removes the passwordless sudo rule as well.
|
||||
|
||||
Be clear-eyed about this one: while it's on, anything running as your user can do anything as root without being asked. That's the whole point, and it's also the whole risk.
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
echo "Configure locate to skip Btrfs snapshots and index Btrfs subvolumes"
|
||||
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
locate_config_script="$OMARCHY_PATH/install/config/locate.sh"
|
||||
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
[[ -f $UPDATEDB_CONF_PATH ]] || exit 0
|
||||
[[ -f $locate_config_script ]] || exit 0
|
||||
|
||||
if grep -q '^PRUNE_BIND_MOUNTS = "no"' "$UPDATEDB_CONF_PATH" &&
|
||||
grep -E '^PRUNEPATHS' "$UPDATEDB_CONF_PATH" | grep -E '(^|[[:space:]"])/\.snapshots([[:space:]"]|$)' >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
as_root env OMARCHY_UPDATEDB_CONF_PATH="$UPDATEDB_CONF_PATH" bash -euo pipefail "$locate_config_script"
|
||||
|
||||
# Rebuild the index with the new exclusions; pruning /.snapshots turns
|
||||
# multi-hour runs on snapshot-heavy systems back into one-minute runs. Restart
|
||||
# rather than start: the machines this targets are the ones with an updatedb
|
||||
# already grinding through every snapshot, and a run that started before the
|
||||
# rewrite keeps using the config it read at startup.
|
||||
as_root systemctl restart --no-block plocate-updatedb.service >/dev/null 2>&1 || true
|
||||
@@ -1,17 +1,8 @@
|
||||
echo "Switch fingerprint support back to stock libfprint"
|
||||
echo "Repair fingerprint support left without a libfprint"
|
||||
|
||||
# libfprint-git existed to carry the focaltech_moc driver and the FocalTech
|
||||
# FT9349 device ID (2808:a97a) before any release shipped them. libfprint
|
||||
# 1.94.100 has both, so fingerprint setups go back to the stock Arch package.
|
||||
|
||||
# The remove/install pair below isn't one transaction: if the install failed
|
||||
# on a previous run, libfprint-git is already gone but fprintd is left with
|
||||
# no libfprint — the elif finishes the job on rerun.
|
||||
if pacman -Q libfprint-git &>/dev/null; then
|
||||
# Deps-only removal keeps fprintd installed while its libfprint
|
||||
# dependency is swapped out underneath it.
|
||||
sudo pacman -Rdd --noconfirm libfprint-git
|
||||
omarchy-pkg-add libfprint
|
||||
elif pacman -Q fprintd &>/dev/null && ! pacman -Q libfprint &>/dev/null; then
|
||||
omarchy-pkg-add libfprint
|
||||
# An earlier version of this migration swapped libfprint-git for stock
|
||||
# libfprint in two steps. A run that failed between them left fprintd with
|
||||
# no library; finish with the driver the fingerprint setup installs now.
|
||||
if omarchy-pkg-present fprintd && omarchy-pkg-missing libfprint && omarchy-pkg-missing libfprint-git; then
|
||||
omarchy-pkg-add libfprint-git
|
||||
fi
|
||||
@@ -0,0 +1,3 @@
|
||||
echo "Stop mise upgrades from pruning versions still in use"
|
||||
|
||||
mise settings set upgrade.auto_prune false
|
||||
@@ -0,0 +1,25 @@
|
||||
echo "Install the Hermes CLI wrapper for existing installs"
|
||||
|
||||
# Users who removed the preinstalls opted out of the mise wrappers, and Hermes
|
||||
# is one of them.
|
||||
[[ -f $HOME/.local/state/omarchy/preinstalls-removed ]] && exit 0
|
||||
|
||||
# Hermes Desktop provides its own Hermes. The installer stands aside for it,
|
||||
# removing the mise copy and the Omarchy wrapper an earlier install may have
|
||||
# left beside the app. It also reports when the app has not finished setting
|
||||
# Hermes up, which is the app's to finish, not this migration's to fail on.
|
||||
if omarchy-pkg-present hermes-desktop; then
|
||||
omarchy-install-hermes-cli || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Anything already answering to hermes that this installer did not write --
|
||||
# an official install, a hand-rolled wrapper, even a dangling link -- belongs to
|
||||
# the user and stays exactly as it is. The installer is asked rather than
|
||||
# matched against here, so there is one answer to who owns that wrapper.
|
||||
wrapper="$HOME/.local/bin/hermes"
|
||||
if [[ -e $wrapper || -L $wrapper ]] && ! omarchy-install-hermes-cli --owns; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
omarchy-install-hermes-cli
|
||||
@@ -0,0 +1,22 @@
|
||||
echo "Link Omarchy agent skills into Hermes skill directories"
|
||||
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
skills_source="$OMARCHY_PATH/default/agents/skills"
|
||||
|
||||
[[ -d $skills_source ]] || exit 0
|
||||
|
||||
mkdir -p "$HOME/.hermes/skills"
|
||||
|
||||
for skill in "$skills_source"/*/; do
|
||||
[[ -d $skill ]] || continue
|
||||
name=${skill%/}
|
||||
name=${name##*/}
|
||||
ln -sfn "$skills_source/$name" "$HOME/.hermes/skills/$name"
|
||||
if [[ -d $HOME/.hermes/profiles ]]; then
|
||||
for profile in "$HOME"/.hermes/profiles/*/; do
|
||||
[[ -d $profile ]] || continue
|
||||
mkdir -p "$profile/skills"
|
||||
ln -sfn "$skills_source/$name" "$profile/skills/$name"
|
||||
done
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,8 @@
|
||||
echo "Install Cursor CLI via mise wrapper"
|
||||
|
||||
# Cursor's own installer links ~/.local/bin/cursor-agent, so an existing
|
||||
# command is the user's and stays. The wrapper resolves cursor-agent through
|
||||
# mise's registry, which lists it from 2026.8.15 on.
|
||||
if omarchy-cmd-missing cursor-agent && [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install cursor-agent
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
echo "Hand Hermes Desktop the Omarchy theme as a skin"
|
||||
|
||||
# Only the app Omarchy installed under Install > AI follows the theme by itself.
|
||||
# A Hermes the user set up some other way keeps whatever skin they chose.
|
||||
omarchy-pkg-present hermes-desktop || exit 0
|
||||
|
||||
# The same hand-over a fresh install does. A Hermes that is not ready or refuses
|
||||
# the write is reported and done with there; only Omarchy's own failures return.
|
||||
omarchy-theme-set-hermes --activate
|
||||
@@ -0,0 +1,303 @@
|
||||
echo "Repair user-owned system-sleep hooks and hybrid GPU service configuration"
|
||||
|
||||
system_sleep_dir=/usr/lib/systemd/system-sleep
|
||||
supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf
|
||||
quarantine_root=/var/lib/omarchy/migrations/1788662350-system-sleep
|
||||
reload_needed_marker=/var/lib/omarchy/migrations/1788662350-systemd-reload-needed
|
||||
keyboard_source="$OMARCHY_PATH/default/systemd/system-sleep/keyboard-backlight"
|
||||
force_igpu_source="$OMARCHY_PATH/default/systemd/system-sleep/force-igpu"
|
||||
supergfxd_source="$OMARCHY_PATH/default/systemd/system/supergfxd.service.d/delay-start.conf"
|
||||
legacy_keyboard_sha256=f313a81e47401f0d38b8602e5997f52c5286d5e97f74027564ddd515b3d16511
|
||||
legacy_force_igpu_sha256=d604e7c4903829563e45fc52188fc5602c3f1bc66e247f0a2cc0a974ed6e57db
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
path_is_root_controlled() {
|
||||
local path="$1"
|
||||
local current=/ component candidate file_mode link metadata part status uid gid mode
|
||||
local missing_depth=0 symlink_count=0
|
||||
local -a pending resolved link_components
|
||||
|
||||
[[ $path == /* ]] || return 1
|
||||
IFS=/ read -r -a pending <<<"$path"
|
||||
# A non-root group is harmless when neither it nor everyone else can write.
|
||||
# Resolve symlinks component by component so an indirect link cannot hide an
|
||||
# intermediate directory controlled by an unprivileged user.
|
||||
metadata=$(path_metadata /) || return 1
|
||||
read -r file_mode uid gid mode <<<"$metadata"
|
||||
(( uid == 0 && (8#$mode & 8#022) == 0 )) || return 1
|
||||
|
||||
while ((${#pending[@]})); do
|
||||
component=${pending[0]}
|
||||
pending=("${pending[@]:1}")
|
||||
[[ -n $component ]] || continue
|
||||
[[ $component == "." ]] && continue
|
||||
|
||||
if [[ $component == ".." ]]; then
|
||||
if ((${#resolved[@]})); then
|
||||
unset 'resolved[-1]'
|
||||
fi
|
||||
|
||||
current=/
|
||||
for part in "${resolved[@]}"; do
|
||||
if [[ $current == "/" ]]; then
|
||||
current="/$part"
|
||||
else
|
||||
current="$current/$part"
|
||||
fi
|
||||
done
|
||||
if (( missing_depth > 0 && ${#resolved[@]} < missing_depth )); then
|
||||
missing_depth=0
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ $current == "/" ]]; then
|
||||
candidate="/$component"
|
||||
else
|
||||
candidate="$current/$component"
|
||||
fi
|
||||
|
||||
if (( missing_depth > 0 )); then
|
||||
# The first missing component makes descendants inactive today, but keep
|
||||
# consuming the lexical suffix. A later .. can escape back into an
|
||||
# existing user-controlled path that would become active if an
|
||||
# administrator creates the missing directory.
|
||||
resolved+=("$component")
|
||||
current=$candidate
|
||||
continue
|
||||
elif metadata=$(path_metadata "$candidate"); then
|
||||
read -r file_mode uid gid mode <<<"$metadata"
|
||||
else
|
||||
status=$?
|
||||
if (( status == 2 )); then
|
||||
resolved+=("$component")
|
||||
current=$candidate
|
||||
missing_depth=${#resolved[@]}
|
||||
continue
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if (( (16#$file_mode & 16#f000) == 16#a000 )); then
|
||||
((++symlink_count <= 40)) || return 1
|
||||
link=$(readlink_with_privilege "$candidate") || return 1
|
||||
IFS=/ read -r -a link_components <<<"$link"
|
||||
pending=("${link_components[@]}" "${pending[@]}")
|
||||
if [[ $link == /* ]]; then
|
||||
resolved=()
|
||||
current=/
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
(( uid == 0 && (8#$mode & 8#022) == 0 )) || return 1
|
||||
resolved+=("$component")
|
||||
current=$candidate
|
||||
done
|
||||
}
|
||||
|
||||
path_metadata() {
|
||||
local path="$1"
|
||||
local metadata parent
|
||||
|
||||
if /usr/bin/stat -c '%f %u %g %a' -- "$path" 2>/dev/null; then
|
||||
return 0
|
||||
elif [[ ! -e $path && ! -L $path ]]; then
|
||||
parent=${path%/*}
|
||||
[[ -n $parent ]] || parent=/
|
||||
# Avoid asking for sudo for ordinary ENOENT. If the parent is searchable,
|
||||
# the absence is conclusive; an inaccessible root-only chain still needs a
|
||||
# privileged metadata check so safe administrator symlinks are preserved.
|
||||
[[ -x $parent ]] && return 2
|
||||
if metadata=$(as_root /usr/bin/stat -c '%f %u %g %a' -- "$path" 2>/dev/null); then
|
||||
printf '%s\n' "$metadata"
|
||||
return 0
|
||||
elif as_root /usr/bin/test -x "$parent"; then
|
||||
# The privileged probe could search the protected parent, so stat's
|
||||
# failure identifies a target that does not exist yet.
|
||||
return 2
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
as_root /usr/bin/stat -c '%f %u %g %a' -- "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
readlink_with_privilege() {
|
||||
local path="$1"
|
||||
|
||||
if /usr/bin/readlink -- "$path" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
as_root /usr/bin/readlink -- "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
privileged_entry_is_safe() {
|
||||
local path="$1"
|
||||
|
||||
path_is_root_controlled "$path"
|
||||
}
|
||||
|
||||
file_matches_source() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
[[ -f $destination && ! -L $destination ]] || return 1
|
||||
|
||||
if [[ -r $destination ]]; then
|
||||
/usr/bin/cmp -s -- "$source" "$destination"
|
||||
else
|
||||
as_root /usr/bin/cmp -s -- "$source" "$destination"
|
||||
fi
|
||||
}
|
||||
|
||||
file_matches_sha256() {
|
||||
local destination="$1"
|
||||
local expected="$2"
|
||||
local digest
|
||||
|
||||
[[ -f $destination && ! -L $destination ]] || return 1
|
||||
if [[ -r $destination ]]; then
|
||||
digest=$(/usr/bin/sha256sum -- "$destination") || return 1
|
||||
else
|
||||
digest=$(as_root /usr/bin/sha256sum -- "$destination") || return 1
|
||||
fi
|
||||
[[ ${digest%% *} == "$expected" ]]
|
||||
}
|
||||
|
||||
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}$ ]]
|
||||
}
|
||||
|
||||
install_root_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="$3"
|
||||
local stage
|
||||
|
||||
stage=$(as_root /usr/bin/mktemp -- "${destination%/*}/.${destination##*/}.omarchy.XXXXXX") || return 1
|
||||
safe_stage_path "$stage" "$destination" || return 1
|
||||
|
||||
if as_root /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage" &&
|
||||
as_root /usr/bin/mv -Tf -- "$stage" "$destination"; then
|
||||
return 0
|
||||
else
|
||||
safe_stage_path "$stage" "$destination" && as_root /usr/bin/rm -f -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
preserve_unsafe_customization() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
local backup_dir backup
|
||||
|
||||
if ! as_root /usr/bin/install -d -m 0700 -o root -g root "$quarantine_root"; then
|
||||
echo "Could not create the root-only system-sleep quarantine at $quarantine_root" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! backup_dir=$(as_root /usr/bin/mktemp -d -- "$quarantine_root/${label}.XXXXXX"); then
|
||||
echo "Could not reserve a quarantine path for $path" >&2
|
||||
return 1
|
||||
fi
|
||||
backup="$backup_dir/original"
|
||||
|
||||
if as_root /usr/bin/cp -a --no-dereference -T -- "$path" "$backup"; then
|
||||
printf '%s\n' "$backup"
|
||||
else
|
||||
as_root /usr/bin/rm -rf -- "$backup_dir"
|
||||
echo "Could not preserve unsafe custom content from $path before repairing it" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
repair_unsafe_privileged_entry() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="$3"
|
||||
local label="$4"
|
||||
local legacy_sha256="${5:-}"
|
||||
local backup current_mode
|
||||
|
||||
[[ -e $destination || -L $destination ]] || return 0
|
||||
[[ -f $destination || -L $destination ]] || return 0
|
||||
|
||||
if file_matches_source "$source" "$destination"; then
|
||||
current_mode=$(/usr/bin/stat -c '%a' -- "$destination" 2>/dev/null) ||
|
||||
current_mode=$(as_root /usr/bin/stat -c '%a' -- "$destination") || return 1
|
||||
if privileged_entry_is_safe "$destination" && [[ $current_mode == "${mode#0}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
elif [[ -n $legacy_sha256 ]] && file_matches_sha256 "$destination" "$legacy_sha256"; then
|
||||
:
|
||||
else
|
||||
privileged_entry_is_safe "$destination" && return 0
|
||||
backup=$(preserve_unsafe_customization "$destination" "$label") || return 1
|
||||
fi
|
||||
|
||||
if install_root_file "$source" "$destination" "$mode"; then
|
||||
if [[ -n ${backup:-} ]]; then
|
||||
echo "Preserved unsafe custom content from $destination at $backup for administrator review" >&2
|
||||
fi
|
||||
else
|
||||
if [[ -n ${backup:-} ]]; then
|
||||
echo "Preserved unsafe custom content from $destination at $backup, but could not repair the active path" >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Replace rather than chown an unsafe destination: its current owner may have
|
||||
# already changed the contents or kept a writable file descriptor open. The
|
||||
# root-owned staging inode makes the final rename an atomic trust transition.
|
||||
repair_unsafe_privileged_entry "$keyboard_source" \
|
||||
"$system_sleep_dir/keyboard-backlight" 0755 keyboard-backlight "$legacy_keyboard_sha256"
|
||||
|
||||
force_igpu="$system_sleep_dir/force-igpu"
|
||||
repair_unsafe_privileged_entry "$force_igpu_source" "$force_igpu" 0755 force-igpu "$legacy_force_igpu_sha256"
|
||||
|
||||
systemd_reload_needed=false
|
||||
if [[ -e $reload_needed_marker || -L $reload_needed_marker ]]; then
|
||||
systemd_reload_needed=true
|
||||
fi
|
||||
|
||||
if [[ -e $supergfxd_drop_in || -L $supergfxd_drop_in ]]; then
|
||||
if ! privileged_entry_is_safe "$supergfxd_drop_in"; then
|
||||
# Replacing the drop-in and reloading systemd are one repair. Record the
|
||||
# second half before changing the file so failure or interruption cannot
|
||||
# be forgotten when a retry sees only the trusted replacement on disk.
|
||||
if ! as_root /usr/bin/install -Dm0644 -o root -g root /dev/null "$reload_needed_marker"; then
|
||||
echo "Could not persist the pending systemd reload for the repaired supergfxd configuration" >&2
|
||||
exit 1
|
||||
fi
|
||||
systemd_reload_needed=true
|
||||
repair_unsafe_privileged_entry "$supergfxd_source" "$supergfxd_drop_in" 0644 delay-start.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
if $systemd_reload_needed; then
|
||||
if ! as_root /usr/bin/systemctl daemon-reload; then
|
||||
echo "Could not reload systemd after repairing the supergfxd configuration; the migration will retry" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! as_root /usr/bin/rm -f -- "$reload_needed_marker"; then
|
||||
echo "Could not clear the pending systemd reload marker; the migration will retry" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,5 @@
|
||||
echo "Install Muse Code via mise wrapper"
|
||||
|
||||
if omarchy-cmd-missing muse && [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install "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]" muse
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
echo "Update Kitty configuration"
|
||||
|
||||
kitty_config="$HOME/.config/kitty/kitty.conf"
|
||||
# config/kitty/kitty.conf as shipped after 008f3a22 (Kitty cwd lookup).
|
||||
stock_sha="856cd466bf568d091cb775c5b90d1852178090a419f492fde02a4eaef6407bf9"
|
||||
unrestricted='^[[:space:]]*allow_remote_control[[:space:]]+(yes|y|true)[[:space:]]*$'
|
||||
|
||||
if [[ -f $kitty_config ]]; then
|
||||
changed=false
|
||||
|
||||
if [[ $(sha256sum "$kitty_config" | cut -d ' ' -f 1) == $stock_sha ]]; then
|
||||
omarchy-refresh-config kitty/kitty.conf
|
||||
changed=true
|
||||
elif grep -qE "$unrestricted" "$kitty_config"; then
|
||||
# Preserve customizations and ordering. An otherwise stock line can be an
|
||||
# intentional override of an earlier include or mapping.
|
||||
backup=$(mktemp "$kitty_config.bak.XXXXXX")
|
||||
cp -p "$kitty_config" "$backup"
|
||||
sed --follow-symlinks -i -E "s/$unrestricted/# &/" "$kitty_config"
|
||||
printf '\n%s\n' \
|
||||
"Unrestricted remote control disabled." \
|
||||
"Your other Kitty settings were preserved."
|
||||
printf '\nBackup saved to:\n %s\n' "$backup"
|
||||
changed=true
|
||||
fi
|
||||
|
||||
if [[ $changed == "true" ]]; then
|
||||
# Kitty reads allow_remote_control at startup; config reload is insufficient.
|
||||
gum style --border rounded --border-foreground 3 --padding "1 2" --margin "1 0" \
|
||||
"Restart Kitty" "" \
|
||||
"Close and reopen all Kitty windows to apply this change."
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,21 @@
|
||||
echo "Retire the stock user icon font missed by the Quattro upgrade"
|
||||
|
||||
legacy_font="$HOME/.local/share/fonts/omarchy.ttf"
|
||||
|
||||
# The upgrader treated this as ~/.config/omarchy.ttf and left the old family
|
||||
# registered alongside the packaged font. Preserve custom fonts and symlinks.
|
||||
if [[ -f $legacy_font && ! -L $legacy_font ]]; then
|
||||
legacy_hash=$(sha256sum "$legacy_font")
|
||||
if [[ ${legacy_hash%% *} == "e55e67119e82f56f92d90cbf54b7ccc1b2946b32c535a29370439d7ef5215966" ]]; then
|
||||
if [[ ! -f /usr/share/fonts/omarchy/omarchy.ttf ]]; then
|
||||
echo "Packaged Omarchy icon font is missing; keeping the legacy font." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "$legacy_font"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also refresh on retries after removal succeeded but the cache refresh failed.
|
||||
# The normal update restarts the shell, which reloads Qt's font database.
|
||||
fc-cache -f
|
||||
@@ -0,0 +1,44 @@
|
||||
echo "Install the Omarchy kernel and make it the first Limine boot entry"
|
||||
|
||||
# linux-omarchy is an x86_64 kernel. T2 Macs must keep their specialized kernel,
|
||||
# including when other kernels are installed or the running T2 package is gone.
|
||||
[[ $(uname -m) == "x86_64" ]] || exit 0
|
||||
running_kernel=$(uname -r)
|
||||
if omarchy-pkg-present linux-t2 || [[ ${running_kernel,,} == *-t2* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
limine_conf="${OMARCHY_KERNEL_LIMINE_CONF:-/etc/default/limine}"
|
||||
rebuild_marker="${OMARCHY_KERNEL_REBUILD_MARKER:-/var/lib/omarchy/migrations/1789325478}"
|
||||
kernel="linux-omarchy"
|
||||
|
||||
# Completion is machine-wide even though migrations run once per user. Leave
|
||||
# the old kernel installed so it remains available if the new one cannot boot.
|
||||
# The new filename and marker also reach users who completed 1789095456.
|
||||
[[ ! -e $rebuild_marker ]] || exit 0
|
||||
omarchy-pkg-add "$kernel" "$kernel-headers"
|
||||
|
||||
# /etc/default/limine has priority over every drop-in, including old Dell
|
||||
# settings and customized package files whose updates landed in a .pacnew.
|
||||
# Set the exact kernel first: linux-omarchy-* only matches its older variants.
|
||||
# Preserve unrelated settings, especially the root filesystem's kernel cmdline.
|
||||
sudo mkdir -p "$(dirname "$limine_conf")"
|
||||
sudo touch "$limine_conf"
|
||||
sudo sed -i -E '/^[[:space:]]*BOOT_ORDER[[:space:]]*=/d' "$limine_conf"
|
||||
printf '\n%s\n' 'BOOT_ORDER="linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' | sudo tee -a "$limine_conf" >/dev/null
|
||||
|
||||
# Package hooks ran before the config repair. Rebuild the new kernel's image
|
||||
# and boot entry explicitly, including on retries after a failed rebuild.
|
||||
sudo limine-mkinitcpio "$kernel"
|
||||
|
||||
# limine-mkinitcpio can return success after skipping a failed kernel build.
|
||||
# Do not mark the migration complete unless the new kernel is in the menu.
|
||||
if ! sudo limine-entry-tool --tree | grep -E "(^|[^[:alnum:]_-])$kernel([^[:alnum:]_-]|$)" >/dev/null; then
|
||||
echo "The Omarchy kernel has no Limine boot entry; rerun omarchy-migrate after fixing the boot image build." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keeping the running kernel installed prevents the updater from detecting a
|
||||
# kernel replacement, so request the reboot explicitly.
|
||||
omarchy-state set reboot-required
|
||||
sudo install -Dm644 /dev/null "$rebuild_marker"
|
||||
@@ -0,0 +1,10 @@
|
||||
echo "Install missing headers for the Omarchy or T2 kernel"
|
||||
|
||||
# Fresh ISO installs mark earlier migrations complete, so the kernel migration
|
||||
# cannot repair headers omitted by those installers. Package installation is
|
||||
# idempotent when another user has already applied this repair.
|
||||
for kernel in linux-omarchy linux-t2; do
|
||||
if omarchy-pkg-present "$kernel"; then
|
||||
omarchy-pkg-add "$kernel-headers"
|
||||
fi
|
||||
done
|
||||
+14
-10
@@ -86,8 +86,16 @@ Only one `bar` plugin is active at a time. Missing or invalid selections fall
|
||||
back to the built-in `omarchy.bar`, so users always have a safe path home.
|
||||
Panels, overlays, and menus are loaded when summoned. Plugins that need
|
||||
to outlive a single summon can set `keepLoaded: true` (e.g. the image
|
||||
picker keeps its overlay window mounted between summons). First-party
|
||||
services are loaded at startup.
|
||||
picker keeps its overlay window mounted between summons). The same flag
|
||||
keeps a service mounted across plugin hot-reload, so tearing down a
|
||||
changed bar widget cannot destroy `omarchy.lock` while Hyprland still
|
||||
holds the session lock. The kept instance is not replaced, so code
|
||||
changes to a `keepLoaded` service itself only take effect on a shell
|
||||
restart. First-party services are loaded at startup.
|
||||
|
||||
Entry points may declare `omarchyPath`, `shell`, `manifest`, `pluginRegistry`, and `barWidgetRegistry` properties for host injection. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades: ordinary plugins can 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. A full-bar plugin additionally receives 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 are stamped from trusted first-party manifests, authentication services are retained outside the host's public service map and QML object tree, and changing a third-party registry or configuration snapshot cannot mutate host state. Facades do not isolate visual widgets from the parent hierarchy of the shared QML scene, so sensitive state must remain outside that reachable graph.
|
||||
|
||||
Widgets rendered by a third-party replacement bar receive a service-less entry facade with target-scoped lifecycle and settings operations. Their live service objects are available only when the trusted built-in bar hosts them; otherwise the replacement bar could request and retain any configured widget's service.
|
||||
|
||||
The full schema lives in `services/PluginRegistry.qml`.
|
||||
|
||||
@@ -104,15 +112,11 @@ omarchy plugin update # updates every git-managed plugin
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns
|
||||
> you before cloning, plugins land disabled so you can review the code before
|
||||
> enabling, and updates show a diff of the changes before touching anything.
|
||||
> Only add repos whose code you are willing to run.
|
||||
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns you before cloning, plugins land disabled so you can review the code before enabling, and updates show a diff of the changes before touching anything. The scoped QML interfaces remove direct authentication-service and generic replacement-bar service lookups, but visual plugins still share and can traverse the ordinary host scene. Only add repos whose code you are willing to run.
|
||||
|
||||
Each command is **interactive** when run bare in a terminal (gum pickers,
|
||||
confirmation, a diff to review) and fully **non-interactive** when given
|
||||
arguments. Pass `--yes` to skip every prompt — this is the path for scripts and
|
||||
AI agents:
|
||||
Add, update, and remove commands confirm in a terminal even when given
|
||||
arguments; without a terminal they refuse rather than guess. Pass `--yes` to
|
||||
skip every prompt — this is the path for scripts and AI agents:
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
|
||||
// Bar surface exposed to an installed third-party widget. Scalar presentation
|
||||
// state is mirrored by Bar.qml and operations are delegated through scoped
|
||||
// callbacks. The facade avoids direct host-Bar injection; it cannot isolate a
|
||||
// visual child from the parent hierarchy of the QML scene that renders it.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
required property string moduleName
|
||||
property var shell: null
|
||||
|
||||
property color foreground: "transparent"
|
||||
property color barForeground: "transparent"
|
||||
property color background: "transparent"
|
||||
property color urgent: "transparent"
|
||||
property string fontFamily: ""
|
||||
property string position: "top"
|
||||
property bool vertical: false
|
||||
property int barSize: 0
|
||||
property bool transparent: false
|
||||
property bool foregroundAnimationEnabled: true
|
||||
property bool centerSectionRevealHeld: false
|
||||
property bool _centerHoverRevealSuppressed: false
|
||||
readonly property bool centerHoverRevealSuppressed: _centerHoverRevealSuppressed
|
||||
property var activePopout: null
|
||||
property var clickTargets: []
|
||||
property var layoutConfig: ({})
|
||||
readonly property var foreignPopoutMarker: ({ foreign: true })
|
||||
|
||||
property var _showTooltip: null
|
||||
property var _hideTooltip: null
|
||||
property var _registerClickTarget: null
|
||||
property var _unregisterClickTarget: null
|
||||
property var _requestPopout: null
|
||||
property var _releasePopout: null
|
||||
property var _switchPanelFrom: null
|
||||
property var _targetBelongsToWindow: null
|
||||
property var _moduleWidgets: null
|
||||
property var _run: null
|
||||
property var _setCenterHoverRevealSuppressed: null
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (_setCenterHoverRevealSuppressed) _setCenterHoverRevealSuppressed(!!value)
|
||||
}
|
||||
|
||||
function showTooltip(target, text) {
|
||||
if (_showTooltip) _showTooltip(target, String(text || ""))
|
||||
}
|
||||
|
||||
function hideTooltip(target) {
|
||||
if (_hideTooltip) _hideTooltip(target)
|
||||
}
|
||||
|
||||
function registerClickTarget(target) {
|
||||
if (_registerClickTarget) _registerClickTarget(target)
|
||||
}
|
||||
|
||||
function unregisterClickTarget(target) {
|
||||
if (_unregisterClickTarget) _unregisterClickTarget(target)
|
||||
}
|
||||
|
||||
function requestPopout(owner) {
|
||||
if (_requestPopout) _requestPopout(owner)
|
||||
}
|
||||
|
||||
function releasePopout(owner) {
|
||||
if (_releasePopout) _releasePopout(owner)
|
||||
}
|
||||
|
||||
function switchPanelFrom(owner, direction) {
|
||||
return _switchPanelFrom ? _switchPanelFrom(owner, direction) : false
|
||||
}
|
||||
|
||||
function targetBelongsToWindow(target, window) {
|
||||
return _targetBelongsToWindow ? _targetBelongsToWindow(target, window) : false
|
||||
}
|
||||
|
||||
function moduleWidgets(id) {
|
||||
return _moduleWidgets ? _moduleWidgets(String(id || "")) : []
|
||||
}
|
||||
|
||||
function run(command) {
|
||||
if (_run) _run(String(command || ""))
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
|
||||
PanelSeparator 1.0 PanelSeparator.qml
|
||||
PanelSlider 1.0 PanelSlider.qml
|
||||
PanelToolTip 1.0 PanelToolTip.qml
|
||||
PluginBarApi 1.0 PluginBarApi.qml
|
||||
PointerMoveGate 1.0 PointerMoveGate.qml
|
||||
ScreenMoveRemap 1.0 ScreenMoveRemap.qml
|
||||
PopupCard 1.0 PopupCard.qml
|
||||
|
||||
@@ -83,6 +83,9 @@ separate PAM services: `omarchy-lock-password` for password auth and,
|
||||
only when fingerprints are enrolled, `omarchy-lock-fingerprint` for
|
||||
fingerprint auth. It mirrors the previous lock screen field dimensions,
|
||||
colors, blurred wallpaper, placeholder, and Hyprland-driven corners.
|
||||
The plugin sets `keepLoaded: true` so a plugin hot-reload (for example
|
||||
an installed bar widget changing on disk) does not destroy the lock
|
||||
client while Hyprland still holds the session lock.
|
||||
|
||||
## Polkit agent
|
||||
|
||||
|
||||
+238
-4
@@ -12,20 +12,29 @@ Item {
|
||||
id: root
|
||||
|
||||
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
|
||||
required property string omarchyPath
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
// Injected by the host shell so bar slots can resolve enabled widgets.
|
||||
required property var barWidgetRegistry
|
||||
property var barWidgetRegistry: fallbackBarWidgetRegistry
|
||||
// Read-only registry view for third-party full bars; the built-in bar does
|
||||
// not otherwise need it, but declaring it keeps clone construction atomic.
|
||||
property var pluginRegistry: null
|
||||
// Injected by the host shell every time shell.json is reloaded. Holds the
|
||||
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
|
||||
// the bar just renders whatever it's handed. The bar font follows the
|
||||
// OS-level fontconfig monospace binding — it is not stored in shell.json.
|
||||
required property var barConfig
|
||||
property var barConfig: ({})
|
||||
// Injected by the host shell. Used for shell-wide actions such as opening
|
||||
// settings and persisting inline widget state.
|
||||
property var shell: null
|
||||
// Manifest for the active bar option. Present for custom bars and useful for
|
||||
// diagnostics; the built-in bar does not otherwise need it.
|
||||
property var manifest: null
|
||||
QtObject {
|
||||
id: fallbackBarWidgetRegistry
|
||||
property var widgets: ({})
|
||||
property int revision: 0
|
||||
function metadataFor(id) { return null }
|
||||
}
|
||||
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
|
||||
// killing the entire shell. Hidden panels stay mapped but park off-screen
|
||||
// without an exclusion zone; updated by the FileView watcher further down.
|
||||
@@ -100,6 +109,223 @@ Item {
|
||||
property var barMoveScreen: null
|
||||
property var clickTargets: []
|
||||
property var moduleSlots: []
|
||||
property var pluginBarApis: ({})
|
||||
property var pluginObjectOwners: []
|
||||
|
||||
Component {
|
||||
id: pluginBarApiComponent
|
||||
PluginBarApi { }
|
||||
}
|
||||
|
||||
function publicLayoutConfig() {
|
||||
return JSON.parse(JSON.stringify(root.layoutConfig || {}))
|
||||
}
|
||||
|
||||
function bindPluginBarApi(api) {
|
||||
if (!api) return
|
||||
api.foreground = Qt.binding(function() { return root.foreground })
|
||||
api.barForeground = Qt.binding(function() { return root.barForeground })
|
||||
api.background = Qt.binding(function() { return root.background })
|
||||
api.urgent = Qt.binding(function() { return root.urgent })
|
||||
api.fontFamily = Qt.binding(function() { return root.fontFamily })
|
||||
api.position = Qt.binding(function() { return root.position })
|
||||
api.vertical = Qt.binding(function() { return root.vertical })
|
||||
api.barSize = Qt.binding(function() { return root.barSize })
|
||||
api.transparent = Qt.binding(function() { return root.transparent })
|
||||
api.foregroundAnimationEnabled = Qt.binding(function() { return root.foregroundAnimationEnabled })
|
||||
api.centerSectionRevealHeld = Qt.binding(function() { return root.centerSectionRevealHeld })
|
||||
api._centerHoverRevealSuppressed = Qt.binding(function() { return root.centerHoverRevealSuppressed })
|
||||
root.syncPluginBarApiObjects(api)
|
||||
}
|
||||
|
||||
function syncPluginBarApiObjects(api) {
|
||||
if (!api) return
|
||||
api.activePopout = root.pluginOwnsBarObject(api.pluginId, root.activePopout)
|
||||
? root.activePopout : (root.activePopout ? api.foreignPopoutMarker : null)
|
||||
api.clickTargets = root.pluginClickTargets(api.pluginId)
|
||||
api.layoutConfig = root.publicLayoutConfig()
|
||||
}
|
||||
|
||||
function pluginObjectRecord(target) {
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var record = pluginObjectOwners[i]
|
||||
if (record && record.target === target) return record
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function markPluginObject(pluginId, target, role) {
|
||||
var key = String(pluginId || "")
|
||||
if (!key || !target) return false
|
||||
var record = root.pluginObjectRecord(target)
|
||||
if (record && record.pluginId !== key) return false
|
||||
var next = []
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var existing = pluginObjectOwners[i]
|
||||
if (!existing || existing.target !== target) next.push(existing)
|
||||
}
|
||||
var updated = record || { target: target, pluginId: key, clickTarget: false, popout: false }
|
||||
updated[role] = true
|
||||
next.push(updated)
|
||||
pluginObjectOwners = next
|
||||
return true
|
||||
}
|
||||
|
||||
function unmarkPluginObject(pluginId, target, role) {
|
||||
var key = String(pluginId || "")
|
||||
var next = []
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var record = pluginObjectOwners[i]
|
||||
if (!record || record.target !== target || record.pluginId !== key) {
|
||||
next.push(record)
|
||||
continue
|
||||
}
|
||||
record[role] = false
|
||||
if (record.clickTarget || record.popout) next.push(record)
|
||||
}
|
||||
pluginObjectOwners = next
|
||||
}
|
||||
|
||||
function pluginOwnsBarObject(pluginId, target) {
|
||||
var record = target ? root.pluginObjectRecord(target) : null
|
||||
return !!record && record.pluginId === String(pluginId || "")
|
||||
}
|
||||
|
||||
function pluginClickTargets(pluginId) {
|
||||
var out = []
|
||||
for (var i = 0; i < root.clickTargets.length; i++) {
|
||||
var target = root.clickTargets[i]
|
||||
if (root.pluginOwnsBarObject(pluginId, target)) out.push(target)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function syncAllPluginBarApiObjects() {
|
||||
for (var id in pluginBarApis) root.syncPluginBarApiObjects(pluginBarApis[id])
|
||||
}
|
||||
|
||||
function registerPluginClickTarget(pluginId, target) {
|
||||
if (!root.markPluginObject(pluginId, target, "clickTarget")) return
|
||||
root.registerClickTarget(target)
|
||||
}
|
||||
|
||||
function unregisterPluginClickTarget(pluginId, target) {
|
||||
if (!root.pluginOwnsBarObject(pluginId, target)) return
|
||||
root.unregisterClickTarget(target)
|
||||
root.unmarkPluginObject(pluginId, target, "clickTarget")
|
||||
}
|
||||
|
||||
function requestPluginPopout(pluginId, owner) {
|
||||
if (!root.markPluginObject(pluginId, owner, "popout")) return
|
||||
root.requestPopout(owner)
|
||||
}
|
||||
|
||||
function releasePluginPopout(pluginId, owner) {
|
||||
if (!root.pluginOwnsBarObject(pluginId, owner)) return
|
||||
root.releasePopout(owner)
|
||||
root.unmarkPluginObject(pluginId, owner, "popout")
|
||||
}
|
||||
|
||||
function pluginBarApiFor(pluginId, moduleName, registered) {
|
||||
var key = String(pluginId || "")
|
||||
if (!key) return null
|
||||
|
||||
var pluginShell = null
|
||||
if (registered && root.shell && typeof root.shell.pluginShellForId === "function") {
|
||||
// Only the trusted built-in bar receives ShellRoot and can request a
|
||||
// service-capable facade for the widget it is instantiating.
|
||||
pluginShell = root.shell.pluginShellForId(moduleName)
|
||||
} else if (root.shell && typeof root.shell.pluginShellForBarEntry === "function") {
|
||||
// Replacement bars receive a service-less entry facade. Giving an
|
||||
// untrusted bar a generic facade factory would let it retrieve another
|
||||
// third-party plugin's live service object.
|
||||
pluginShell = root.shell.pluginShellForBarEntry(key, moduleName)
|
||||
}
|
||||
|
||||
if (pluginBarApis[key]) {
|
||||
pluginBarApis[key].shell = pluginShell
|
||||
return pluginBarApis[key]
|
||||
}
|
||||
|
||||
var api = pluginBarApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
moduleName: String(moduleName || ""),
|
||||
shell: pluginShell,
|
||||
_showTooltip: function(target, text) { root.showTooltip(target, text) },
|
||||
_hideTooltip: function(target) { root.hideTooltip(target) },
|
||||
_registerClickTarget: function(target) { root.registerPluginClickTarget(key, target) },
|
||||
_unregisterClickTarget: function(target) { root.unregisterPluginClickTarget(key, target) },
|
||||
_requestPopout: function(owner) { root.requestPluginPopout(key, owner) },
|
||||
_releasePopout: function(owner) { root.releasePluginPopout(key, owner) },
|
||||
_switchPanelFrom: function(owner, direction) { return root.switchPanelFrom(owner, direction) },
|
||||
_targetBelongsToWindow: function(target, window) { return root.targetBelongsToWindow(target, window) },
|
||||
_moduleWidgets: function(requestedId) {
|
||||
return String(requestedId || "") === String(moduleName || "")
|
||||
? root.moduleWidgets(moduleName) : []
|
||||
},
|
||||
_run: function(command) { root.run(command) },
|
||||
_setCenterHoverRevealSuppressed: function(value) {
|
||||
root.centerHoverRevealSuppressed = !!value
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
root.bindPluginBarApi(api)
|
||||
|
||||
var next = ({})
|
||||
for (var id in pluginBarApis) next[id] = pluginBarApis[id]
|
||||
next[key] = api
|
||||
pluginBarApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarApiUsed(pluginId) {
|
||||
for (var i = 0; i < moduleSlots.length; i++) {
|
||||
var slot = moduleSlots[i]
|
||||
if (slot && slot.pluginApiId === pluginId) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function releasePluginObjects(pluginId) {
|
||||
var owned = pluginObjectOwners.slice()
|
||||
for (var i = 0; i < owned.length; i++) {
|
||||
var record = owned[i]
|
||||
if (!record || record.pluginId !== pluginId) continue
|
||||
if (record.clickTarget) root.unregisterClickTarget(record.target)
|
||||
if (record.popout && root.activePopout === record.target) root.releasePopout(record.target)
|
||||
}
|
||||
pluginObjectOwners = pluginObjectOwners.filter(function(record) {
|
||||
return record && record.pluginId !== pluginId
|
||||
})
|
||||
}
|
||||
|
||||
function prunePluginBarApis() {
|
||||
var next = ({})
|
||||
for (var id in pluginBarApis) {
|
||||
var api = pluginBarApis[id]
|
||||
if (root.pluginBarApiUsed(id)) {
|
||||
next[id] = api
|
||||
continue
|
||||
}
|
||||
root.releasePluginObjects(id)
|
||||
if (api && typeof api.destroy === "function") api.destroy()
|
||||
}
|
||||
pluginBarApis = next
|
||||
}
|
||||
|
||||
onActivePopoutChanged: syncAllPluginBarApiObjects()
|
||||
onClickTargetsChanged: syncAllPluginBarApiObjects()
|
||||
onLayoutConfigChanged: syncAllPluginBarApiObjects()
|
||||
onModuleSlotsChanged: Qt.callLater(prunePluginBarApis)
|
||||
|
||||
Component.onDestruction: {
|
||||
for (var id in pluginBarApis) {
|
||||
root.releasePluginObjects(id)
|
||||
if (pluginBarApis[id] && typeof pluginBarApis[id].destroy === "function")
|
||||
pluginBarApis[id].destroy()
|
||||
}
|
||||
pluginBarApis = ({})
|
||||
}
|
||||
|
||||
function registerClickTarget(target) {
|
||||
if (!target || clickTargets.indexOf(target) !== -1) return
|
||||
@@ -599,6 +825,10 @@ Item {
|
||||
if (barHoverCount === 0) centerSectionRevealTimer.restart()
|
||||
}
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
centerHoverRevealSuppressed = !!value
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: centerSectionRevealTimer
|
||||
interval: 120
|
||||
@@ -1548,6 +1778,9 @@ Item {
|
||||
readonly property string moduleName: root.entryId(entry)
|
||||
readonly property var moduleSettings: root.entrySettings(entry)
|
||||
readonly property string customType: root.customModuleType(entry)
|
||||
readonly property var registryMetadata: root.barWidgetRegistry.metadataFor(root.canonicalWidgetId(moduleName))
|
||||
readonly property bool firstParty: registryMetadata && registryMetadata.firstParty === true
|
||||
readonly property string pluginApiId: registered ? root.canonicalWidgetId(moduleName) : "bar-entry:" + moduleName
|
||||
// Re-evaluate when the registry mutates (Component reference changes,
|
||||
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
|
||||
// the binding dependency — the wrapped function call alone wouldn't.
|
||||
@@ -1766,7 +1999,8 @@ Item {
|
||||
function injectProps() {
|
||||
var target = activeItem
|
||||
if (!target) return
|
||||
if ("bar" in target) target.bar = root
|
||||
if ("bar" in target) target.bar = firstParty
|
||||
? root : root.pluginBarApiFor(pluginApiId, moduleName, registered)
|
||||
if ("moduleName" in target) target.moduleName = moduleName
|
||||
if ("settings" in target) target.settings = moduleSettings
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Quickshell session lock with separate password and fingerprint PAM flows.",
|
||||
"omarchy": {
|
||||
"capabilities": [
|
||||
"authentication"
|
||||
]
|
||||
},
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
|
||||
@@ -119,7 +119,9 @@ Panel {
|
||||
// Summoning by hotkey moves no pointer, so a hover the bar was still
|
||||
// holding must not keep the center indicators revealed behind the panel.
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
|
||||
root.bar.setCenterHoverRevealSuppressed(value)
|
||||
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
root.bar.centerHoverRevealSuppressed = value
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ Panel {
|
||||
}
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
|
||||
root.bar.setCenterHoverRevealSuppressed(value)
|
||||
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
root.bar.centerHoverRevealSuppressed = value
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Theme-aware authentication dialog for privileged actions.",
|
||||
"omarchy": {
|
||||
"capabilities": [
|
||||
"authentication"
|
||||
]
|
||||
},
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
|
||||
@@ -16,7 +16,8 @@ Item {
|
||||
readonly property string stayAwakeStatePath: stayAwakeStateDir + "/stay-awake"
|
||||
readonly property int defaultScreensaverSeconds: 150
|
||||
readonly property int defaultLockSeconds: 300
|
||||
readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle ? shell.shellConfig.idle : ({})
|
||||
readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle
|
||||
? shell.shellConfig.idle : (shell && shell.idleConfig ? shell.idleConfig : ({}))
|
||||
readonly property int screensaverTimeoutSeconds: secondsFromConfig(idleConfig.screensaver, defaultScreensaverSeconds)
|
||||
readonly property int lockTimeoutSeconds: secondsFromConfig(idleConfig.lock, defaultLockSeconds)
|
||||
readonly property int firstIdleTimeoutSeconds: Math.min(screensaverTimeoutSeconds, lockTimeoutSeconds)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Intentionally not `.pragma library`: QML JavaScript imports get a private
|
||||
// module instance per importing component. shell.qml's instance retains the
|
||||
// authentication services; a third-party plugin importing this file receives
|
||||
// a separate empty store rather than a shared path to credential-bearing QML.
|
||||
|
||||
var services = ({})
|
||||
var trustedIds = ({})
|
||||
|
||||
function has(id) {
|
||||
return services[String(id || "")] !== undefined
|
||||
}
|
||||
|
||||
function put(id, service) {
|
||||
var key = String(id || "")
|
||||
if (!key || !service) return
|
||||
trustedIds[key] = true
|
||||
if (services[key] && services[key] !== service && typeof services[key].destroy === "function")
|
||||
services[key].destroy()
|
||||
services[key] = service
|
||||
}
|
||||
|
||||
function isTrusted(id) {
|
||||
return trustedIds[String(id || "")] === true
|
||||
}
|
||||
|
||||
function ids() {
|
||||
return Object.keys(services)
|
||||
}
|
||||
|
||||
function updateManifest(id, manifest) {
|
||||
var service = services[String(id || "")]
|
||||
if (service && "manifest" in service) service.manifest = manifest
|
||||
}
|
||||
|
||||
function destroy(id) {
|
||||
var key = String(id || "")
|
||||
var service = services[key]
|
||||
if (service && typeof service.destroy === "function") service.destroy()
|
||||
delete services[key]
|
||||
}
|
||||
|
||||
function destroyAll() {
|
||||
var keys = ids()
|
||||
for (var i = 0; i < keys.length; i++) destroy(keys[i])
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached application-library capability for third-party menus. Callbacks
|
||||
// expose the supported app-list operations without retaining AppLibrary or its
|
||||
// ShellRoot parent in the plugin-visible object graph.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
signal appsChanged()
|
||||
|
||||
property var _entryName: null
|
||||
property var _entrySubtext: null
|
||||
property var _sortedEntries: null
|
||||
property var _iconSource: null
|
||||
property var _refreshIcons: null
|
||||
property var _launch: null
|
||||
property var _remove: null
|
||||
|
||||
function entryName(entry) {
|
||||
return _entryName ? _entryName(entry) : ""
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return _entrySubtext ? _entrySubtext(entry) : ""
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
return _sortedEntries ? _sortedEntries(String(query || "")) : []
|
||||
}
|
||||
|
||||
function iconSource(icon) {
|
||||
return _iconSource ? _iconSource(icon) : ""
|
||||
}
|
||||
|
||||
function refreshIcons() {
|
||||
if (_refreshIcons) _refreshIcons()
|
||||
}
|
||||
|
||||
function launch(desktopId, name) {
|
||||
if (_launch) _launch(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
|
||||
function remove(desktopId, name) {
|
||||
if (_remove) _remove(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Scalar-only view of the active bar for plugins that position independent
|
||||
// windows. The active Bar QObject is never retained here.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
property bool barHidden: false
|
||||
property int barSize: 0
|
||||
property string fontFamily: ""
|
||||
property string position: "top"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached widget-catalogue snapshot for third-party full-bar implementations.
|
||||
// Plugins can render the referenced components, but mutating this local view
|
||||
// cannot replace a registration in the host registry.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
property var widgets: ({})
|
||||
property int revision: 0
|
||||
|
||||
function metadataFor(id) {
|
||||
var entry = widgets[String(id || "")]
|
||||
return entry ? entry.metadata : null
|
||||
}
|
||||
|
||||
function availableIds() {
|
||||
return Object.keys(widgets)
|
||||
}
|
||||
|
||||
function has(id) {
|
||||
return widgets[String(id || "")] !== undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Narrow proxy for the non-authentication first-party services used by the
|
||||
// built-in bar. It intentionally has no generic property or method forwarding.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
required property string serviceId
|
||||
|
||||
property bool stayAwake: false
|
||||
property bool enabled: false
|
||||
property bool doNotDisturb: false
|
||||
property var activePlayer: null
|
||||
property var sourcePlayers: []
|
||||
|
||||
property var _setIdleEnabled: null
|
||||
property var _setNightlight: null
|
||||
property var _setDoNotDisturb: null
|
||||
property var _runAction: null
|
||||
property var _playerKey: null
|
||||
property var _selectPlayer: null
|
||||
|
||||
function setIdleEnabled(value) {
|
||||
if (serviceId === "omarchy.idle" && _setIdleEnabled) _setIdleEnabled(!!value)
|
||||
}
|
||||
|
||||
function setNightlight(value) {
|
||||
if (serviceId === "omarchy.nightlight" && _setNightlight) _setNightlight(!!value)
|
||||
}
|
||||
|
||||
function setDoNotDisturb(value) {
|
||||
if (serviceId === "omarchy.notifications" && _setDoNotDisturb) _setDoNotDisturb(!!value)
|
||||
}
|
||||
|
||||
function runAction(action, showFeedback, playerId) {
|
||||
if (serviceId === "omarchy.media" && _runAction)
|
||||
_runAction(String(action || ""), !!showFeedback, String(playerId || ""))
|
||||
}
|
||||
|
||||
function playerKey(player) {
|
||||
return serviceId === "omarchy.media" && _playerKey ? _playerKey(player) : ""
|
||||
}
|
||||
|
||||
function selectPlayer(playerId) {
|
||||
if (serviceId === "omarchy.media" && _selectPlayer) _selectPlayer(String(playerId || ""))
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ QtObject {
|
||||
property var shellConfigProvider: null
|
||||
property var shellConfigMutator: null
|
||||
|
||||
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
|
||||
// { pluginId: manifest } — manifests have source/trust metadata stamped in.
|
||||
property var installedPlugins: ({})
|
||||
property int registryRevision: 0
|
||||
property bool scanning: false
|
||||
@@ -78,8 +78,7 @@ QtObject {
|
||||
}
|
||||
}
|
||||
// Every entry point must be a relative path inside the plugin's source
|
||||
// directory. Reject the whole manifest if anything looks like an attempt
|
||||
// to escape the plugin's sandbox.
|
||||
// directory. Reject the whole manifest if an entry point escapes it.
|
||||
for (var key in manifest.entryPoints) {
|
||||
if (!isSafeEntryPoint(manifest.entryPoints[key])) {
|
||||
console.warn("PluginRegistry: unsafe entryPoint '" + key + "'='"
|
||||
@@ -90,6 +89,32 @@ QtObject {
|
||||
return manifest
|
||||
}
|
||||
|
||||
function trustedCapabilities(manifest) {
|
||||
if (!manifest || !manifest.__isFirstParty) return []
|
||||
var metadata = Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var declared = metadata && Array.isArray(metadata.capabilities) ? metadata.capabilities : []
|
||||
var out = []
|
||||
for (var i = 0; i < declared.length; i++) {
|
||||
var capability = String(declared[i] || "")
|
||||
if (capability && out.indexOf(capability) === -1) out.push(capability)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function stampHostCapabilities(firstParty, thirdParty) {
|
||||
for (var firstPartyId in firstParty)
|
||||
firstParty[firstPartyId].__hostCapabilities = trustedCapabilities(firstParty[firstPartyId])
|
||||
|
||||
for (var thirdPartyId in thirdParty) {
|
||||
var manifest = thirdParty[thirdPartyId]
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var clonedFrom = metadata ? String(metadata.clonedFrom || "") : ""
|
||||
var source = clonedFrom ? firstParty[clonedFrom] : null
|
||||
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
|
||||
? source.__hostCapabilities.slice() : []
|
||||
}
|
||||
}
|
||||
|
||||
function entryPointUrl(manifest, kind) {
|
||||
if (!Util.isPlainObject(manifest)) return ""
|
||||
var ep = manifest.entryPoints ? manifest.entryPoints[kind] : null
|
||||
@@ -594,6 +619,8 @@ QtObject {
|
||||
}
|
||||
flush()
|
||||
|
||||
stampHostCapabilities(firstParty, thirdParty)
|
||||
|
||||
var merged = {}
|
||||
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
||||
// Third-party plugins never shadow first-party ids. The whole
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
|
||||
// Read-only, self-scoped registry view for an installed third-party plugin.
|
||||
// The host updates manifest/enabled when it rescans; no host registry object is
|
||||
// retained here, so `parent` and property traversal cannot reach ShellRoot.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
property var manifest: null
|
||||
property bool enabled: false
|
||||
property var _entryPointUrl: null
|
||||
|
||||
readonly property var installedPlugins: {
|
||||
var out = ({})
|
||||
if (manifest) out[pluginId] = manifest
|
||||
return out
|
||||
}
|
||||
|
||||
function isEnabled(id) {
|
||||
return String(id || "") === pluginId && enabled
|
||||
}
|
||||
|
||||
function resolveEnabledId(id) {
|
||||
return String(id || "") === pluginId ? pluginId : ""
|
||||
}
|
||||
|
||||
function entryPointUrl(candidate, kind) {
|
||||
if (!candidate || String(candidate.id || "") !== pluginId) return ""
|
||||
return _entryPointUrl ? _entryPointUrl(String(kind || "")) : ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import QtQuick
|
||||
|
||||
// Capability-scoped shell surface for installed third-party plugins.
|
||||
//
|
||||
// The callbacks are closed over one plugin id by shell.qml. A plugin can call
|
||||
// them directly, but it cannot widen their scope: ordinary plugins are limited
|
||||
// to their own id, and full-bar callbacks independently enforce their explicit
|
||||
// non-authentication UI scope. This object avoids directly injecting the host
|
||||
// shell, but it is not a QML sandbox: visual plugins share the host object tree.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
|
||||
property var appLibrary: null
|
||||
property var bar: null
|
||||
property var barConfig: ({})
|
||||
property var idleConfig: ({})
|
||||
|
||||
property var _serviceLookup: null
|
||||
property var _firstPartyServiceLookup: null
|
||||
property var _barEntryShellLookup: null
|
||||
property var _summon: null
|
||||
property var _hide: null
|
||||
property var _toggle: null
|
||||
property var _isOpen: null
|
||||
property var _updateSettings: null
|
||||
property var _mutateBarConfig: null
|
||||
|
||||
function serviceFor(id) {
|
||||
return _serviceLookup ? _serviceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
// Full-bar facades and configured Indicators clones under the trusted bar
|
||||
// receive narrow proxies for their specific non-authentication services.
|
||||
function firstPartyServiceFor(id) {
|
||||
return _firstPartyServiceLookup
|
||||
? _firstPartyServiceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
function pluginShellForBarEntry(ownerId, moduleName) {
|
||||
return _barEntryShellLookup
|
||||
? _barEntryShellLookup(String(ownerId || ""), String(moduleName || "")) : null
|
||||
}
|
||||
|
||||
function summon(id, payloadJson) {
|
||||
return _summon ? _summon(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
return _hide ? _hide(String(id || "")) : false
|
||||
}
|
||||
|
||||
function toggle(id, payloadJson) {
|
||||
return _toggle ? _toggle(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function isPluginOpen(id) {
|
||||
return _isOpen ? _isOpen(String(id || "")) : false
|
||||
}
|
||||
|
||||
function updateEntryInline(id, settings) {
|
||||
return _updateSettings ? _updateSettings(String(id || ""), settings) : false
|
||||
}
|
||||
|
||||
function mutateShellConfig(mutator) {
|
||||
return _mutateBarConfig && typeof mutator === "function"
|
||||
? _mutateBarConfig(mutator) : false
|
||||
}
|
||||
}
|
||||
+742
-29
@@ -7,6 +7,7 @@ import qs.Commons
|
||||
|
||||
import "plugins/bar"
|
||||
import "services"
|
||||
import "services/AuthServiceStore.js" as AuthServiceStore
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
@@ -113,7 +114,10 @@ ShellRoot {
|
||||
}
|
||||
|
||||
readonly property var barConfig: shellConfig && Util.isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
|
||||
onBarConfigChanged: if (bar && "barConfig" in bar) bar.barConfig = shell.barConfig
|
||||
onBarConfigChanged: {
|
||||
if (bar && "barConfig" in bar)
|
||||
bar.barConfig = shell.barConfigFor(shell.activeBarManifest)
|
||||
}
|
||||
FileView {
|
||||
id: defaultsFile
|
||||
path: shell.defaultsPath
|
||||
@@ -214,11 +218,11 @@ ShellRoot {
|
||||
function configureBar(target, manifest) {
|
||||
if (!target) return
|
||||
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in target) target.shell = shell
|
||||
if ("manifest" in target) target.manifest = manifest
|
||||
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
|
||||
if ("barConfig" in target) target.barConfig = shell.barConfig
|
||||
if ("shell" in target) target.shell = shell.pluginShellFor(manifest)
|
||||
if ("manifest" in target) target.manifest = shell.publicPluginManifest(manifest)
|
||||
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
|
||||
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistryFor(manifest)
|
||||
if ("barConfig" in target) target.barConfig = shell.barConfigFor(manifest)
|
||||
shell.bar = target
|
||||
}
|
||||
|
||||
@@ -253,8 +257,7 @@ ShellRoot {
|
||||
onActiveChanged: if (!active) shell.bar = null
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
var detail = errorString && errorString() ? errorString() : ""
|
||||
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
|
||||
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId)
|
||||
shell.failedBarId = shell.activeBarId
|
||||
}
|
||||
}
|
||||
@@ -271,13 +274,628 @@ ShellRoot {
|
||||
}
|
||||
|
||||
property var _services: ({})
|
||||
property var _pluginShellApis: ({})
|
||||
property var _pluginShellApiDescriptors: ({})
|
||||
property var _pluginBarEntryShellApis: ({})
|
||||
property var _pluginRegistryApis: ({})
|
||||
property var _pluginBarWidgetRegistryApis: ({})
|
||||
property var _pluginAppLibraryApis: ({})
|
||||
property var _pluginBarStateApis: ({})
|
||||
property var _pluginFirstPartyServiceApis: ({})
|
||||
|
||||
Component {
|
||||
id: pluginShellApiComponent
|
||||
PluginShellApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginRegistryApiComponent
|
||||
PluginRegistryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginBarWidgetRegistryApiComponent
|
||||
PluginBarWidgetRegistryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginAppLibraryApiComponent
|
||||
PluginAppLibraryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginBarStateApiComponent
|
||||
PluginBarStateApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginFirstPartyServiceApiComponent
|
||||
PluginFirstPartyServiceApi { }
|
||||
}
|
||||
|
||||
function publicPluginManifest(manifest) {
|
||||
if (!manifest) return null
|
||||
if (manifest.__isFirstParty) return manifest
|
||||
var copy = JSON.parse(JSON.stringify(manifest))
|
||||
delete copy.__sourceDir
|
||||
delete copy.__isFirstParty
|
||||
delete copy.__hostCapabilities
|
||||
return copy
|
||||
}
|
||||
|
||||
function publicBarConfig() {
|
||||
return JSON.parse(JSON.stringify(shell.barConfig || {}))
|
||||
}
|
||||
|
||||
function barConfigFor(manifest) {
|
||||
return !manifest || manifest.__isFirstParty
|
||||
? shell.barConfig : shell.publicBarConfig()
|
||||
}
|
||||
|
||||
function publicBarWidgetSnapshot() {
|
||||
var source = shell.barWidgetRegistry.widgets || {}
|
||||
var snapshot = {}
|
||||
for (var id in source) {
|
||||
var entry = source[id]
|
||||
if (!entry) continue
|
||||
snapshot[id] = {
|
||||
component: entry.component,
|
||||
metadata: JSON.parse(JSON.stringify(entry.metadata || {}))
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function manifestHasKind(manifest, kind) {
|
||||
return !!manifest && Array.isArray(manifest.kinds)
|
||||
&& manifest.kinds.indexOf(kind) !== -1
|
||||
}
|
||||
|
||||
function pluginHasBarCapabilities(manifest) {
|
||||
return shell.manifestHasKind(manifest, "bar")
|
||||
}
|
||||
|
||||
function pluginIsIndicatorsClone(manifest) {
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
return shell.manifestHasKind(manifest, "bar-widget")
|
||||
&& !!metadata && String(metadata.clonedFrom || "") === "omarchy.indicators"
|
||||
}
|
||||
|
||||
function publicIdleConfigFor(manifest) {
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
if (!metadata || String(metadata.clonedFrom || "") !== "omarchy.idle") return ({})
|
||||
var idle = shell.shellConfig && Util.isPlainObject(shell.shellConfig.idle)
|
||||
? shell.shellConfig.idle : ({})
|
||||
return JSON.parse(JSON.stringify(idle))
|
||||
}
|
||||
|
||||
function pluginCloneMaySummon(manifest, requestedId) {
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var sourceId = metadata ? String(metadata.clonedFrom || "") : ""
|
||||
var allowed = {
|
||||
"omarchy.audio": ["omarchy.osd"],
|
||||
"omarchy.media": ["omarchy.osd"],
|
||||
"omarchy.monitor": ["omarchy.osd"],
|
||||
"omarchy.network": ["omarchy.speedtest", "omarchy.wifiqr"]
|
||||
}
|
||||
var targets = allowed[sourceId] || []
|
||||
return targets.indexOf(String(requestedId || "")) !== -1
|
||||
}
|
||||
|
||||
function pluginOwnsTarget(pluginId, requestedId) {
|
||||
var caller = String(pluginId || "")
|
||||
if (!caller) return false
|
||||
return shell.pluginRegistry.resolveEnabledId(String(requestedId || "")) === caller
|
||||
}
|
||||
|
||||
function pluginServiceFor(pluginId, requestedId) {
|
||||
if (!shell.pluginOwnsTarget(pluginId, requestedId)) return null
|
||||
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
}
|
||||
|
||||
function barEntryConfigured(pluginId) {
|
||||
var location = shell.pluginRegistry.findEntryLocation(shell.shellConfig, pluginId)
|
||||
return location && location.kind === "bar"
|
||||
}
|
||||
|
||||
function barPluginMayControl(manifest, requestedId) {
|
||||
if (!shell.pluginHasBarCapabilities(manifest)) return false
|
||||
var id = shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
|
||||
var target = shell.pluginRegistry.installedPlugins[id]
|
||||
if (!target || shell.isAuthenticationService(target, id)) return false
|
||||
if (shell.barEntryConfigured(id)) return true
|
||||
var uiKinds = ["bar-widget", "panel", "overlay", "menu"]
|
||||
for (var i = 0; i < uiKinds.length; i++)
|
||||
if (shell.manifestHasKind(target, uiKinds[i])) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function mutatePluginBarConfig(mutator) {
|
||||
if (typeof mutator !== "function") return false
|
||||
shell.mutateShellConfig(function(config) {
|
||||
var scoped = { bar: JSON.parse(JSON.stringify(config.bar || {})) }
|
||||
mutator(scoped)
|
||||
if (Util.isPlainObject(scoped.bar)) config.bar = JSON.parse(JSON.stringify(scoped.bar))
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function pluginAppLibraryFor(cacheKey, pluginId) {
|
||||
if (_pluginAppLibraryApis[cacheKey]) return _pluginAppLibraryApis[cacheKey]
|
||||
var api = pluginAppLibraryApiComponent.createObject(null, {
|
||||
ownerPluginId: pluginId,
|
||||
_entryName: function(entry) { return shell.appLibrary.entryName(entry) },
|
||||
_entrySubtext: function(entry) { return shell.appLibrary.entrySubtext(entry) },
|
||||
_sortedEntries: function(query) { return shell.appLibrary.sortedEntries(query) },
|
||||
_iconSource: function(icon) { return shell.appLibrary.iconSource(icon) },
|
||||
_refreshIcons: function() { shell.appLibrary.refreshIcons() },
|
||||
_launch: function(desktopId, name) { shell.appLibrary.launch(desktopId, name) },
|
||||
_remove: function(desktopId, name) { shell.appLibrary.remove(desktopId, name) }
|
||||
})
|
||||
if (!api) return null
|
||||
var next = ({})
|
||||
for (var id in _pluginAppLibraryApis) next[id] = _pluginAppLibraryApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginAppLibraryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarStateFor(cacheKey, pluginId) {
|
||||
if (_pluginBarStateApis[cacheKey]) return _pluginBarStateApis[cacheKey]
|
||||
var api = pluginBarStateApiComponent.createObject(null, { ownerPluginId: pluginId })
|
||||
if (!api) return null
|
||||
api.barHidden = Qt.binding(function() { return shell.bar ? shell.bar.barHidden === true : false })
|
||||
api.barSize = Qt.binding(function() { return shell.bar ? Math.max(0, shell.bar.barSize || 0) : 0 })
|
||||
api.fontFamily = Qt.binding(function() { return shell.bar ? String(shell.bar.fontFamily || "") : "" })
|
||||
api.position = Qt.binding(function() { return shell.bar ? String(shell.bar.position || "top") : "top" })
|
||||
var next = ({})
|
||||
for (var id in _pluginBarStateApis) next[id] = _pluginBarStateApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginBarStateApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginFirstPartyServiceFor(cacheKey, pluginId, requestedId) {
|
||||
var id = String(requestedId || "")
|
||||
var allowed = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
|
||||
if (allowed.indexOf(id) === -1) return null
|
||||
var proxyKey = cacheKey + "::" + id
|
||||
if (_pluginFirstPartyServiceApis[proxyKey]) return _pluginFirstPartyServiceApis[proxyKey]
|
||||
|
||||
function service() {
|
||||
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(id))
|
||||
}
|
||||
var api = pluginFirstPartyServiceApiComponent.createObject(null, {
|
||||
ownerPluginId: pluginId,
|
||||
serviceId: id,
|
||||
_setIdleEnabled: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setIdleEnabled === "function") target.setIdleEnabled(value)
|
||||
},
|
||||
_setNightlight: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setNightlight === "function") target.setNightlight(value)
|
||||
},
|
||||
_setDoNotDisturb: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setDoNotDisturb === "function") target.setDoNotDisturb(value)
|
||||
},
|
||||
_runAction: function(action, showFeedback, targetKey) {
|
||||
var target = service()
|
||||
if (target && typeof target.runAction === "function") target.runAction(action, showFeedback, targetKey)
|
||||
},
|
||||
_playerKey: function(player) {
|
||||
var target = service()
|
||||
return target && typeof target.playerKey === "function" ? target.playerKey(player) : ""
|
||||
},
|
||||
_selectPlayer: function(playerKey) {
|
||||
var target = service()
|
||||
if (target && typeof target.selectPlayer === "function") target.selectPlayer(playerKey)
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
api.stayAwake = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.stayAwake === true : false
|
||||
})
|
||||
api.enabled = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.enabled === true : false
|
||||
})
|
||||
api.doNotDisturb = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.doNotDisturb === true : false
|
||||
})
|
||||
api.activePlayer = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.activePlayer : null
|
||||
})
|
||||
api.sourcePlayers = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target && Array.isArray(target.sourcePlayers) ? target.sourcePlayers : []
|
||||
})
|
||||
var next = ({})
|
||||
for (var existing in _pluginFirstPartyServiceApis) next[existing] = _pluginFirstPartyServiceApis[existing]
|
||||
next[proxyKey] = api
|
||||
_pluginFirstPartyServiceApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities) {
|
||||
return [
|
||||
allowOwnService ? "own-service" : "no-own-service",
|
||||
barCapabilities ? "bar" : "no-bar",
|
||||
allowOwnService && shell.pluginIsIndicatorsClone(manifest) ? "indicators" : "no-indicators",
|
||||
shell.manifestHasKind(manifest, "menu") ? "menu" : "no-menu"
|
||||
].join("|")
|
||||
}
|
||||
|
||||
function cacheWithoutKey(cache, key, destroyValue) {
|
||||
var next = ({})
|
||||
for (var existing in cache) {
|
||||
if (existing === key) {
|
||||
var value = cache[existing]
|
||||
if (destroyValue && value && typeof value.destroy === "function") value.destroy()
|
||||
} else {
|
||||
next[existing] = cache[existing]
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function cacheWithoutPrefix(cache, prefix) {
|
||||
var next = ({})
|
||||
for (var existing in cache) {
|
||||
if (existing.indexOf(prefix) === 0) {
|
||||
var value = cache[existing]
|
||||
if (value && typeof value.destroy === "function") value.destroy()
|
||||
} else {
|
||||
next[existing] = cache[existing]
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function revokePluginShellApi(cacheKey) {
|
||||
var key = String(cacheKey || "")
|
||||
if (!key) return
|
||||
_pluginAppLibraryApis = shell.cacheWithoutKey(_pluginAppLibraryApis, key, true)
|
||||
_pluginFirstPartyServiceApis = shell.cacheWithoutPrefix(_pluginFirstPartyServiceApis, key + "::")
|
||||
_pluginBarEntryShellApis = shell.cacheWithoutPrefix(_pluginBarEntryShellApis, key + ":")
|
||||
_pluginShellApis = shell.cacheWithoutKey(_pluginShellApis, key, true)
|
||||
_pluginShellApiDescriptors = shell.cacheWithoutKey(_pluginShellApiDescriptors, key, false)
|
||||
}
|
||||
|
||||
function createScopedPluginShell(manifest, cacheKey, allowOwnService, barCapabilities) {
|
||||
var key = String(manifest && manifest.id || "")
|
||||
if (!key) return null
|
||||
var profile = shell.pluginShellCapabilityProfile(manifest, allowOwnService, barCapabilities)
|
||||
var cached = _pluginShellApis[cacheKey]
|
||||
var descriptor = _pluginShellApiDescriptors[cacheKey]
|
||||
if (cached && descriptor && descriptor.pluginId === key
|
||||
&& descriptor.profile === profile) return cached
|
||||
if (cached || descriptor) shell.revokePluginShellApi(cacheKey)
|
||||
|
||||
function currentManifest() {
|
||||
return shell.pluginRegistry.installedPlugins[key] || null
|
||||
}
|
||||
|
||||
function hasCurrentBarCapabilities() {
|
||||
return barCapabilities && shell.pluginHasBarCapabilities(currentManifest())
|
||||
}
|
||||
|
||||
// Construct the narrow service proxies before any plugin binding can call
|
||||
// firstPartyServiceFor(). Creating a QObject while evaluating that binding
|
||||
// makes QML re-enter the binding and report a loop on the caller's service
|
||||
// property, even though the resulting proxy is otherwise acyclic.
|
||||
var firstPartyServices = ({})
|
||||
var serviceIds = barCapabilities
|
||||
? ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
|
||||
: (allowOwnService && shell.pluginIsIndicatorsClone(manifest)
|
||||
? ["omarchy.idle", "omarchy.nightlight", "omarchy.notifications"] : [])
|
||||
for (var i = 0; i < serviceIds.length; i++) {
|
||||
var serviceId = serviceIds[i]
|
||||
firstPartyServices[serviceId] = shell.pluginFirstPartyServiceFor(cacheKey, key, serviceId)
|
||||
}
|
||||
|
||||
var api = pluginShellApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
appLibrary: shell.manifestHasKind(manifest, "menu")
|
||||
? shell.pluginAppLibraryFor(cacheKey, key) : null,
|
||||
bar: shell.pluginBarStateFor(cacheKey, key),
|
||||
barConfig: shell.publicBarConfig(),
|
||||
idleConfig: shell.publicIdleConfigFor(manifest),
|
||||
_serviceLookup: function(requestedId) {
|
||||
return allowOwnService ? shell.pluginServiceFor(key, requestedId) : null
|
||||
},
|
||||
_firstPartyServiceLookup: function(requestedId) {
|
||||
if (allowOwnService && shell.pluginOwnsTarget(key, requestedId))
|
||||
return shell.pluginServiceFor(key, requestedId)
|
||||
// Indicators clones have no service of their own. Under the trusted
|
||||
// bar they need only these prebuilt non-authentication proxies, not
|
||||
// full-bar capabilities or access to another plugin's live service.
|
||||
var indicatorsAllowed = allowOwnService && shell.pluginIsIndicatorsClone(currentManifest())
|
||||
&& shell.activeBarManifest && shell.activeBarManifest.__isFirstParty
|
||||
&& shell.pluginRegistry.isEnabled(key) && shell.barEntryConfigured(key)
|
||||
return hasCurrentBarCapabilities() || indicatorsAllowed
|
||||
? (firstPartyServices[requestedId] || null) : null
|
||||
},
|
||||
_barEntryShellLookup: function(ownerId, moduleName) {
|
||||
return hasCurrentBarCapabilities()
|
||||
? shell.pluginShellForBarEntry(cacheKey + ":" + ownerId, moduleName) : null
|
||||
},
|
||||
_summon: function(requestedId, payloadJson) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(currentManifest(), requestedId)
|
||||
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
|
||||
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
|
||||
},
|
||||
_hide: function(requestedId) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
|
||||
return shell.hide(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
},
|
||||
_toggle: function(requestedId, payloadJson) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
|
||||
return shell.toggle(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
|
||||
},
|
||||
_isOpen: function(requestedId) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(currentManifest(), requestedId)) return false
|
||||
return shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
},
|
||||
_updateSettings: function(requestedId, settings) {
|
||||
if (shell.pluginOwnsTarget(key, requestedId)) return shell.updateEntryInline(key, settings)
|
||||
if (hasCurrentBarCapabilities() && shell.barEntryConfigured(requestedId))
|
||||
return shell.updateEntryInline(requestedId, settings)
|
||||
return false
|
||||
},
|
||||
_mutateBarConfig: function(mutator) {
|
||||
return hasCurrentBarCapabilities() ? shell.mutatePluginBarConfig(mutator) : false
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginShellApis) next[id] = _pluginShellApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginShellApis = next
|
||||
var descriptorNext = ({})
|
||||
for (var descriptorKey in _pluginShellApiDescriptors)
|
||||
descriptorNext[descriptorKey] = _pluginShellApiDescriptors[descriptorKey]
|
||||
descriptorNext[cacheKey] = {
|
||||
pluginId: key,
|
||||
allowOwnService: allowOwnService === true,
|
||||
profile: profile
|
||||
}
|
||||
_pluginShellApiDescriptors = descriptorNext
|
||||
return api
|
||||
}
|
||||
|
||||
function scopedPluginShellForId(pluginId) {
|
||||
var key = String(pluginId || "")
|
||||
var manifest = shell.pluginRegistry.installedPlugins[key]
|
||||
if (!manifest) return null
|
||||
if (!manifest.__isFirstParty) return shell.pluginShellFor(manifest)
|
||||
return shell.createScopedPluginShell(manifest, "hosted:" + key, false, false)
|
||||
}
|
||||
|
||||
function pluginShellForId(pluginId) {
|
||||
return shell.scopedPluginShellForId(pluginId)
|
||||
}
|
||||
|
||||
function pluginShellForBarEntry(ownerId, moduleName) {
|
||||
var owner = String(ownerId || "")
|
||||
var target = String(moduleName || "")
|
||||
if (!owner || !target) return null
|
||||
if (!shell.barEntryConfigured(target)) return null
|
||||
var cacheKey = owner + "::" + target
|
||||
if (_pluginBarEntryShellApis[cacheKey]) return _pluginBarEntryShellApis[cacheKey]
|
||||
|
||||
function owns(requestedId) {
|
||||
return shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
|
||||
=== shell.pluginRegistry.resolveEnabledId(target)
|
||||
}
|
||||
|
||||
function currentManifest() {
|
||||
var id = shell.pluginRegistry.resolveEnabledId(target)
|
||||
return shell.pluginRegistry.installedPlugins[id] || null
|
||||
}
|
||||
|
||||
var api = pluginShellApiComponent.createObject(null, {
|
||||
pluginId: target,
|
||||
barConfig: shell.publicBarConfig(),
|
||||
_summon: function(requestedId, payloadJson) {
|
||||
if (!owns(requestedId)
|
||||
&& !shell.pluginCloneMaySummon(currentManifest(), requestedId)) return false
|
||||
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
|
||||
},
|
||||
_hide: function(requestedId) {
|
||||
return owns(requestedId)
|
||||
? shell.hide(shell.pluginRegistry.resolveEnabledId(target)) : false
|
||||
},
|
||||
_toggle: function(requestedId, payloadJson) {
|
||||
return owns(requestedId)
|
||||
? shell.toggle(shell.pluginRegistry.resolveEnabledId(target), payloadJson) : false
|
||||
},
|
||||
_isOpen: function(requestedId) {
|
||||
return owns(requestedId)
|
||||
? shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(target)) : false
|
||||
},
|
||||
_updateSettings: function(requestedId, settings) {
|
||||
return String(requestedId || "") === target
|
||||
? shell.updateEntryInline(target, settings) : false
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
var next = ({})
|
||||
for (var id in _pluginBarEntryShellApis) next[id] = _pluginBarEntryShellApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginBarEntryShellApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginShellFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
|
||||
}
|
||||
|
||||
function pluginRegistryFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell.pluginRegistry
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
if (_pluginRegistryApis[key]) return _pluginRegistryApis[key]
|
||||
|
||||
var api = pluginRegistryApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
manifest: shell.publicPluginManifest(manifest),
|
||||
enabled: shell.pluginRegistry.isEnabled(key),
|
||||
_entryPointUrl: function(kind) {
|
||||
var current = shell.pluginRegistry.installedPlugins[key]
|
||||
return current ? shell.pluginRegistry.entryPointUrl(current, kind) : ""
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginRegistryApis) next[id] = _pluginRegistryApis[id]
|
||||
next[key] = api
|
||||
_pluginRegistryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarWidgetRegistryFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell.barWidgetRegistry
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
if (_pluginBarWidgetRegistryApis[key]) return _pluginBarWidgetRegistryApis[key]
|
||||
|
||||
var api = pluginBarWidgetRegistryApiComponent.createObject(null, {
|
||||
widgets: shell.publicBarWidgetSnapshot(),
|
||||
revision: shell.barWidgetRegistry.revision
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginBarWidgetRegistryApis) next[id] = _pluginBarWidgetRegistryApis[id]
|
||||
next[key] = api
|
||||
_pluginBarWidgetRegistryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginApiActive(api, plugins) {
|
||||
var id = api ? String(api.pluginId || api.ownerPluginId || "") : ""
|
||||
var manifest = id ? plugins[id] : null
|
||||
return !!manifest && shell.pluginRegistry.isEnabled(id)
|
||||
}
|
||||
|
||||
function prunePluginApis() {
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
var shellKeys = Object.keys(_pluginShellApis)
|
||||
for (var si = 0; si < shellKeys.length; si++) {
|
||||
var shellKey = shellKeys[si]
|
||||
var shellApi = _pluginShellApis[shellKey]
|
||||
var descriptor = _pluginShellApiDescriptors[shellKey]
|
||||
var manifest = descriptor ? plugins[descriptor.pluginId] : null
|
||||
var barCapabilities = descriptor && descriptor.allowOwnService
|
||||
&& shell.pluginHasBarCapabilities(manifest)
|
||||
var expectedProfile = descriptor
|
||||
? shell.pluginShellCapabilityProfile(manifest, descriptor.allowOwnService, barCapabilities) : ""
|
||||
var active = descriptor && manifest && shell.pluginRegistry.isEnabled(descriptor.pluginId)
|
||||
if (!active || descriptor.profile !== expectedProfile)
|
||||
shell.revokePluginShellApi(shellKey)
|
||||
}
|
||||
|
||||
var registryNext = ({})
|
||||
for (var registryKey in _pluginRegistryApis) {
|
||||
var registryApi = _pluginRegistryApis[registryKey]
|
||||
if (shell.pluginApiActive(registryApi, plugins)) registryNext[registryKey] = registryApi
|
||||
else if (registryApi && typeof registryApi.destroy === "function") registryApi.destroy()
|
||||
}
|
||||
_pluginRegistryApis = registryNext
|
||||
|
||||
var widgetNext = ({})
|
||||
for (var widgetKey in _pluginBarWidgetRegistryApis) {
|
||||
var widgetApi = _pluginBarWidgetRegistryApis[widgetKey]
|
||||
if (plugins[widgetKey] && shell.pluginRegistry.isEnabled(widgetKey)) widgetNext[widgetKey] = widgetApi
|
||||
else if (widgetApi && typeof widgetApi.destroy === "function") widgetApi.destroy()
|
||||
}
|
||||
_pluginBarWidgetRegistryApis = widgetNext
|
||||
|
||||
var appNext = ({})
|
||||
for (var appKey in _pluginAppLibraryApis) {
|
||||
var appApi = _pluginAppLibraryApis[appKey]
|
||||
if (shell.pluginApiActive(appApi, plugins)) appNext[appKey] = appApi
|
||||
else if (appApi && typeof appApi.destroy === "function") appApi.destroy()
|
||||
}
|
||||
_pluginAppLibraryApis = appNext
|
||||
|
||||
var barStateNext = ({})
|
||||
for (var barStateKey in _pluginBarStateApis) {
|
||||
var barStateApi = _pluginBarStateApis[barStateKey]
|
||||
if (shell.pluginApiActive(barStateApi, plugins)) barStateNext[barStateKey] = barStateApi
|
||||
else if (barStateApi && typeof barStateApi.destroy === "function") barStateApi.destroy()
|
||||
}
|
||||
_pluginBarStateApis = barStateNext
|
||||
|
||||
var serviceNext = ({})
|
||||
for (var serviceKey in _pluginFirstPartyServiceApis) {
|
||||
var serviceApi = _pluginFirstPartyServiceApis[serviceKey]
|
||||
if (shell.pluginApiActive(serviceApi, plugins)) serviceNext[serviceKey] = serviceApi
|
||||
else if (serviceApi && typeof serviceApi.destroy === "function") serviceApi.destroy()
|
||||
}
|
||||
_pluginFirstPartyServiceApis = serviceNext
|
||||
|
||||
var entryNext = ({})
|
||||
for (var entryKey in _pluginBarEntryShellApis) {
|
||||
var entryApi = _pluginBarEntryShellApis[entryKey]
|
||||
if (entryApi && shell.barEntryConfigured(entryApi.pluginId)) entryNext[entryKey] = entryApi
|
||||
else if (entryApi && typeof entryApi.destroy === "function") entryApi.destroy()
|
||||
}
|
||||
_pluginBarEntryShellApis = entryNext
|
||||
}
|
||||
|
||||
function syncPluginApis() {
|
||||
shell.prunePluginApis()
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
for (var id in _pluginRegistryApis) {
|
||||
var registryApi = _pluginRegistryApis[id]
|
||||
var manifest = plugins[id]
|
||||
registryApi.manifest = shell.publicPluginManifest(manifest)
|
||||
registryApi.enabled = !!manifest && shell.pluginRegistry.isEnabled(id)
|
||||
}
|
||||
for (var widgetId in _pluginBarWidgetRegistryApis) {
|
||||
var widgetApi = _pluginBarWidgetRegistryApis[widgetId]
|
||||
widgetApi.widgets = shell.publicBarWidgetSnapshot()
|
||||
widgetApi.revision = shell.barWidgetRegistry.revision
|
||||
}
|
||||
for (var shellKey in _pluginShellApis) {
|
||||
var shellApi = _pluginShellApis[shellKey]
|
||||
var descriptor = _pluginShellApiDescriptors[shellKey]
|
||||
var shellManifest = descriptor ? plugins[descriptor.pluginId] : null
|
||||
shellApi.barConfig = shell.publicBarConfig()
|
||||
shellApi.idleConfig = shell.publicIdleConfigFor(shellManifest)
|
||||
}
|
||||
for (var entryKey in _pluginBarEntryShellApis)
|
||||
_pluginBarEntryShellApis[entryKey].barConfig = shell.publicBarConfig()
|
||||
}
|
||||
|
||||
function serviceFor(pluginId) {
|
||||
return _services[String(pluginId)] || null
|
||||
}
|
||||
|
||||
function firstPartyServiceFor(pluginId) {
|
||||
return serviceFor(pluginId)
|
||||
return serviceFor(shell.pluginRegistry.resolveEnabledId(pluginId))
|
||||
}
|
||||
|
||||
function isAuthenticationService(manifest, pluginId) {
|
||||
var key = String(pluginId || (manifest && manifest.id) || "")
|
||||
return AuthServiceStore.isTrusted(key)
|
||||
|| (!!manifest && Array.isArray(manifest.__hostCapabilities)
|
||||
&& manifest.__hostCapabilities.indexOf("authentication") !== -1)
|
||||
}
|
||||
|
||||
function ensureService(pluginId) {
|
||||
@@ -290,6 +908,8 @@ ShellRoot {
|
||||
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
|
||||
var url = pluginRegistry.entryPointUrl(manifest, "service")
|
||||
if (!url) return null
|
||||
var authenticationService = shell.isAuthenticationService(manifest, key)
|
||||
if (authenticationService && AuthServiceStore.has(key)) return null
|
||||
|
||||
var comp = Qt.createComponent(url, Component.PreferSynchronous)
|
||||
function finalize() {
|
||||
@@ -297,27 +917,37 @@ ShellRoot {
|
||||
console.warn("service plugin load failed for " + key + ": " + comp.errorString())
|
||||
return
|
||||
}
|
||||
var inst = comp.createObject(serviceHost)
|
||||
// Authentication services and third-party services have no visual
|
||||
// parent. Parenting either to serviceHost would let a plugin's object
|
||||
// traversal walk between the host and credential-bearing QML.
|
||||
var inst = comp.createObject(manifest.__isFirstParty && !authenticationService ? serviceHost : null)
|
||||
if (!inst) {
|
||||
console.warn("service plugin createObject returned null for", key)
|
||||
return
|
||||
}
|
||||
if ("omarchyPath" in inst) inst.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in inst) inst.shell = shell
|
||||
if ("manifest" in inst) inst.manifest = manifest
|
||||
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistry
|
||||
var snext = ({})
|
||||
for (var sk in _services) snext[sk] = _services[sk]
|
||||
snext[key] = inst
|
||||
_services = snext
|
||||
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
|
||||
if ("manifest" in inst) inst.manifest = shell.publicPluginManifest(manifest)
|
||||
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
|
||||
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistryFor(manifest)
|
||||
if (authenticationService) {
|
||||
// Never publish lock/polkit through ShellRoot._services. The private JS
|
||||
// import retains their lifetime without adding a traversable property
|
||||
// or QObject parent back to the host shell.
|
||||
AuthServiceStore.put(key, inst)
|
||||
} else {
|
||||
var snext = ({})
|
||||
for (var sk in _services) snext[sk] = _services[sk]
|
||||
snext[key] = inst
|
||||
_services = snext
|
||||
}
|
||||
}
|
||||
if (comp.status === Component.Loading) {
|
||||
comp.statusChanged.connect(finalize)
|
||||
return null
|
||||
}
|
||||
finalize()
|
||||
return _services[key] || null
|
||||
return authenticationService ? null : (_services[key] || null)
|
||||
}
|
||||
|
||||
function _syncServices() {
|
||||
@@ -329,33 +959,115 @@ ShellRoot {
|
||||
if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue
|
||||
if (!m.entryPoints || !m.entryPoints.service) continue
|
||||
if (!pluginRegistry.isEnabled(id)) continue
|
||||
if (_services[id]) continue
|
||||
var authenticationService = shell.isAuthenticationService(m, id)
|
||||
if (_services[id]) {
|
||||
if (authenticationService) {
|
||||
// A service that gains a trusted authentication capability must move
|
||||
// out of the host's public service map before it is recreated.
|
||||
var published = _services[id]
|
||||
if (published && typeof published.destroy === "function") published.destroy()
|
||||
var withoutPublished = ({})
|
||||
for (var publishedId in _services)
|
||||
if (publishedId !== id) withoutPublished[publishedId] = _services[publishedId]
|
||||
_services = withoutPublished
|
||||
} else {
|
||||
// A kept instance outlives the rescan; hand it the fresh manifest.
|
||||
var kept = _services[id]
|
||||
if (kept && "shell" in kept) kept.shell = shell.pluginShellFor(m)
|
||||
if (kept && "manifest" in kept) kept.manifest = shell.publicPluginManifest(m)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (AuthServiceStore.has(id)) {
|
||||
if (authenticationService) {
|
||||
AuthServiceStore.updateManifest(id, shell.publicPluginManifest(m))
|
||||
continue
|
||||
}
|
||||
// A service that loses its trusted authentication capability can move
|
||||
// back to the ordinary service map only after the isolated copy dies.
|
||||
AuthServiceStore.destroy(id)
|
||||
}
|
||||
ensureService(id)
|
||||
}
|
||||
// Drop services for plugins that have been disabled or removed.
|
||||
// Drop services for plugins that have been disabled or removed, or that
|
||||
// no longer declare a service entry point.
|
||||
for (var existingId in _services) {
|
||||
var stillThere = plugins[existingId]
|
||||
var stillService = stillThere && Array.isArray(stillThere.kinds)
|
||||
&& stillThere.kinds.indexOf("service") !== -1
|
||||
&& stillThere.entryPoints && stillThere.entryPoints.service
|
||||
var stillEnabled = stillThere && pluginRegistry.isEnabled(existingId)
|
||||
if (stillThere && stillEnabled) continue
|
||||
if (stillService && stillEnabled) continue
|
||||
var inst = _services[existingId]
|
||||
if (inst && typeof inst.destroy === "function") inst.destroy()
|
||||
var next = ({})
|
||||
for (var k in _services) if (k !== existingId) next[k] = _services[k]
|
||||
_services = next
|
||||
}
|
||||
// Authentication services are retained outside the root object graph, so
|
||||
// reconcile their disable/remove lifecycle separately from _services.
|
||||
var authenticationIds = AuthServiceStore.ids()
|
||||
for (var ai = 0; ai < authenticationIds.length; ai++) {
|
||||
var authenticationId = authenticationIds[ai]
|
||||
var authenticationManifest = plugins[authenticationId]
|
||||
var stillAuthenticationService = authenticationManifest
|
||||
&& Array.isArray(authenticationManifest.kinds)
|
||||
&& authenticationManifest.kinds.indexOf("service") !== -1
|
||||
&& authenticationManifest.entryPoints
|
||||
&& authenticationManifest.entryPoints.service
|
||||
if (stillAuthenticationService && pluginRegistry.isEnabled(authenticationId)
|
||||
&& shell.isAuthenticationService(authenticationManifest, authenticationId)) continue
|
||||
AuthServiceStore.destroy(authenticationId)
|
||||
}
|
||||
}
|
||||
|
||||
function serviceKeepLoaded(pluginId) {
|
||||
var plugins = pluginRegistry && pluginRegistry.installedPlugins
|
||||
var manifest = plugins ? plugins[pluginId] : null
|
||||
return !!(manifest && manifest.keepLoaded === true)
|
||||
}
|
||||
|
||||
// keepLoaded services (lock, idle, polkit) must survive plugin hot-reload.
|
||||
// Destroying omarchy.lock drops the ext-session-lock client while Hyprland
|
||||
// still holds the lock, which surfaces the crashed-lockscreen fallback.
|
||||
function unloadPluginServices() {
|
||||
var next = ({})
|
||||
for (var existingId in _services) {
|
||||
if (serviceKeepLoaded(existingId)) {
|
||||
next[existingId] = _services[existingId]
|
||||
continue
|
||||
}
|
||||
var inst = _services[existingId]
|
||||
if (inst && typeof inst.destroy === "function") inst.destroy()
|
||||
}
|
||||
_services = ({})
|
||||
_services = next
|
||||
var authenticationIds = AuthServiceStore.ids()
|
||||
for (var ai = 0; ai < authenticationIds.length; ai++) {
|
||||
var authenticationId = authenticationIds[ai]
|
||||
if (!serviceKeepLoaded(authenticationId))
|
||||
AuthServiceStore.destroy(authenticationId)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.pluginRegistry
|
||||
function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() }
|
||||
function onPluginsChanged() {
|
||||
shell.syncPluginApis()
|
||||
if (!shell.pluginReloading) shell._syncServices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.barWidgetRegistry
|
||||
function onChanged() { shell.syncPluginApis() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.appLibrary
|
||||
function onAppsChanged() {
|
||||
for (var id in shell._pluginAppLibraryApis)
|
||||
shell._pluginAppLibraryApis[id].appsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Writes inline settings to a bar layout entry or top-level plugin entry in
|
||||
@@ -627,10 +1339,10 @@ ShellRoot {
|
||||
onLoaded: {
|
||||
if (!item) return
|
||||
if ("omarchyPath" in item) item.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in item) item.shell = shell
|
||||
if ("manifest" in item) item.manifest = panelEntry.manifest
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
|
||||
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)
|
||||
if ("manifest" in item) item.manifest = shell.publicPluginManifest(panelEntry.manifest)
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(panelEntry.manifest)
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistryFor(panelEntry.manifest)
|
||||
// Plugins that pair a panel UI with a service entry read shared
|
||||
// state off `service`. Hand them the matching singleton if one was
|
||||
// loaded.
|
||||
@@ -696,7 +1408,8 @@ ShellRoot {
|
||||
schema: meta.schema || [],
|
||||
pluginId: manifest.id,
|
||||
sourceDir: manifest.__sourceDir || "",
|
||||
source: "plugin"
|
||||
source: "plugin",
|
||||
firstParty: !!manifest.__isFirstParty
|
||||
}
|
||||
|
||||
// A load already in flight for this URL registers itself when it
|
||||
|
||||
@@ -24,6 +24,20 @@ verify_core_packages() {
|
||||
pass "all Omarchy core packages are installed (${#missing[@]} missing)"
|
||||
}
|
||||
|
||||
verify_kernel_headers() {
|
||||
local kernel=linux-omarchy
|
||||
local release
|
||||
release=$(uname -r)
|
||||
omarchy-pkg-present linux-t2 && kernel=linux-t2
|
||||
|
||||
[[ $(cat "/usr/lib/modules/$release/pkgbase") == "$kernel" ]] ||
|
||||
fail "the installed system boots the supported kernel" "$release is not $kernel"
|
||||
omarchy-pkg-present "$kernel-headers" || fail "kernel headers are installed" "$kernel-headers is missing"
|
||||
[[ $(cat "/usr/lib/modules/$release/build/include/config/kernel.release") == "$release" ]] ||
|
||||
fail "headers match the running kernel" "$release has missing or mismatched headers"
|
||||
pass "the running $kernel kernel has matching headers ($release)"
|
||||
}
|
||||
|
||||
verify_defaults() {
|
||||
[[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium is the default browser"
|
||||
pass "Chromium is the default browser"
|
||||
@@ -112,7 +126,7 @@ verify_user_setup() {
|
||||
pass "Omarchy user state and shell configuration exist"
|
||||
}
|
||||
|
||||
for check in verify_core_packages verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
for check in verify_core_packages verify_kernel_headers verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
if ! ("$check"); then
|
||||
status=1
|
||||
fi
|
||||
|
||||
@@ -55,6 +55,14 @@ const entries = [
|
||||
}
|
||||
]
|
||||
|
||||
// Keep the packaged launcher when upstream rebuilds register their own entry.
|
||||
const configuredHides = new Set(fs.readFileSync(path.join(root, 'default/omarchy/launcher.hides'), 'utf8').trim().split(/\n/))
|
||||
const hermesEntries = [{ name: 'Hermes', id: 'hermes' }, { name: 'Hermes', id: 'hermes-desktop' }]
|
||||
for (const query of ['', 'hermes']) {
|
||||
const visible = search.sortedEntries(hermesEntries, query, entry => configuredHides.has(entry.id))
|
||||
assertDeepEqual(visible.map(row => row.entry.id), ['hermes-desktop'], 'only the packaged Hermes launcher is visible')
|
||||
}
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
apply_lock="$ROOT/bin/omarchy-apply-lock"
|
||||
|
||||
root_path_guard=$(awk '
|
||||
/^if \(\( EUID == 0 \)\); then$/ { inside = 1 }
|
||||
inside { print }
|
||||
inside && /^fi$/ { exit }
|
||||
' "$apply_lock")
|
||||
grep -Fx ' export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin' <<<"$root_path_guard" >/dev/null ||
|
||||
fail "the root lock helper replaces its inherited command path"
|
||||
if grep -E '(\.local/bin|target_user|target_home)' <<<"$root_path_guard" >/dev/null; then
|
||||
fail "the root lock helper does not retain a user-controlled command directory"
|
||||
fi
|
||||
pass "the root lock helper uses only trusted command directories"
|
||||
|
||||
grep -F '[[ -x /usr/bin/fprintd-list ]]' "$apply_lock" >/dev/null ||
|
||||
fail "the lock helper checks the trusted fprintd-list executable"
|
||||
grep -F '/usr/bin/fprintd-list "$target_user"' "$apply_lock" >/dev/null ||
|
||||
fail "the lock helper invokes fprintd-list by its trusted absolute path"
|
||||
if grep -F 'omarchy-cmd-present fprintd-list' "$apply_lock" >/dev/null ||
|
||||
grep -E '(^|[[:space:];&|])fprintd-list([[:space:]]|$)' "$apply_lock" >/dev/null ||
|
||||
grep -E 'command[[:space:]]+-v[[:space:]]+fprintd-list' "$apply_lock" >/dev/null; then
|
||||
fail "the lock helper does not resolve fprintd-list through PATH"
|
||||
fi
|
||||
pass "the lock helper pins fprintd-list to its packaged system path"
|
||||
|
||||
# Exercise the helper as real root when the suite already has it, or as root in
|
||||
# an unprivileged user namespace otherwise. A hardened kernel can disable user
|
||||
# namespaces, so preserve the static coverage above and skip only this probe.
|
||||
root_runner=()
|
||||
root_runtime_available=1
|
||||
if (( EUID != 0 )); then
|
||||
if command -v unshare >/dev/null && unshare --user --map-root-user true 2>/dev/null; then
|
||||
root_runner=(unshare --user --map-root-user)
|
||||
else
|
||||
root_runtime_available=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ! root_runtime_available )); then
|
||||
pass "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Retarget the two PAM files, the trusted fprintd-list binary, and the final
|
||||
# shell status query in copies under this scratch directory. The production
|
||||
# files and service stay untouched even when this suite itself runs as root.
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
poison_bin="$test_tmp/poison-bin"
|
||||
trusted_root_bin="$test_tmp/trusted-root-bin"
|
||||
trusted_fprintd="$test_tmp/trusted-fprintd-list"
|
||||
password_pam="$test_tmp/omarchy-lock-password"
|
||||
fingerprint_pam="$test_tmp/omarchy-lock-fingerprint"
|
||||
attack_marker="$test_tmp/user-fprintd-list-ran"
|
||||
trusted_uid="$test_tmp/trusted-fprintd-list.uid"
|
||||
trusted_args="$test_tmp/trusted-fprintd-list.args"
|
||||
attack_args="$test_tmp/user-fprintd-list.args"
|
||||
patched_helper="$test_tmp/omarchy-apply-lock-patched"
|
||||
absolute_only_helper="$test_tmp/omarchy-apply-lock-absolute-only"
|
||||
root_path_only_helper="$test_tmp/omarchy-apply-lock-root-path-only"
|
||||
unprotected_helper="$test_tmp/omarchy-apply-lock-unprotected"
|
||||
target_user=omarchy-regression-user
|
||||
mkdir -p "$poison_bin" "$trusted_root_bin"
|
||||
|
||||
# The runtime copy pins to this isolated root path. It contains every bare
|
||||
# command the exercised helper needs, but deliberately no fprintd-list.
|
||||
for helper in grep rm tee; do
|
||||
ln -s "/usr/bin/$helper" "$trusted_root_bin/$helper"
|
||||
done
|
||||
|
||||
export TEST_ATTACK_ARGS="$attack_args"
|
||||
export TEST_ATTACK_MARKER="$attack_marker"
|
||||
export TEST_TRUSTED_ARGS="$trusted_args"
|
||||
export TEST_TRUSTED_UID="$trusted_uid"
|
||||
|
||||
cat >"$trusted_fprintd" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$EUID" >"$TEST_TRUSTED_UID"
|
||||
printf '%s\n' "$*" >"$TEST_TRUSTED_ARGS"
|
||||
echo "Fingerprints are enrolled"
|
||||
EOF
|
||||
|
||||
cat >"$poison_bin/fprintd-list" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$EUID" >"$TEST_ATTACK_MARKER"
|
||||
printf '%s\n' "$*" >"$TEST_ATTACK_ARGS"
|
||||
echo "Fingerprints are enrolled"
|
||||
EOF
|
||||
|
||||
chmod +x "$trusted_fprintd" "$poison_bin/fprintd-list"
|
||||
|
||||
prepare_helper() {
|
||||
local destination="$1" keep_root_path="$2" use_absolute_fprintd="$3"
|
||||
|
||||
awk \
|
||||
-v password_pam="$password_pam" \
|
||||
-v fingerprint_pam="$fingerprint_pam" \
|
||||
-v trusted_root_bin="$trusted_root_bin" \
|
||||
-v trusted_fprintd="$trusted_fprintd" \
|
||||
-v keep_root_path="$keep_root_path" \
|
||||
-v use_absolute_fprintd="$use_absolute_fprintd" '
|
||||
{
|
||||
line = $0
|
||||
gsub("/etc/pam\\.d/omarchy-lock-password", "\"" password_pam "\"", line)
|
||||
gsub("/etc/pam\\.d/omarchy-lock-fingerprint", "\"" fingerprint_pam "\"", line)
|
||||
|
||||
if (line == "if (( EUID == 0 )); then" && keep_root_path == 0) {
|
||||
print "if (( 0 )); then"
|
||||
next
|
||||
}
|
||||
if (line == " export PATH=/usr/share/omarchy/bin:/usr/local/bin:/usr/bin:/bin") {
|
||||
print " export PATH=\"" trusted_root_bin "\""
|
||||
next
|
||||
}
|
||||
if (line == "if [[ -x /usr/bin/fprintd-list ]] &&") {
|
||||
if (use_absolute_fprintd == 1) {
|
||||
print "if [[ -x \"" trusted_fprintd "\" ]] &&"
|
||||
} else {
|
||||
print "if command -v fprintd-list >/dev/null 2>&1 &&"
|
||||
}
|
||||
next
|
||||
}
|
||||
if (line == " /usr/bin/fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then") {
|
||||
if (use_absolute_fprintd == 1) {
|
||||
print " \"" trusted_fprintd "\" \"$target_user\" 2>/dev/null | grep -qi finger; then"
|
||||
} else {
|
||||
print " fprintd-list \"$target_user\" 2>/dev/null | grep -qi finger; then"
|
||||
}
|
||||
next
|
||||
}
|
||||
if (line == "if omarchy-shell lock status >/dev/null 2>&1; then") {
|
||||
print "if false; then"
|
||||
next
|
||||
}
|
||||
|
||||
print line
|
||||
}
|
||||
' "$apply_lock" >"$destination"
|
||||
chmod +x "$destination"
|
||||
}
|
||||
|
||||
prepare_helper "$patched_helper" 1 1
|
||||
prepare_helper "$absolute_only_helper" 0 1
|
||||
prepare_helper "$root_path_only_helper" 1 0
|
||||
prepare_helper "$unprotected_helper" 0 0
|
||||
|
||||
for helper in "$patched_helper" "$absolute_only_helper" "$root_path_only_helper" "$unprotected_helper"; do
|
||||
if grep -F '/etc/pam.d/' "$helper" >/dev/null ||
|
||||
grep -F '/usr/bin/fprintd-list' "$helper" >/dev/null ||
|
||||
grep -F 'omarchy-shell lock status' "$helper" >/dev/null; then
|
||||
fail "the isolated root fixture redirects every live-system lock-helper target"
|
||||
fi
|
||||
done
|
||||
|
||||
reset_runtime_files() {
|
||||
rm -f "$password_pam" "$fingerprint_pam" "$trusted_uid" "$trusted_args" "$attack_marker" "$attack_args"
|
||||
}
|
||||
|
||||
run_as_root() {
|
||||
local helper="$1" description="$2" output
|
||||
|
||||
if ! output=$(PATH="$poison_bin:/usr/bin:/bin" OMARCHY_INSTALL_USER="$target_user" \
|
||||
"${root_runner[@]}" /bin/bash "$helper" 2>&1); then
|
||||
fail "$description" "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
reset_runtime_files
|
||||
run_as_root "$patched_helper" "the fully hardened lock helper runs in an isolated root context"
|
||||
[[ ! -e $attack_marker ]] || fail "the hardened root lock helper executes the user-planted fprintd-list"
|
||||
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the trusted fprintd-list probe runs with EUID 0"
|
||||
grep -Fx "$target_user" "$trusted_args" >/dev/null || fail "the trusted fprintd-list probe receives the target user"
|
||||
[[ -s $password_pam && -s $fingerprint_pam ]] ||
|
||||
fail "the isolated root lock-helper run writes both scratch PAM fixtures"
|
||||
pass "the hardened root lock helper uses the trusted fingerprint probe"
|
||||
|
||||
reset_runtime_files
|
||||
run_as_root "$absolute_only_helper" "the absolute-path-only lock helper runs in an isolated root context"
|
||||
[[ ! -e $attack_marker ]] || fail "an absolute fprintd-list path permits the user-planted command"
|
||||
grep -Fx '0' "$trusted_uid" >/dev/null || fail "the absolute-path defense runs the trusted probe as root"
|
||||
pass "the absolute fprintd-list path independently blocks the user-planted command"
|
||||
|
||||
reset_runtime_files
|
||||
run_as_root "$root_path_only_helper" "the root-PATH-only lock helper runs in an isolated root context"
|
||||
[[ ! -e $attack_marker ]] || fail "the trusted root path permits the user-planted fprintd-list"
|
||||
pass "the trusted root path independently blocks the user-planted command"
|
||||
|
||||
# Mutation control: removing both protections must execute the planted command
|
||||
# as UID 0, proving the matrix detects the original privilege-boundary failure.
|
||||
reset_runtime_files
|
||||
run_as_root "$unprotected_helper" "the unprotected mutation runs in an isolated root context"
|
||||
grep -Fx '0' "$attack_marker" >/dev/null ||
|
||||
fail "the root lock-helper fixture detects a PATH-resolved fprintd-list regression"
|
||||
grep -Fx "$target_user" "$attack_args" >/dev/null ||
|
||||
fail "the planted fprintd-list receives the target user"
|
||||
[[ -s $fingerprint_pam ]] || fail "the planted fprintd-list controls the fingerprint PAM branch"
|
||||
pass "the root lock-helper matrix rejects the vulnerable PATH lookup"
|
||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user