Compare commits

..
Author SHA1 Message Date
ZacharyZhang-NY 466c2ca14a Point package managers at China mirrors on cn installs
npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler,
and Maven reach their registries slowly or not at all from China.
On a cn install, link the profile's environment, cargo and gem
drop-ins into place. A regular file the admin wrote is never
replaced; the install fails instead.
2026-10-12 00:29:13 -04:00
Kros DaiandClaude Opus 5.5 ec4579a461 🐛 fix(test): skip exact zone match without tzdata.zi
- 🐛 minimal containers lack the link table, so the exact china zone check failed on a correct profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:44:15 -04:00
Kros DaiandClaude Opus 5.5 d9a781e3c9 📝 docs(region): scope the refresh render boundary
- 📝 state that same-user code is outside the refresh boundary, so reviewers see the trade-off was deliberate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:08:36 -04:00
Kros DaiandClaude Opus 5.5 a6c3480ff8 📝 docs(region): state marker mode and archlinuxcn risk
- 📝 refresh reads the marker unprivileged, and rolling archlinuxcn packages can outpace stable snapshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 545190596d ♻️ refactor(pacman): branch on render with if/else
- ♻️ follow the repo style instead of exiting early to skip the install block

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 b381093e32 ✅ test(region): derive china zones from tzdata
- ✅ dropping a backward-compatible zone name from the profile now fails the test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 9d74c813f1 🐛 fix(channel): require every region fragment for dev
- 🐛 leaving dev refreshes stable or rc with the checkout's tools, so a profile missing those fragments would drop regional repositories
- ✅ refuse a profile missing any single fragment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 fce85abf48 📝 docs(region): explain mirror order and fallback
- 📝 pacman's three-error skip and first-server databases decide what the china replicas must serve

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 3c68a1bb04 ✨ feat(region): put the china channel mirrors first
- ✨ omarchy now runs per-channel china replicas, so they should lead instead of trailing as a fallback
- ♻️ prepend per-channel fragments so each channel's own mirror stays the fallback
- 🔥 drop the ustc arch mirror, which was never a stable or rc snapshot replica

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 b89def6c7f 📝 docs(region): describe timezone-derived regions
- 📝 the --region build flag is gone, so the installer contract and validation plan change with it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:46 -04:00
Kros DaiandClaude Opus 5.5 0de49b231a ✨ feat(region): map timezones to regions
- ✨ one iso serves every region, so the installer derives it from the timezone the user already picks
- ✅ reject unknown zones and zones claimed by two regions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:41 -04:00
Kros DaiandClaude Opus 5.5 3e00a48c00 ✅ test(pacman): sandbox the region for every boundary test
- ✅ The channel and update boundary tests copied the real render and dev-region guard, so on a China install they read the host's `/etc/omarchy/region` and failed before reaching their checks
- ♻️ The shared fixture now points both lookups at a sandbox target that starts global, and the regional test only writes `cn` there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:14:56 -04:00
Kros DaiandClaude Opus 5.5 a1bdc46119 ✅ test(pacman): isolate the regional refresh boundary test
- ✅ the render read the host's region marker, so a global machine never rendered China and a China machine failed on missing fragments
- ✅ check the finished China files root installs, not only the copy order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 7a60c8dea4 🐛 fix(channel): require a usable region profile for dev
- 🐛 an empty profile directory or a helper that cannot run passed the guard, linked dev, and then broke the refresh
- ✅ the success case now uses a complete profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 6be88d14a5 🐛 fix(pacman): remove the render dir on every refresh exit
- 🐛 a failed render, a refused sudo prompt, or a signal left the scratch directory behind; repeated attempts piled them up
- 🔒 the cleanup runs before the sudo revocation, which stays the last step of the exit path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 21:58:48 -04:00
Kros DaiandClaude Opus 5.5 50a26be6a7 📝 docs(region): scope the cn profile to repositories
- 📝 frame USTC and ArchLinuxCN as a bridge until an official China mirror exists
- 📝 record that language, timezone, and input defaults follow as separate changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d5ef1c1d15 🔥 refactor(region): drop input defaults from the cn profile
- 🔥 keep this change to the regional build architecture; Chinese input gets its own proposal instead of fixing one engine here
- ✅ assert the shipped profile adds only its repository keyring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d024c82a9d 📝 docs(plans): move the china proposal to a discussion
- 📝 upstream plans are implementation plans; the open questions belong in the discussion that links these changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d7f17a0424 ♻️ refactor(pacman): render regional config without root
- 🔒 upstream refresh now gives root only command-scoped copies, so regional files are rendered as the caller and installed with the same four `sudo cp` calls
- ✅ exercise refresh only inside the sudo-boundary sandbox; the old host-level test reached real sudo once refresh pinned its PATH

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 87a2931209 📝 docs(region): drop the unreachable menu input gate
- 📝 the menu bypasses the input method, so requiring Pinyin there made the release procedure impossible until upstream fixes it
- 📝 record the required `skel` profile and the dev-switch region check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 647979bde7 🐛 fix(pacman): keep existing config file modes
- 🐛 refresh used to copy over the existing files, so a customized mode must survive the shared regional applicator too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 123535ea14 🐛 fix(channel): keep regional repos when switching to dev
- 🛡️ refuse a dev checkout that lacks this machine's region profile before linking it, since its own refresh would silently replace the regional pacman files
- ✅ isolate channel tests from the host region marker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros Dai 645cfc2a36 📝 docs(region): explain china bootstrap choices
- 📝 clarify optional defaults, trust requirements, and opt-out behavior
- ⚠️ document snapshot compatibility and release validation limits

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai e989960895 ✅ test(region): guard runtime defaults
- ✅ protect global behavior and regional mirror ordering
- ⌨️ guard personal settings and reserved input shortcuts

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai 48f7c411c0 ✨ feat(pacman): preserve regional defaults
- ✨ share regional repository generation across install and refresh
- 🛡️ reject invalid profiles before replacing configuration

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros DaiandAmp 7727d40c92 ✨ feat(region): seed offline chinese input
- ✨ make first-login pinyin available without downloads
- ⌨️ preserve compose and terminal prefix shortcuts

Amp-Thread-ID: https://ampcode.com/threads/T-01a0df38-0ea7-759b-bcff-29da223cc567
Co-authored-by: Amp <amp@ampcode.com>
2026-10-06 14:43:25 -04:00
559 changed files with 1736 additions and 28145 deletions

No files matched your search

+5 -24
View File
@@ -63,30 +63,6 @@ guidance does not drift from the router.
- `$OMARCHY_PATH` is set at the top level by the uwsm session environment and is always available to Omarchy runtime code. - `$OMARCHY_PATH` is set at the top level by the uwsm session environment and is always available to Omarchy runtime code.
- Commands in `bin/` and Quickshell QML should rely on `$OMARCHY_PATH` / `Quickshell.env("OMARCHY_PATH")`; do not derive fallback paths from `HOME`, `Quickshell.shellDir`, or re-export/default `OMARCHY_PATH` manually. - Commands in `bin/` and Quickshell QML should rely on `$OMARCHY_PATH` / `Quickshell.env("OMARCHY_PATH")`; do not derive fallback paths from `HOME`, `Quickshell.shellDir`, or re-export/default `OMARCHY_PATH` manually.
# Platforms
Omarchy runs on x86 and aarch64 (ARM). A platform name is the CPU architecture, then a hardware family only when that family needs handling of its own:
- `x86` - every x86_64 machine
- `aarch64` - every ARM machine without a family of its own (Snapdragon laptops, Raspberry Pis and ARM VMs today)
- `aarch64-apple` - Apple Silicon Macs
A new family is named `aarch64-<family>` (e.g. `aarch64-qualcomm`, `aarch64-n1x`) and is added only once it needs code of its own, so `aarch64*` always means ARM. `omarchy-hw-platform` prints the most specific name. Use these exact names wherever a platform is named: package lists, pacman template directories, image manifests, dispatch registration, tests and docs. Never introduce `generic`, `arm64`, `apple-silicon` or other spellings; the old `apple-silicon`, `generic-aarch64` and `generic` are read only for compatibility with older image builders.
Ask a helper; don't read `uname -m` or the device tree in feature code:
- `omarchy-hw-x86` / `omarchy-hw-aarch64` - the CPU architecture. Use them for binary and ABI availability: a package with no aarch64 build, an x86 guest VM, multilib.
- `omarchy-hw-aarch64-apple` (and later `omarchy-hw-aarch64-<family>`) - built on `omarchy-hw-platform`, image-build aware. Use them for hardware behaviour. `omarchy-hw-apple-silicon` is a hidden alias kept for the omarchy-mac packages; don't use it in new code.
Where platform code lives:
- Packages: `install/omarchy-base.packages` for everyone, `install/omarchy-aarch64.packages` for every ARM machine, `install/omarchy-<platform>.packages` for one family, and base packages with no aarch64 build in `install/omarchy-x86_64-only.packages`. `omarchy-pkg-defaults` composes them.
- Pacman templates: `default/pacman/` for x86, `default/pacman/<platform>/` for each ARM platform.
- Hardware setup: `install/hardware/<vendor-or-family>/`, gated by a predicate. `install/hardware/apple/` holds Intel Mac quirks; Apple Silicon's own setup ships in the omarchy-mac packages.
- Boot chains Omarchy can't drive generically: a platform package behind `omarchy-lifecycle-dispatch` ([`docs/lifecycle-dispatch.md`](docs/lifecycle-dispatch.md)). Platforms that boot Limine with UKIs like x86 keep the generic path.
The experience should be the same on every platform. When a package or feature is missing or broken on one, the default is to get it fixed upstream (an aarch64 build, a bug report, a patch to the maintainer) and leave the gap visible meanwhile, not to paper over it. When asked for a platform workaround, say so and propose the upstream fix first. Hiding a menu entry or skipping a package is fine only for things that can't exist there (an x86 guest VM, an x86-only proprietary binary). Alternative packages and platform-only replacements are a last resort, used only when upstream can't or won't fix it, with the reason written next to the code.
# Privileged Commands # Privileged Commands
- Follow the "Privilege Escalation" section of `default/agents/skills/omarchy/SKILL.md`. It draws the - Follow the "Privilege Escalation" section of `default/agents/skills/omarchy/SKILL.md`. It draws the
@@ -120,6 +96,11 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Do not add `aliases` to new menu entries. Aliases are reserved for - Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility. established alternate names users already type, kept for compatibility.
# Region
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create.
# Config Structure # Config Structure
- `config/` - default configs copied to `~/.config/` - `config/` - default configs copied to `~/.config/`
-2
View File
@@ -12,8 +12,6 @@ commands and reusable setup leaves:
- avoid `exit` in sourced setup scripts unless intentionally aborting setup. - avoid `exit` in sourced setup scripts unless intentionally aborting setup.
- use `$OMARCHY_INSTALL` and `$OMARCHY_PATH` instead of hard-coded Omarchy paths. - 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 root-scoped hardware setup under `install/hardware/` and orchestrate it through `install/hardware/all.sh`.
- in an image build every `run_logged` leaf in `install/hardware/all.sh` is queued and runs on the machine's first boot instead (`bin/omarchy-provision-hardware`, see `docs/file-layout.md`), possibly offline, using packages already in the image, and before any user exists; keep those leaves idempotent and put nothing but `run_logged` lines in `all.sh`.
- gate platform-specific setup on a predicate built on `omarchy-hw-platform` (`omarchy-hw-aarch64-apple`), not on `uname -m`; architecture checks (`omarchy-hw-x86`, `omarchy-hw-aarch64`) are only for ABI, binary availability or a repository's `$arch`. Platform names and where platform code lives are in the Platforms section of `AGENTS.md`.
- 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. - 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. - 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. - prefer helper commands for package and command checks where available.
-4
View File
@@ -45,7 +45,3 @@ unicode characters. Agent file-editing tools can strip multi-byte codepoints
in some positions — do **not** rewrite widget files wholesale through those in some positions — do **not** rewrite widget files wholesale through those
tools. For glyph fixes, make a targeted edit with the surrounding context, or tools. For glyph fixes, make a targeted edit with the surrounding context, or
use a Python script that inserts codepoints via `chr(0xXXXXX)`. use a Python script that inserts codepoints via `chr(0xXXXXX)`.
## Palette references
Import `qs.Commons as Commons` and use `Commons.Color` for the palette singleton, including `Connections.target`. Qt 6.12 introduces a `Color` type in `QtQuick` that shadows unqualified palette references. This applies to first-party components and third-party plugin QML alike. Keep the unqualified `qs.Commons` import when the file also uses `Style`, `Util`, or `Border`.
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Disk Usage
Exec=xdg-terminal-exec --app-id=TUI.float -e bash -c "dua i /"
Terminal=false
Type=Application
Icon=disk-usage
StartupNotify=true
-1
View File
@@ -46,7 +46,6 @@ GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
GROUP_DESCRIPTIONS[default]="Default application selection" GROUP_DESCRIPTIONS[default]="Default application selection"
GROUP_DESCRIPTIONS[dev]="Omarchy development tools" GROUP_DESCRIPTIONS[dev]="Omarchy development tools"
GROUP_DESCRIPTIONS[disk]="Disk performance helpers" GROUP_DESCRIPTIONS[disk]="Disk performance helpers"
GROUP_DESCRIPTIONS[dictation]="Dictation recording"
GROUP_DESCRIPTIONS[display]="Display and text scaling" GROUP_DESCRIPTIONS[display]="Display and text scaling"
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration" GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption" GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
+1 -15
View File
@@ -19,9 +19,6 @@ for diagnostics or after hardware/package updates.
--defer-provisioning runs without an install user (deferred-provisioning installs create the user at --defer-provisioning runs without an install user (deferred-provisioning installs create the user at
first boot); group grants are recorded in /var/lib/omarchy/provisioning/groups. first boot); group grants are recorded in /var/lib/omarchy/provisioning/groups.
In an image build (/var/lib/omarchy/image/target exists) nothing runs here: every step is queued
for omarchy-provision-hardware on the machine's first boot.
USAGE USAGE
} }
@@ -69,21 +66,10 @@ else
fi fi
export OMARCHY_INSTALL_USER="$install_user" export OMARCHY_INSTALL_USER="$install_user"
# Only omarchy-provision-hardware runs steps as an image's first boot.
unset OMARCHY_IMAGE_DEFERRED_HARDWARE
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
export OMARCHY_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}" export OMARCHY_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}"
export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}" export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}"
export PATH="$OMARCHY_PATH/bin:$PATH" export PATH="$OMARCHY_PATH/bin:$PATH"
source "$OMARCHY_INSTALL/helpers/logging.sh" source "$OMARCHY_INSTALL/helpers/logging.sh"
source "$OMARCHY_INSTALL/helpers/image-target.sh" source "$OMARCHY_INSTALL/hardware/all.sh"
omarchy_image_init
omarchy_image_lock
if omarchy_image_manifest_present; then
omarchy_image_read_manifest
omarchy_image_defer_hardware
else
source "$OMARCHY_INSTALL/hardware/all.sh"
fi
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
usage() {
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
exit 1
}
# --render only builds the two files, so an unprivileged caller can hand root
# nothing but plain copies of the finished result.
render_dir=""
if [[ ${1:-} == "--render" ]]; then
(( $# == 3 || $# == 4 )) || usage
channel="$2"
render_dir="$3"
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
etc="${4:-/}/etc"
else
(( $# >= 1 && $# <= 2 )) || usage
channel="$1"
etc="${2:-/}/etc"
fi
case "$channel" in
stable|rc|edge) ;;
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
esac
# The ISO writes this plain-text marker before target finalization. Missing
# markers keep existing installations global; never source it as shell code.
region=global
if [[ -e $etc/omarchy/region ]]; then
region=$(<"$etc/omarchy/region")
fi
case "$region" in
global|cn) ;;
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
esac
# Build both files before replacing either. Explicit refreshes retain backups;
# ordinary updates do not call this command or enforce regional defaults.
if [[ -n $render_dir ]]; then
staging="$render_dir"
else
staging=$(mktemp -d)
trap 'rm -rf "$staging"' EXIT
fi
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
if [[ $region == "global" ]]; then
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
else
profile="$OMARCHY_PATH/default/regions/$region/pacman"
printf '\n' >> "$staging/pacman.conf"
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
# pacman tries servers in order, so the regional mirror leads and the
# channel's own mirror stays as the fallback.
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
fi
if [[ -z $render_dir ]]; then
mkdir -p "$etc/pacman.d"
for file in pacman.conf pacman.d/mirrorlist; do
if [[ -e $etc/$file ]]; then
cp -f "$etc/$file" "$etc/$file.bak"
fi
done
# Copying over an existing file keeps its owner and mode, as refresh always has.
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
fi
+1 -9
View File
@@ -15,14 +15,6 @@ fi
wpctl set-default "$node_id" 2>/dev/null || true wpctl set-default "$node_id" 2>/dev/null || true
pactl set-default-source "$source_name" 2>/dev/null || true pactl set-default-source "$source_name" 2>/dev/null || true
# A filter's own capture (the input half of a filter chain, loopback or echo pactl list short source-outputs 2>/dev/null | awk '{ print $1 }' | while read -r output; do
# canceller, which PipeWire joins to its output by node.link-group) is the
# processing, not an app: moving it would feed the filter from the new input, or
# from its own output. pactl translates the listing, so it is read in the C
# locale.
LC_ALL=C pactl list source-outputs 2>/dev/null | awk '
/^Source Output #/ { if (id != "" && !filter) print id; id = substr($3, 2); filter = 0 }
/^[[:space:]]*node\.link-group = / { filter = 1 }
END { if (id != "" && !filter) print id }' | while read -r output; do
[[ -n $output ]] && pactl move-source-output "$output" "$source_name" 2>/dev/null || true [[ -n $output ]] && pactl move-source-output "$output" "$source_name" 2>/dev/null || true
done done
+4 -17
View File
@@ -1,27 +1,14 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Print PulseAudio sink or source availability for the shell # omarchy:summary=Print PulseAudio sink availability for the shell
# omarchy:group=audio # omarchy:group=audio
# omarchy:args=[sources]
# With "sources", print source availability instead: a jack input such as a
# headset microphone reports its port unavailable while nothing is plugged in.
# pactl translates the listing ("not available" too), so it is read in the C
# locale.
kind=sinks
header="Sink #"
if [[ ${1:-} == "sources" ]]; then
kind=sources
header="Source #"
fi
# A speaker tuning is a virtual sink in front of the real speakers. Both exist # A speaker tuning is a virtual sink in front of the real speakers. Both exist
# in the graph, but selecting the physical one would only bypass the tuning, so # in the graph, but selecting the physical one would only bypass the tuning, so
# report it unavailable and keep it out of the output list. # report it unavailable and keep it out of the output list.
fronted="" fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
[[ $kind == sinks ]] && fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
LC_ALL=C pactl list "$kind" 2>/dev/null | awk -v fronted="$fronted" -v header="$header" ' pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
function emit_sink() { function emit_sink() {
if (name == "") return if (name == "") return
if (fronted != "" && name == fronted) { if (fronted != "" && name == fronted) {
@@ -31,7 +18,7 @@ LC_ALL=C pactl list "$kind" 2>/dev/null | awk -v fronted="$fronted" -v header="$
print name "\t" ((port_count == 0 || available) ? 1 : 0) print name "\t" ((port_count == 0 || available) ? 1 : 0)
} }
index($0, header) == 1 { /^Sink #/ {
emit_sink() emit_sink()
name = "" name = ""
in_ports = 0 in_ports = 0
+4 -10
View File
@@ -2,16 +2,10 @@
# omarchy:summary=Returns true if a battery is present on the system. # omarchy:summary=Returns true if a battery is present on the system.
# Not every battery is called BAT*: Apple Silicon reports macsmc-battery and for bat in /sys/class/power_supply/BAT*; do
# some Framework and Chromebook firmware CMB0, so decide by type, not by name. [[ -r $bat/present ]] &&
power_supply_path="${OMARCHY_POWER_SUPPLY_PATH:-/sys/class/power_supply}" [[ $(cat $bat/present) == "1" ]] &&
[[ $(cat $bat/type) == "Battery" ]] &&
for bat in "$power_supply_path"/*; do
[[ -r $bat/present && $(<"$bat/present") == "1" ]] || continue
[[ -r $bat/type && $(<"$bat/type") == "Battery" ]] || continue
# A wireless mouse or keyboard registers a Battery too, scoped Device. The
# one that powers the machine is scoped System, or unscoped on older drivers.
[[ -r $bat/scope && $(<"$bat/scope") == "Device" ]] && continue
exit 0 exit 0
done done
+6 -27
View File
@@ -18,23 +18,11 @@ case "${1:-}" in
;; ;;
esac esac
# UPower names the device after the sysfs entry, and only some kernels call battery=$(upower -e 2>/dev/null | grep BAT | head -n 1)
# that BAT*: Apple Silicon reports battery_macsmc_battery. Match on the class,
# then take the first one that powers the machine: a wireless mouse or keyboard
# is a battery_ device too, and UPower marks it "power supply: no".
battery=""
battery_info=""
while read -r candidate; do
[[ -n $candidate ]] || continue
candidate_info=$(upower -i "$candidate" 2>/dev/null) || continue
if grep -qE '^\s*power supply:\s+yes' <<<"$candidate_info"; then
battery=$candidate
battery_info=$candidate_info
break
fi
done < <(upower -e 2>/dev/null | grep '/battery_')
[[ -z $battery ]] && exit 0 [[ -z $battery ]] && exit 0
battery_info=$(upower -i "$battery")
percentage=$(awk '/percentage/ { print int($2); exit }' <<<"$battery_info") percentage=$(awk '/percentage/ { print int($2); exit }' <<<"$battery_info")
capacity=$(awk '/energy-full:/ { printf "%d", $2; exit }' <<<"$battery_info") capacity=$(awk '/energy-full:/ { printf "%d", $2; exit }' <<<"$battery_info")
time_remaining=$(awk '/time to (empty|full)/ { time_remaining=$(awk '/time to (empty|full)/ {
@@ -68,10 +56,7 @@ elif [[ -r $battery_path/current_now && -r $battery_path/voltage_now ]]; then
'BEGIN { print microamps * microvolts / 1000000000000 }') 'BEGIN { print microamps * microvolts / 1000000000000 }')
fi fi
# Some kernels sign power_now by direction (negative while discharging on
# Apple Silicon); the state field already says which way it flows.
power_rate=$(awk -v rate="${power_rate_raw:-0}" 'BEGIN { power_rate=$(awk -v rate="${power_rate_raw:-0}" 'BEGIN {
if (rate < 0) rate = -rate
rounded = sprintf("%.1f", rate) rounded = sprintf("%.1f", rate)
sub(/\.0$/, "", rounded) sub(/\.0$/, "", rounded)
print rounded print rounded
@@ -80,12 +65,8 @@ state=$(awk '/state/ { print $2; exit }' <<<"$battery_info")
threshold_start=$(awk '/charge-start-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info") threshold_start=$(awk '/charge-start-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
threshold_end=$(awk '/charge-end-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info") threshold_end=$(awk '/charge-end-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
# Read the thresholds from the battery UPower resolved rather than a BAT* glob, [[ -z $threshold_end ]] && threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
# which misses every battery with another name. [[ -z $threshold_start ]] && threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
[[ -n $threshold_end || ! -r $battery_path/charge_control_end_threshold ]] || threshold_end=$(<"$battery_path/charge_control_end_threshold")
[[ -n $threshold_start || ! -r $battery_path/charge_control_start_threshold ]] || threshold_start=$(<"$battery_path/charge_control_start_threshold")
[[ -n $threshold_end ]] || threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
[[ -n $threshold_start ]] || threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
ac_online=false ac_online=false
for supply in "$power_supply_path"/*; do for supply in "$power_supply_path"/*; do
@@ -126,9 +107,7 @@ if [[ $shell_output == "true" ]]; then
printf 'size\t%s\n' "${capacity}Wh" printf 'size\t%s\n' "${capacity}Wh"
printf 'time\t%s\n' "$time_remaining" printf 'time\t%s\n' "$time_remaining"
cycles="" cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
[[ ! -r $battery_path/cycle_count ]] || cycles=$(<"$battery_path/cycle_count")
[[ -n $cycles ]] || cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
[[ -n $cycles ]] && printf 'cycles\t%s\n' "$cycles" [[ -n $cycles ]] && printf 'cycles\t%s\n' "$cycles"
+1 -28
View File
@@ -40,30 +40,7 @@ use_apple_display() {
} }
use_ddc_display() { use_ddc_display() {
[[ -n $monitor ]] && ! monitor_is_internal && monitor_may_have_ddc [[ -n $monitor ]] && ! monitor_is_internal
}
# A platform whose display driver registers no DDC channel says so in its
# displays.conf (see docs/file-layout.md): there a probe finds only unrelated
# I2C buses, slowly, so only a connector with a ddc node is probed. Everywhere
# else every external monitor is.
monitor_may_have_ddc() {
local displays_conf=/usr/share/omarchy-platform/displays.conf directive argument
[[ -r $displays_conf ]] || return 0
while IFS=$' \t\r' read -r directive argument || [[ -n $directive ]]; do
if [[ $directive == "ddc-require-connector-ddc" && -z $argument ]]; then
compgen -G "/sys/class/drm/card*-$monitor/ddc" >/dev/null
return
fi
done <"$displays_conf"
return 0
}
# The kernel backlight is the built-in panel's, so an external monitor without
# DDC fails rather than dimming the panel.
use_backlight_display() {
[[ -z $monitor ]] || monitor_is_internal
} }
if (( $# == 0 )); then if (( $# == 0 )); then
@@ -73,8 +50,6 @@ if (( $# == 0 )); then
elif use_ddc_display; then elif use_ddc_display; then
omarchy-brightness-display-ddc "$monitor" omarchy-brightness-display-ddc "$monitor"
exit exit
elif ! use_backlight_display; then
exit 1
fi fi
device="$(omarchy-hw-display)" || exit 1 device="$(omarchy-hw-display)" || exit 1
@@ -110,8 +85,6 @@ if use_apple_display; then
elif use_ddc_display; then elif use_ddc_display; then
brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1 brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1
(( no_osd )) || omarchy-osd -i brightness -p "$brightness" (( no_osd )) || omarchy-osd -i brightness -p "$brightness"
elif ! use_backlight_display; then
exit 1
else else
# Current device highlighted # Current device highlighted
device="$(omarchy-hw-display)" || exit 1 device="$(omarchy-hw-display)" || exit 1
+48 -307
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Start or stop screen recording # omarchy:summary=Start or stop screen recording
# omarchy:group=capture # omarchy:group=capture
# omarchy:args=[--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording] [--status] # omarchy:args=[--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording]
# omarchy:examples=omarchy screenrecord | omarchy capture screenrecording --with-desktop-audio # omarchy:examples=omarchy screenrecord | omarchy capture screenrecording --with-desktop-audio
# omarchy:aliases=omarchy screenrecord # omarchy:aliases=omarchy screenrecord
# #
@@ -14,20 +14,19 @@
# DMA-BUF modifier import on some configurations, leaving recording unable to # DMA-BUF modifier import on some configurations, leaving recording unable to
# start. # start.
# #
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends the recorder's stderr (and the # Env: OMARCHY_SCREENRECORD_DEBUG=true appends gpu-screen-recorder's stderr (and
# picker target it was launched with) to $XDG_RUNTIME_DIR/omarchy-screenrecord.log # the picker target it was launched with) to
# so users can attach a log when reporting capture failures. # $XDG_RUNTIME_DIR/omarchy-screenrecord.log so users can attach a log when
# # reporting capture failures.
# Backend: gpu-screen-recorder. Where it is not installed, or it exits before
# the recording starts (it knows only NVIDIA, AMD, Intel and Broadcom GPUs, and
# aborts on others such as Apple Silicon's), wf-recorder records instead when
# it is installed. wf-recorder encodes on the CPU and captures one audio
# device, so desktop and microphone audio together go through a temporary null
# sink. Portal capture and --resolution are gpu-screen-recorder only.
[[ -f ~/.config/user-dirs.dirs ]] && source ~/.config/user-dirs.dirs [[ -f ~/.config/user-dirs.dirs ]] && source ~/.config/user-dirs.dirs
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}" OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
if [[ ! -d $OUTPUT_DIR ]]; then
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR"
exit 1
fi
DESKTOP_AUDIO="false" DESKTOP_AUDIO="false"
MICROPHONE_AUDIO="false" MICROPHONE_AUDIO="false"
WEBCAM="false" WEBCAM="false"
@@ -36,7 +35,6 @@ WEBCAM_SIZE="medium"
RESOLUTION="" RESOLUTION=""
FULLSCREEN="false" FULLSCREEN="false"
STOP_RECORDING="false" STOP_RECORDING="false"
STATUS="false"
# Both of these live in the per-user runtime directory, which is 0700. What # Both of these live in the per-user runtime directory, which is 0700. What
# RECORDING_FILE holds is read back on stop and used as a path -- ffmpeg writes # RECORDING_FILE holds is read back on stop and used as a path -- ffmpeg writes
# beside it, `mv` replaces it, and `rm -f` deletes its preview -- so a name in # beside it, `mv` replaces it, and `rm -f` deletes its preview -- so a name in
@@ -48,17 +46,6 @@ STATUS="false"
RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}" RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename" RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename"
REGION_FILE="$RUNTIME_DIR/omarchy-screenrecord-region" REGION_FILE="$RUNTIME_DIR/omarchy-screenrecord-region"
# The recorder's pid, so stop and status act on that process alone.
REC_PID_FILE="$RUNTIME_DIR/omarchy-screenrecord-pid"
# The PulseAudio module ids of a combined-audio null sink, for cleanup.
PA_MODULES_FILE="$RUNTIME_DIR/omarchy-screenrecord-pa-modules"
# Present while a start holds the lock; another toggle or stop then leaves
# CANCEL_FILE for that start instead of queueing behind its picker or recorder.
STARTING_FILE="$RUNTIME_DIR/omarchy-screenrecord-starting"
CANCEL_FILE="$RUNTIME_DIR/omarchy-screenrecord-cancel"
# How long a recorder may take to create its file, except through the portal,
# whose picker waits on the user.
READY_TIMEOUT_TICKS=75
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "$RUNTIME_DIR/omarchy-screenrecord.log" || echo "/dev/null") LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "$RUNTIME_DIR/omarchy-screenrecord.log" || echo "/dev/null")
for arg in "$@"; do for arg in "$@"; do
@@ -71,7 +58,6 @@ for arg in "$@"; do
--resolution=*) RESOLUTION="${arg#*=}" ;; --resolution=*) RESOLUTION="${arg#*=}" ;;
--fullscreen) FULLSCREEN="true" ;; --fullscreen) FULLSCREEN="true" ;;
--stop-recording) STOP_RECORDING="true" ;; --stop-recording) STOP_RECORDING="true" ;;
--status) STATUS="true" ;;
esac esac
done done
@@ -83,13 +69,6 @@ small | medium | large) ;;
;; ;;
esac esac
# --status only answers whether our recording runs, for the bar and the menu, so
# it needs no recordings directory and never notifies.
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR"
exit 1
fi
start_webcam_overlay() { start_webcam_overlay() {
cleanup_webcam cleanup_webcam
@@ -120,7 +99,7 @@ start_webcam_overlay() {
'--vf=lavfi=[crop=ih*8/9:ih]' \ '--vf=lavfi=[crop=ih*8/9:ih]' \
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \ --title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
--no-border --no-audio --no-osc --osd-level=0 \ --no-border --no-audio --no-osc --osd-level=0 \
--really-quiet 9>&- &>/dev/null & --really-quiet &>/dev/null &
# The move has to settle before gpu-screen-recorder starts, or the camera is # The move has to settle before gpu-screen-recorder starts, or the camera is
# recorded sliding into its corner. Waiting for the map is what the blind # recorded sliding into its corner. Waiting for the map is what the blind
@@ -174,132 +153,12 @@ select_capture_target() {
echo "region:${BASH_REMATCH[3]}x${BASH_REMATCH[4]}+${BASH_REMATCH[1]}+${BASH_REMATCH[2]}" echo "region:${BASH_REMATCH[3]}x${BASH_REMATCH[4]}+${BASH_REMATCH[1]}+${BASH_REMATCH[2]}"
} }
# gpu-screen-recorder first, then wf-recorder, of those installed. The base
# packages carry gpu-screen-recorder, but an architecture may build none.
recorder_backends() {
local backend found=false
for backend in gpu-screen-recorder wf-recorder; do
if omarchy-cmd-present "$backend"; then
echo "$backend"
found=true
fi
done
# Neither installed: fail the way a missing gpu-screen-recorder always has.
$found || echo gpu-screen-recorder
}
# wf-recorder records a single audio device. Map the desktop/mic flags onto one:
# desktop -> default sink monitor, mic -> default source. When both are requested,
# route them through a throwaway null sink so a single track carries both; the
# module ids are saved to PA_MODULES_FILE so stop can unload them. A requested
# source that can't be recorded is left out with a notification naming it, and
# the recording carries whichever source remains.
wf_audio_device() {
local sink="" source="" m
[[ $DESKTOP_AUDIO == "true" ]] && sink=$(pactl get-default-sink 2>/dev/null)
[[ $MICROPHONE_AUDIO == "true" ]] && source=$(pactl get-default-source 2>/dev/null)
if [[ -n $sink && -n $source ]]; then
if m=$(pactl load-module module-null-sink sink_name=omarchy_screenrec sink_properties=device.description=omarchy_screenrec 2>/dev/null); then
echo "$m" >"$PA_MODULES_FILE"
if m=$(pactl load-module module-loopback source="${sink}.monitor" sink=omarchy_screenrec latency_msec=20 2>/dev/null); then
echo "$m" >>"$PA_MODULES_FILE"
else
sink=""
fi
if m=$(pactl load-module module-loopback source="$source" sink=omarchy_screenrec latency_msec=20 2>/dev/null); then
echo "$m" >>"$PA_MODULES_FILE"
else
source=""
fi
if [[ -n $sink && -n $source ]]; then
echo "omarchy_screenrec.monitor"
return
fi
wf_audio_cleanup
else
# No device to mix into: desktop audio records alone, as it always has.
source=""
fi
fi
[[ $DESKTOP_AUDIO == "true" && -z $sink ]] && wf_audio_unavailable desktop
[[ $MICROPHONE_AUDIO == "true" && -z $source ]] && wf_audio_unavailable microphone
if [[ -n $sink ]]; then
echo "${sink}.monitor"
elif [[ -n $source ]]; then
echo "$source"
fi
}
wf_audio_unavailable() {
omarchy-notification-send -u critical -t 5000 "Recording without $1 audio" "The $1 audio could not be added to this recording." >/dev/null 2>&1
}
wf_audio_cleanup() {
[[ -f $PA_MODULES_FILE ]] || return
# Unload in reverse so the loopbacks drop before the null sink they feed.
tac "$PA_MODULES_FILE" 2>/dev/null | while read -r m; do
[[ -n $m ]] && pactl unload-module "$m" 2>/dev/null
done
rm -f "$PA_MODULES_FILE"
}
# Launches wf-recorder for the given picker target, backgrounded, with REC_PID set.
launch_wf_recorder() {
local target=$1 filename=$2
local geom_args=()
case $target in
monitor:*)
geom_args=(-o "${target#monitor:}")
;;
region:*)
# region:WxH+X+Y (logical coords) -> wf-recorder -g "X,Y WxH" (same space).
[[ ${target#region:} =~ ^([0-9]+)x([0-9]+)\+(-?[0-9]+)\+(-?[0-9]+)$ ]] || return 1
geom_args=(-g "${BASH_REMATCH[3]},${BASH_REMATCH[4]} ${BASH_REMATCH[1]}x${BASH_REMATCH[2]}")
;;
*)
return 1
;;
esac
local audio_args=() dev
dev=$(wf_audio_device)
[[ -n $dev ]] && audio_args=(--audio="$dev")
# -r 60 caps the framerate: capturing a 120 Hz panel at its native rate makes
# needlessly large files. -R 48000 keeps the AAC track at 48 kHz rather than
# following a sink that advertises 96 kHz into a file many players, and some
# speaker DSPs, play as silence.
wf-recorder "${geom_args[@]}" -r 60 -R 48000 "${audio_args[@]}" -f "$filename" 9>&- 2>>"$LOG_FILE" &
REC_PID=$!
}
start_cancelled() {
[[ -e $CANCEL_FILE ]]
}
# Stops a recorder this start launched: TERM, two seconds to exit, then KILL.
end_launched_recorder() {
local pid=$1 waited=0 state
kill "$pid" 2>/dev/null
while (( waited++ < 20 )); do
state=$(<"/proc/$pid/stat") 2>/dev/null || break
state=${state##*) }
[[ ${state:0:1} != "Z" ]] || break
sleep 0.1
done
kill -KILL "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
}
start_screenrecording() { start_screenrecording() {
# A pid left by a recording that ended without a stop would otherwise hide mkdir -p "$RUNTIME_DIR" || return 1
# this one from the toggle until it has started and saved its own. # XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
rm -f "$REC_PID_FILE" if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
chmod 700 "$RUNTIME_DIR" || return 1
fi
local capture_args=() local capture_args=()
local target local target
@@ -315,7 +174,6 @@ start_screenrecording() {
capture_args=(-w portal -s "${RESOLUTION:-$(default_resolution)}") capture_args=(-w portal -s "${RESOLUTION:-$(default_resolution)}")
else else
target=$(select_capture_target) || return 1 target=$(select_capture_target) || return 1
start_cancelled && return 1
case $target in case $target in
monitor:*) monitor:*)
@@ -329,101 +187,50 @@ start_screenrecording() {
fi fi
[[ $WEBCAM == "true" ]] && start_webcam_overlay "$target" [[ $WEBCAM == "true" ]] && start_webcam_overlay "$target"
start_cancelled && return 1
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4" local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
local backend pid="" local audio_devices=""
local audio_args=()
for backend in $(recorder_backends); do [[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output"
[[ $backend == "wf-recorder" && $target == "portal" ]] && continue
echo "===== $(date '+%F %T') args: $* backend: $backend target: $target =====" >>"$LOG_FILE"
if [[ $backend == "wf-recorder" ]]; then if [[ $MICROPHONE_AUDIO == "true" ]]; then
launch_wf_recorder "$target" "$filename" || continue # Merge audio tracks into one - separate tracks only play one at a time in most players
pid=$REC_PID [[ -n $audio_devices ]] && audio_devices+="|"
else audio_devices+="default_input"
local audio_devices="" fi
local audio_args=()
[[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output" [[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac)
if [[ $MICROPHONE_AUDIO == "true" ]]; then echo "===== $(date '+%F %T') args: $* target: $target =====" >>"$LOG_FILE"
# Merge audio tracks into one - separate tracks only play one at a time in most players gpu-screen-recorder "${capture_args[@]}" -k auto -f 60 -fm cfr -fallback-cpu-encoding yes -o "$filename" "${audio_args[@]}" 2>>"$LOG_FILE" &
[[ -n $audio_devices ]] && audio_devices+="|" local pid=$!
audio_devices+="default_input"
fi
[[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac) while kill -0 $pid 2>/dev/null && [[ ! -f $filename ]]; do
sleep 0.2
gpu-screen-recorder "${capture_args[@]}" -k auto -f 60 -fm cfr -fallback-cpu-encoding yes -o "$filename" "${audio_args[@]}" 9>&- 2>>"$LOG_FILE" &
pid=$!
fi
local ticks=0
while kill -0 $pid 2>/dev/null && [[ ! -f $filename ]]; do
start_cancelled && break
if [[ $target != "portal" ]] && (( ++ticks > READY_TIMEOUT_TICKS )); then
end_launched_recorder "$pid"
wf_audio_cleanup
rm -f "$filename"
continue 2
fi
sleep 0.2
done
if start_cancelled; then
end_launched_recorder "$pid"
wf_audio_cleanup
rm -f "$filename"
return 1
fi
if kill -0 $pid 2>/dev/null; then
echo "$pid" >"$REC_PID_FILE"
echo "$filename" >"$RECORDING_FILE"
toggle_screenrecording_indicator
return 0
fi
wf_audio_cleanup
rm -f "$filename"
done done
return 1 if kill -0 $pid 2>/dev/null; then
echo "$filename" >"$RECORDING_FILE"
toggle_screenrecording_indicator
fi
} }
# Selects the recorder this script started, by the pid it saved. A saved pid
# that is no longer a recorder is a recording that ended without a stop, so
# nothing of ours is recording and no other recorder is touched. Without a
# saved pid (a recording started before the pid was saved), every recorder the
# user runs counts, as for the bar indicator and the menu.
select_recorder() {
RECORDER=()
[[ -f $REC_PID_FILE ]] || return 0
local pid
pid=$(<"$REC_PID_FILE")
[[ $pid =~ ^[0-9]+$ ]] && omarchy-capture-screenrecording-process --pid "$pid" || return 1
RECORDER=(--pid "$pid")
}
# Stops the recorder screenrecording_active selected.
stop_screenrecording() { stop_screenrecording() {
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal INT # SIGINT required to save video properly pkill -SIGINT -f "^gpu-screen-recorder" # SIGINT required to save video properly
# Wait a maximum of 5 seconds to finish before hard killing # Wait a maximum of 5 seconds to finish before hard killing
local count=0 local count=0
while omarchy-capture-screenrecording-process "${RECORDER[@]}" && ((count < 50)); do while pgrep -f "^gpu-screen-recorder" >/dev/null && ((count < 50)); do
sleep 0.1 sleep 0.1
count=$((count + 1)) count=$((count + 1))
done done
toggle_screenrecording_indicator toggle_screenrecording_indicator
cleanup_webcam cleanup_webcam
wf_audio_cleanup
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then if pgrep -f "^gpu-screen-recorder" >/dev/null; then
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL pkill -9 -f "^gpu-screen-recorder"
omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted." omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted."
else else
finalize_recording finalize_recording
@@ -444,10 +251,10 @@ stop_screenrecording() {
( (
sleep 2 sleep 2
rm -f "$preview" rm -f "$preview"
) 9>&- & ) &
fi fi
rm -f "$RECORDING_FILE" "$REC_PID_FILE" rm -f "$RECORDING_FILE"
} }
toggle_screenrecording_indicator() { toggle_screenrecording_indicator() {
@@ -455,14 +262,7 @@ toggle_screenrecording_indicator() {
} }
screenrecording_active() { screenrecording_active() {
if select_recorder && omarchy-capture-screenrecording-process "${RECORDER[@]}"; then pgrep -f "^gpu-screen-recorder" >/dev/null
return 0
else
if [[ -f $REC_PID_FILE ]]; then
wf_audio_cleanup
fi
return 1
fi
} }
finalize_recording() { finalize_recording() {
@@ -483,10 +283,7 @@ finalize_recording() {
# Hard-mute the first 400ms to drop the PipeWire capture-open pop (a near-clipping # Hard-mute the first 400ms to drop the PipeWire capture-open pop (a near-clipping
# transient around 130-200ms that a gentle fade-in can't attenuate enough), then a # transient around 130-200ms that a gentle fade-in can't attenuate enough), then a
# 50ms fade avoids a click at the boundary before loudnorm normalizes the rest. # 50ms fade avoids a click at the boundary before loudnorm normalizes the rest.
# args+=(-af "volume=enable='lt(t,0.4)':volume=0,afade=t=in:st=0.4:d=0.05,loudnorm=I=-14:TP=-1.5:LRA=11")
# loudnorm declares 192 kHz output, so without -ar the AAC encoder writes
# 96 kHz from a 48 kHz recording.
args+=(-ar 48000 -af "volume=enable='lt(t,0.4)':volume=0,afade=t=in:st=0.4:d=0.05,loudnorm=I=-14:TP=-1.5:LRA=11")
fi fi
local processed="${latest%.mp4}-processed.mp4" local processed="${latest%.mp4}-processed.mp4"
@@ -497,66 +294,10 @@ finalize_recording() {
fi fi
} }
mkdir -p "$RUNTIME_DIR" || exit 1 if screenrecording_active; then
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too. stop_screenrecording
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then elif [[ $STOP_RECORDING == "true" ]]; then
chmod 700 "$RUNTIME_DIR" || exit 1 exit 1
else
start_screenrecording || cleanup_webcam
fi fi
# Serialize changes to the recorder and its files; background children close 9.
# A busy status query stays read-only so a picker cannot stall the bar.
{
if [[ $STATUS == "true" ]]; then
if flock -n 9; then
screenrecording_active
else
select_recorder && omarchy-capture-screenrecording-process "${RECORDER[@]}"
fi
else
# A start still in its picker or waiting on its recorder is cancelled, not
# queued behind: that toggle would otherwise begin another recording.
# The start reads CANCEL_FILE only after it removes STARTING_FILE, so a
# request left while STARTING_FILE stands is always seen. One that finds it
# gone waits its turn, and a request the start used up by then is done.
if ! flock -w 1 9; then
touch "$CANCEL_FILE"
# A start that was killed leaves its marker: it counts only while that
# start still runs.
starter=""
[[ -r $STARTING_FILE ]] && starter=$(<"$STARTING_FILE")
if [[ $starter =~ ^[0-9]+$ ]] && kill -0 "$starter" 2>/dev/null; then
exit 0
fi
flock 9 || exit 1
[[ -e $CANCEL_FILE ]] || exit 0
rm -f "$CANCEL_FILE"
fi
if screenrecording_active; then
stop_screenrecording
elif [[ $STOP_RECORDING == "true" ]]; then
exit 1
else
rm -f "$CANCEL_FILE"
echo $$ >"$STARTING_FILE"
if start_screenrecording; then
rm -f "$STARTING_FILE"
# Cancelled while the recording was being saved as started.
if start_cancelled; then
end_launched_recorder "$(<"$REC_PID_FILE")"
wf_audio_cleanup
cleanup_webcam
rm -f "$(<"$RECORDING_FILE")" "$REC_PID_FILE" "$RECORDING_FILE"
toggle_screenrecording_indicator
rm -f "$CANCEL_FILE"
fi
# A request after that check is left for its requester, who stops this
# recording once it has the lock.
else
rm -f "$STARTING_FILE"
wf_audio_cleanup
cleanup_webcam
rm -f "$CANCEL_FILE"
fi
fi
fi
} 9>>"$RUNTIME_DIR/omarchy-screenrecord.lock"
@@ -1,72 +0,0 @@
#!/bin/bash
# omarchy:summary=Check or signal the current user's recording processes
# omarchy:args=[--pid <pid>] [--signal INT|KILL]
# omarchy:hidden=true
usage() {
echo "Usage: omarchy-capture-screenrecording-process [--pid <pid>]... [--signal INT|KILL]" >&2
exit 2
}
proc_root=${OMARCHY_PROC_ROOT:-/proc}
wanted=" "
signal=""
while (($#)); do
case $1 in
--pid)
# Compare textually first: bash arithmetic wraps values past 64 bits.
[[ ${2-} =~ ^0*([0-9]+)$ ]] || usage
pid=${BASH_REMATCH[1]}
((${#pid} <= 10 && 10#$pid <= 4194304)) || usage
wanted+="$((10#$pid)) "
shift 2
;;
--signal)
[[ ${2-} == "INT" || ${2-} == "KILL" ]] || usage
signal=$2
shift 2
;;
*)
usage
;;
esac
done
is_recorder() {
local exe
exe=$(readlink "$proc_root/$1/exe" 2>/dev/null) || return 1
# Linux appends this when an upgrade unlinks a still-running executable.
exe=${exe% (deleted)}
exe=${exe##*/}
[[ $exe == "gpu-screen-recorder" || $exe == "wf-recorder" ]]
}
# Linux truncates a process name to 15 characters: gpu-screen-recorder runs as
# gpu-screen-reco, which any executable sharing that prefix also matches, and a
# process can rename itself, so the executable path confirms each candidate.
matched=()
while read -r pid; do
if [[ $wanted == " " || $wanted == *" $pid "* ]] && is_recorder "$pid"; then
matched+=("$pid")
fi
done < <(pgrep -u "$UID" -x 'gpu-screen-reco(rder)?|wf-recorder')
if ((${#matched[@]} == 0)); then
exit 1
fi
if [[ -n $signal ]]; then
delivered=0
for pid in "${matched[@]}"; do
# The pid can be reused between discovery and kill; re-checking exe here narrows that window.
if is_recorder "$pid" && kill "-$signal" "$pid" 2>/dev/null; then
delivered=1
fi
done
# A recorder that exits before its signal has stopped anyway; fail only if none was signalled.
((delivered)) || exit 1
fi
exit 0
+25 -31
View File
@@ -47,9 +47,31 @@ validate_dev_checkout() {
fi fi
} }
# The dev channel refreshes pacman with the checkout's own tools. A checkout
# without this machine's region profile would silently drop its repositories.
validate_dev_region() {
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
[[ -f $region_file ]] && region=$(<"$region_file")
[[ $region == "global" ]] && return 0
# The refresh after linking needs a runnable helper and every fragment, not
# just a directory with the region's name: dev refreshes edge, and leaving
# dev later refreshes stable or rc with this checkout's tools.
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
[[ -r $profile/$fragment ]] || supported=false
done
if ! $supported; then
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
fi
}
link_dev_checkout() { link_dev_checkout() {
local checkout="$1" required local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout" [[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
validate_dev_region "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path. # Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy. # An existing checkout is not pulled automatically and may predate this policy.
@@ -60,23 +82,6 @@ link_dev_checkout() {
fi fi
done done
# The checkout's own channel refresh runs next, from its own templates.
if [[ ! -f $checkout/${templates#"$OMARCHY_PATH"/}/pacman-$pacman_channel.conf ||
! -f $checkout/${templates#"$OMARCHY_PATH"/}/mirrorlist-$pacman_channel ]]; then
fail "Update the checkout before switching to dev; it has no $pacman_channel templates for $platform."
fi
# On aarch64 one from before it had platform templates would replace this
# machine's repositories with x86's.
if [[ $platform != "x86" ]]; then
for required in bin/omarchy-hw-platform install/helpers/pacman.sh; do
if [[ ! -f $checkout/$required || ! -r $checkout/$required ||
( $required == "bin/omarchy-hw-platform" && ! -x $checkout/$required ) ]]; then
fail "Update the checkout before switching to dev; on $platform it needs $required for this machine's repositories."
fi
done
fi
omarchy-dev-link "$checkout" --no-reboot omarchy-dev-link "$checkout" --no-reboot
} }
@@ -119,6 +124,9 @@ case "$channel" in
packages=(omarchy-dev omarchy-settings-dev) packages=(omarchy-dev omarchy-settings-dev)
;; ;;
dev) dev)
confirm_dev || { echo "Cancelled."; exit 0; }
dev_checkout="$HOME/omarchy"
validate_dev_checkout "$dev_checkout"
pacman_channel=edge pacman_channel=edge
packages=(omarchy-dev omarchy-settings-dev) packages=(omarchy-dev omarchy-settings-dev)
;; ;;
@@ -129,20 +137,6 @@ case "$channel" in
;; ;;
esac esac
# A channel this machine's platform has no templates for is refused before
# anything changes (install/helpers/pacman.sh).
source "$OMARCHY_PATH/install/helpers/pacman.sh"
platform=$(omarchy-hw-platform) || fail "Cannot tell this machine's platform; nothing changed."
templates=$(omarchy_pacman_templates "$platform") || exit 1
[[ -f $templates/pacman-$pacman_channel.conf && -f $templates/mirrorlist-$pacman_channel ]] ||
fail "Omarchy has no $pacman_channel channel for $platform; nothing changed."
if [[ $channel == "dev" ]]; then
confirm_dev || { echo "Cancelled."; exit 0; }
dev_checkout="$HOME/omarchy"
validate_dev_checkout "$dev_checkout"
fi
# A failure past this point leaves the channel switch half-applied, so say how # A failure past this point leaves the channel switch half-applied, so say how
# to pick it back up rather than dying silently under set -e. # to pick it back up rather than dying silently under set -e.
trap 'echo -e "\nThe channel switch did not complete. Review the error above, then rerun: omarchy-channel-set '"$channel"'" >&2' ERR trap 'echo -e "\nThe channel switch did not complete. Review the error above, then rerun: omarchy-channel-set '"$channel"'" >&2' ERR
-37
View File
@@ -1,37 +0,0 @@
#!/bin/bash
# omarchy:summary=Get or set the default dictation backend
# omarchy:args=[backend]
# omarchy:examples=omarchy default dictation | omarchy default dictation superwhisper
if (($# > 1)); then
echo "Usage: omarchy default dictation [backend]" >&2
exit 1
fi
if (($# == 0)); then
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
if [[ ! -s $config ]]; then
echo "No dictation backend configured. Select one in Setup > Defaults > Dictation." >&2
exit 1
fi
read -r backend < "$config"
else
backend=$1
fi
if [[ ! $backend =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "Invalid dictation backend: $backend" >&2
exit 1
fi
if (($# == 0)); then
echo "$backend"
else
installer="omarchy-install-dictation-$backend"
if omarchy-cmd-missing "$installer"; then
echo "No installer available for dictation backend: $backend" >&2
exit 1
fi
omarchy-launch-floating-terminal-with-presentation "$installer"
fi
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
# omarchy:summary=Record dictation with the configured backend
# omarchy:args=<start|stop|toggle>
# omarchy:examples=omarchy dictation start | omarchy dictation toggle
if (($# != 1)) || [[ $1 != "start" && $1 != "stop" && $1 != "toggle" ]]; then
echo "Usage: omarchy dictation <start|stop|toggle>" >&2
exit 1
fi
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
mkdir -p "${config%/*}"
exec 9> "$config.lock"
if ! flock -w 5 9; then
echo "Dictation is busy; try again." >&2
exit 1
fi
backend=$(omarchy-default-dictation) || exit 1
adapter="omarchy-dictation-$backend"
if omarchy-cmd-missing "$adapter"; then
echo "The selected dictation backend ($backend) is not installed." >&2
exit 1
fi
"$adapter" "$1" 9>&-
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# omarchy:summary=Record dictation using Superwhisper
# omarchy:args=<start|stop|toggle>
case "${1:-}" in
start) superwhisper start ;;
stop)
if omarchy-cmd-missing superwhisper || ! systemctl --user is-active --quiet superwhisper.service; then
exit 0
else
superwhisper stop
fi
;;
toggle) superwhisper record ;;
*) echo "Usage: omarchy dictation superwhisper <start|stop|toggle>" >&2; exit 1 ;;
esac
-33
View File
@@ -1,33 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Save a configured dictation backend after stopping the previous backend
# omarchy:args=<backend>
set -e
if (($# != 1)) || [[ ! $1 =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "Usage: omarchy-dictation-use <backend>" >&2
exit 1
fi
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
mkdir -p "${config%/*}"
exec 9> "$config.lock"
if ! flock -w 5 9; then
echo "Dictation is busy; try again." >&2
exit 1
fi
previous=$(omarchy-default-dictation 2>/dev/null) || previous=""
if [[ -n $previous && $previous != "$1" ]] && omarchy-cmd-present "omarchy-dictation-$previous"; then
if ! "omarchy-dictation-$previous" stop 9>&-; then
echo "Could not stop $previous; keeping the existing dictation backend." >&2
exit 1
fi
fi
temporary=$(mktemp "$config.XXXXXX")
trap 'rm -f "$temporary"' EXIT
printf '%s\n' "$1" > "$temporary"
mv -f "$temporary" "$config"
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# omarchy:summary=Record dictation using Voxtype
# omarchy:args=<start|stop|toggle>
case "${1:-}" in
start|toggle) voxtype record "$1" ;;
stop)
if omarchy-cmd-missing voxtype || [[ $(voxtype status) == "stopped" ]]; then
exit 0
else
voxtype record stop
fi
;;
*) echo "Usage: omarchy dictation voxtype <start|stop|toggle>" >&2; exit 1 ;;
esac
+19 -419
View File
@@ -1,432 +1,32 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Set a new password for an encrypted drive # omarchy:summary=Set a new encryption password for a drive selected.
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
# Changes a drive's LUKS key and nothing else. For the disk holding /, the new encrypted_drives=$(blkid -t TYPE=crypto_LUKS -o device)
# key must open the disk and the old one must not before the change counts. The
# journal records only the disk's UUID, key slots and phase, so a change that
# stops part way is finished by the next run. A platform whose boot checks prove
# the disk's key slots records the owner's new one before the run finishes.
# Passwords live in variables; never trace them.
set +x
REKEY_STATE=${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/drive-password.state
source "$OMARCHY_PATH/install/provisioning/luks-rekey.sh"
# The LUKS partition beneath /, or nothing when / is not encrypted. Fails when
# it cannot tell, including a crypt layer whose LUKS partition lsblk cannot name.
root_luks_device() {
local root_source ancestry device
root_source=$(findmnt -no SOURCE /) && [[ -n $root_source ]] || return 1
ancestry=$(lsblk -nsrpo NAME,TYPE,FSTYPE "${root_source%%[*}") || return 1
device=$(awk '$3 == "crypto_LUKS" { print $1; exit }' <<<"$ancestry")
if [[ -n $device ]]; then
readlink -f "$device"
elif awk '$2 == "crypt" { found = 1 } END { exit !found }' <<<"$ancestry"; then
return 1
fi
}
# Every LUKS device, from udev's probe as root_luks_device reads it. An
# unprivileged blkid only reads /run/blkid, empty until root runs blkid. lsblk
# repeats a device under each parent (RAID members), so each is listed once.
luks_drives() {
lsblk -nrpo NAME,FSTYPE | awk '$2 == "crypto_LUKS" && !seen[$1]++ { print $1 }'
}
# Every cryptsetup call holds the disk lock, so one left running by a run that
# was killed finishes before the next run looks at the drive.
# Print the key slot the password opens. Returns 2 when it opens none, and
# another failure when the drive cannot be checked. cryptsetup open tries
# enrolled tokens (TPM2, FIDO2, keyring) before the key and reports a token's
# slot whatever key it was given, so allow only a token type no token has.
drive_key_slot() {
local drive=$1 password=$2 out status=0
out=$(printf '%s' "$password" | sudo flock "$disk_lock" env LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file - "$drive" 2>&1) || status=$?
(( status == 0 )) || return "$status"
grep -o 'Key slot [0-9]* unlocked' <<<"$out" | grep -o '[0-9]*' | head -1
}
# The enabled key slots, comma-separated. A luks2-keyring token under "Tokens:"
# looks like a keyslot, so read only the keyslot section.
drive_slots() {
local dump
dump=$(sudo flock "$disk_lock" cryptsetup luksDump "$1") || return 1
awk '
/^[^ \t]/ { keyslots = ($0 == "Keyslots:") }
keyslots && /^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }
/^Key Slot [0-9]+: ENABLED/ { sub(":", "", $3); print $3 }' <<<"$dump" | paste -sd, -
}
# The key slots other than FOUND that the password also opens, comma-separated,
# or nothing. Fails when a slot cannot be checked.
other_key_slots() {
local drive=$1 password=$2 found=$3 slots slot status
local -a others=()
slots=$(drive_slots "$drive") || return 1
for slot in ${slots//,/ }; do
[[ $slot != "$found" ]] || continue
status=0
printf '%s' "$password" | sudo flock "$disk_lock" cryptsetup open --test-passphrase --token-type passphrase-only --key-slot "$slot" --key-file - "$drive" >/dev/null 2>&1 || status=$?
if (( status == 0 )); then
others+=("$slot")
elif (( status != 2 )); then
return 1
fi
done
local IFS=,
printf '%s' "${others[*]}"
}
kill_slot() {
local drive=$1 password=$2 slot=$3
printf '%s' "$password" | sudo flock "$disk_lock" cryptsetup luksKillSlot -q --key-file - "$drive" "$slot"
}
# luksChangeKey writes the new key before it retires the old one (LUKS2 swaps
# them in one header update), so a failure that leaves the new key unable to
# open the drive changed nothing: that returns 2. One that stops in between
# leaves both keys, so the old key is retired here until it opens nothing.
# Sets new_slot.
change_luks_key() {
local drive=$1 current=$2 new=$3 old_slot=$4 remains attempt status
local -a pbkdf=(--iter-time 2000)
echo "Changing full-disk encryption password for $drive"
if sudo flock "$disk_lock" cryptsetup isLuks --type luks2 "$drive"; then
pbkdf+=(--pbkdf argon2id)
fi
# Both keys travel over stdin, the current one on the first line.
printf '%s\n%s' "$current" "$new" |
sudo flock "$disk_lock" bash -c 'IFS= read -r current; drive=$1; shift; exec cryptsetup luksChangeKey "$@" --key-file <(printf "%s" "$current") "$drive" <(cat)' bash "$drive" "${pbkdf[@]}"
new_slot=$(drive_key_slot "$drive" "$new") || return
for (( attempt = 0; attempt < 8; attempt++ )); do
status=0
remains=$(drive_key_slot "$drive" "$current") || status=$?
(( status == 2 )) && return 0
(( status == 0 )) && [[ $remains != "$new_slot" ]] || return 1
kill_slot "$drive" "$new" "$remains" || return 1
done
return 1
}
# The entrypoint that records the system disk's key slots for the platform's
# boot checks, or nothing where none does. A machine without its platform's
# boot package at all (dispatch exit 3) predates it, and nothing checks its
# slots. Fails with the dispatcher's message otherwise, such as a boot package
# too old to ship luks-slots.
slot_recorder() {
local out status=0
out=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve luks-slots 2>&1) || status=$?
if (( status == 0 )); then
printf '%s' "$out"
elif (( status != 3 )); then
printf '%s\n' "$out" >&2
return 1
fi
}
# Refuse a current password that opens any slot but the owner's where the
# platform records the slots: on a disk an earlier setup gave a second key (a
# recovery key), changing that one would leave the owner's password as it was.
# luks-slots --owner prints the recorded slot (0), or exits 4 when no owner slot
# is recorded, as on a Mac set up before its boot package recorded slots; any
# other status is a failure the entrypoint explains, and stops the change.
# Where nothing is recorded, a password that opens the disk's only key slot is
# the owner's, and the change records its new slot (unrecorded_owner).
owner_slot_matches() {
local slot=$1 recorder owner errors reason status=0
recorder=$(slot_recorder) || return 1
[[ -n $recorder ]] || return 0
errors=$(mktemp) || return 1
owner=$(sudo "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots --owner 2>"$errors") || status=$?
reason=$(<"$errors")
rm -f "$errors"
if (( status == 4 )); then
only_key_slot "$slot" || return 1
unrecorded_owner=1
return 0
elif (( status != 0 )); then
echo "Could not read which key slot holds the system disk password: ${reason:-luks-slots --owner exited $status}"
return 1
elif [[ ! $owner =~ ^[0-9]+$ ]]; then
echo "Could not read which key slot holds the system disk password: the platform's boot package named '$owner', which is not a key slot."
return 1
fi
if [[ $owner != "$slot" ]]; then
echo "That password opens another key slot on the system disk, such as a recovery key from an earlier setup. Enter the system disk password."
return 1
fi
}
# With no owner slot recorded, the password must open the system disk's one key
# slot: with more keys there is no telling which is the owner's, and recording
# only the owner's would fail the platform's boot check, which wants every kept
# slot recorded. Says how to settle it.
only_key_slot() {
local slot=$1 slots others
slots=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
[[ $slots != "$slot" ]] || return 0
others=$(tr ',' '\n' <<<"$slots" | grep -vx -- "$slot" | paste -sd, -)
echo "No key slot is recorded as the system disk password's, and $root_drive has keys in slots $slots, so which one is yours can't be told. The password you entered opens slot $slot."
if [[ $others =~ ^[0-9]+$ ]]; then
echo "If that password is yours and slot $others holds a recovery key you keep, record both: sudo omarchy-lifecycle-dispatch luks-slots owner=$slot recovery=$others"
echo "Otherwise remove the key you don't use: sudo cryptsetup luksKillSlot $root_drive <slot>"
else
echo "Remove the keys you don't use: sudo cryptsetup luksKillSlot $root_drive <slot>"
fi
echo "Then run omarchy-drive-password again."
return 1
}
# Record the slot the owner's password opens now; a no-op where nothing does.
record_owner_slot() {
local recorder
recorder=$(slot_recorder) || return 1
[[ -z $recorder ]] || sudo "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots "owner=$1"
}
# Phase luks: the key change may or may not have landed. Phase record: the new
# key alone opens the recorded slot, which the boot checks still need.
change_system_password() {
local current=$1 new=$2 old_slot=$3 slots status=0
slots=$(drive_slots "$root_drive")
if [[ ,$slots, != *",$old_slot,"* ]]; then
echo "Could not read the key slots of $root_drive."
return 1
fi
# A disk taken as the owner's for holding one key must still hold only it.
if (( unrecorded_owner )) && [[ $slots != "$old_slot" ]]; then
echo "The key slots of $root_drive changed while you entered the new password. Run omarchy-drive-password again."
return 1
fi
mkdir -p "${REKEY_STATE%/*}" &&
rekey_state_put uuid "$root_uuid" old_slot "$old_slot" slots "$slots" checked 1 phase luks || return 1
change_luks_key "$root_drive" "$current" "$new" "$old_slot" || status=$?
if (( status == 2 )); then
rm -f "$REKEY_STATE"
echo "The disk password did not change."
return 1
elif (( status != 0 )); then
echo "Could not confirm the disk password change. Run omarchy-drive-password again to finish."
return 1
fi
rekey_state_put slot "$new_slot" phase record || return 1
if ! record_owner_slot "$new_slot"; then
echo "Could not record the new key slot for the boot checks. Run omarchy-drive-password again to finish."
return 1
fi
rm -f "$REKEY_STATE"
}
# A journal left on this system's disk by a change that stopped part way.
change_pending() {
local phase uuid
phase=$(rekey_state_get phase) || return 1
uuid=$(rekey_state_get uuid)
if [[ $phase == "luks" || $phase == "record" || $phase == "accounts" ]] && [[ -n $uuid && $uuid == "$root_uuid" ]]; then
return 0
fi
rm -f "$REKEY_STATE"
return 1
}
# Settle the disk on the password it opens with now, then record its slot.
# Before the change was confirmed that is the old key (removing the slot the
# change added, if any) or the new key in the one slot the change added
# (retiring the old slot); after, only the confirmed slot. Any other key on the
# disk, such as a recovery passphrase, is refused, and so is a disk that gained
# more than one slot since.
finish_interrupted_change() {
local phase old_slot slots slot header password opened now added extra count=0 status=0
phase=$(rekey_state_get phase)
# Earlier versions named the confirmed phase "accounts".
[[ $phase != "accounts" ]] || phase=record
old_slot=$(rekey_state_get old_slot)
slots=$(rekey_state_get slots)
slot=$(rekey_state_get slot)
if [[ $phase == "luks" && ! ( $old_slot =~ ^[0-9]+$ && ,$slots, == *",$old_slot,"* ) ]] ||
[[ $phase == "record" && ! $slot =~ ^[0-9]+$ ]]; then
echo "$REKEY_STATE is unreadable. Remove it once the disk opens with only the password you want."
return 1
fi
# Earlier versions journaled the change without checking that the old
# password opened one slot alone (checked=1), so with other slots on the disk
# a copy of it may have outlived the change. No password entered now can show
# which slot was whose, so the owner settles that by hand.
if [[ -z $(rekey_state_get checked) ]]; then
now=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
if [[ $now == *,* ]]; then
echo "The last password change for $root_drive was started by an earlier version of omarchy-drive-password, and the disk has key slots $now, so it may still hold a copy of the old password."
echo "Settle it by hand, in this order:"
echo "1. Find the slot the password you want to keep opens, and keep it: sudo cryptsetup open --test-passphrase --token-type passphrase-only --verbose $root_drive names it. If only the old password opens the disk, that is the one you keep."
echo "2. If the password you keep is not the old one, check each other slot with the old password (sudo cryptsetup open --test-passphrase --token-type passphrase-only --key-slot <slot> $root_drive) and remove each one it opens (sudo cryptsetup luksKillSlot $root_drive <slot>). Never remove the slot from step 1."
if [[ -n $(slot_recorder 2>/dev/null) ]]; then
echo "3. Record the slots you kept for the boot checks: sudo omarchy-lifecycle-dispatch luks-slots owner=<slot> recovery=<slot>, with an empty recovery= when no recovery key is left."
fi
echo "Then remove $REKEY_STATE, and run omarchy-drive-password again if the disk should still take a new password."
return 1
fi
fi
echo "The last password change for $root_drive stopped part way."
if [[ $phase == "record" ]]; then
header="New encryption password"
else
header="Encryption password that opens $root_drive now (the new one if the change went through)"
fi
password=$(gum input --password --header "$header") || return 1
opened=$(drive_key_slot "$root_drive" "$password") || status=$?
if (( status != 0 && status != 2 )); then
echo "Could not check the password against $root_drive."
return 1
fi
if [[ $phase == "luks" && -n $opened ]]; then
now=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
for extra in ${now//,/ }; do
if [[ ,$slots, != *",$extra,"* ]]; then
added=$extra
count=$(( count + 1 ))
fi
done
if (( count > 1 )); then
echo "The key slots of $root_drive changed since the interrupted change. Finish it by hand."
return 1
fi
if [[ $opened == "$old_slot" ]]; then
[[ -z $added ]] || kill_slot "$root_drive" "$password" "$added" || return 1
elif [[ -n $added && $opened == "$added" ]]; then
if [[ ,$now, == *",$old_slot,"* ]]; then
kill_slot "$root_drive" "$password" "$old_slot" || return 1
fi
else
opened=""
fi
[[ -z $opened ]] || rekey_state_put slot "$opened" phase record || return 1
elif [[ $phase == "record" && $opened != "$slot" ]]; then
opened=""
fi
if [[ -z $opened ]]; then
echo "That password does not finish the change on $root_drive. Run omarchy-drive-password again to finish."
return 1
fi
if ! record_owner_slot "$opened"; then
echo "Could not record the key slot for the boot checks. Run omarchy-drive-password again to finish."
return 1
fi
rm -f "$REKEY_STATE"
echo "$root_drive now opens with the password you entered, and the change is settled."
}
change_selected_drive() {
local encrypted_drives drive current old_slot others new_password confirmation status=0 system_drive=false
encrypted_drives=$(luks_drives)
if [[ -z $encrypted_drives ]]; then
echo "No encrypted drives available."
return 1
fi
if [[ -n $encrypted_drives ]]; then
if (( $(wc -l <<<"$encrypted_drives") == 1 )); then if (( $(wc -l <<<"$encrypted_drives") == 1 )); then
drive="$encrypted_drives" drive_to_change="$encrypted_drives"
else else
drive="$(omarchy-drive-select "$encrypted_drives")" drive_to_change="$(omarchy-drive-select "$encrypted_drives")"
fi fi
if [[ -z $drive ]]; then if [[ -n $drive_to_change ]]; then
new_password=$(gum input --password --header "New encryption password") || exit 1
[[ -n $new_password ]] || { echo "Password cannot be empty."; exit 1; }
confirmation=$(gum input --password --header "Confirm new encryption password") || exit 1
[[ $new_password == "$confirmation" ]] || { echo "Passwords do not match."; exit 1; }
echo "Changing full-disk encryption password for $drive_to_change"
# The new key travels over stdin and reaches cryptsetup as a keyfile via
# <(cat), leaving the tty free for the current-passphrase prompt.
printf "%s" "$new_password" | sudo bash -c 'exec cryptsetup luksChangeKey --pbkdf argon2id --iter-time 2000 "$1" <(cat) </dev/tty' bash "$drive_to_change"
else
echo "No drive selected." echo "No drive selected."
return 0
fi fi
if [[ -n $root_drive && $(readlink -f "$drive") == "$root_drive" ]]; then
system_drive=true
drive=$root_drive
fi
current=$(gum input --password --header "Current encryption password") || return 1
old_slot=$(drive_key_slot "$drive" "$current") || status=$?
if (( status == 2 )); then
echo "That password does not open $drive."
return 1
elif (( status != 0 )); then
echo "Could not check the password against $drive."
return 1
fi
if [[ $system_drive == "true" ]]; then
owner_slot_matches "$old_slot" || { echo "The system disk password did not change."; return 1; }
# An interrupted change is finished from the one slot the password opened;
# another copy of it would outlive the change.
others=$(other_key_slots "$drive" "$current" "$old_slot") || { echo "Could not check the key slots of $drive."; return 1; }
if [[ -n $others ]]; then
echo "That password also opens key slot $others on $drive. Remove the extra copy first (sudo cryptsetup luksKillSlot $drive <slot>), then run omarchy-drive-password again."
if [[ -n $(slot_recorder 2>/dev/null) ]]; then
echo "Then record the slots you keep for the boot checks: sudo omarchy-lifecycle-dispatch luks-slots owner=<slot> recovery=<slot>, with an empty recovery= when no recovery key is left."
fi
echo "The system disk password did not change."
return 1
fi
fi
new_password=$(gum input --password --header "New encryption password") || return 1
[[ -n $new_password ]] || { echo "Password cannot be empty."; return 1; }
confirmation=$(gum input --password --header "Confirm new encryption password") || return 1
[[ $new_password == "$confirmation" ]] || { echo "Passwords do not match."; return 1; }
[[ $new_password != "$current" ]] || { echo "The new password is the current one."; return 1; }
# Its slot is how the change is followed, so it must not open one already.
status=0
drive_key_slot "$drive" "$new_password" >/dev/null || status=$?
if (( status == 0 )); then
echo "That password already opens a key slot on $drive. Choose another."
return 1
elif (( status != 2 )); then
echo "Could not check the password against $drive."
return 1
fi
status=0
if [[ $system_drive == "true" ]]; then
change_system_password "$current" "$new_password" "$old_slot"
else
change_luks_key "$drive" "$current" "$new_password" "$old_slot" || status=$?
if (( status == 2 )); then
echo "The password of $drive did not change."
return 1
elif (( status != 0 )); then
echo "Could not confirm the password change on $drive."
return 1
fi
fi
}
disk_lock=${REKEY_STATE%/*}/drive-password.disk.lock
mkdir -p "${REKEY_STATE%/*}" && touch "$disk_lock" && exec {lock_fd}>"${REKEY_STATE%/*}/drive-password.lock" && flock -n "$lock_fd" ||
{ echo "Another drive password change is running."; exit 1; }
unrecorded_owner=0
root_drive=$(root_luks_device) || { echo "Could not tell which drive holds the system."; exit 1; }
root_uuid=""
if [[ -n $root_drive ]]; then
root_uuid=$(lsblk -dno UUID "$root_drive") && [[ -n $root_uuid ]] || { echo "Could not read the UUID of $root_drive."; exit 1; }
fi
if change_pending; then
finish_interrupted_change
else else
change_selected_drive echo "No encrypted drives available."
exit 1
fi fi
-6
View File
@@ -13,12 +13,6 @@ for arg in "$@"; do
esac esac
done done
# Resume is Limine-oriented and hibernate is unreliable on Asahi.
if omarchy-hw-aarch64-apple; then
echo "Skipping hibernation setup (not supported on Apple Silicon)"
exit 0
fi
if [[ ! -f /sys/power/image_size ]]; then if [[ ! -f /sys/power/image_size ]]; then
echo -e "Hibernation is not supported on your system" >&2 echo -e "Hibernation is not supported on your system" >&2
exit 0 exit 0
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# omarchy:summary=Detect whether the CPU is aarch64 (every ARM machine, Apple Silicon included).
[[ $(uname -m) == "aarch64" ]]
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Detect whether the computer is an Apple Silicon Mac.
# Callers run this by absolute path, from systemd and from the shell, so use the
# detector shipped beside it rather than whichever one PATH finds. -p keeps a
# root caller's exported functions out, so a launch without it is refused, and
# nothing here searches PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
dir=${BASH_SOURCE[0]%/*}
[[ $dir != "${BASH_SOURCE[0]}" ]] || dir=.
platform=$("$dir/omarchy-hw-platform") || exit 1
[[ $platform == "aarch64-apple" ]]
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Detect whether the computer is an Apple Silicon Mac (the old name of omarchy-hw-aarch64-apple).
# omarchy:hidden=true
# Kept while omarchy-mac's packages and units still call it by this name. Like
# the predicate it runs, it refuses a launch without -p and searches no PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
dir=${BASH_SOURCE[0]%/*}
[[ $dir != "${BASH_SOURCE[0]}" ]] || dir=.
exec "$dir/omarchy-hw-aarch64-apple"
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# omarchy:summary=Match the Dell XPS 13 DX13260 with an Intel Panther Lake GPU.
omarchy-hw-match "DX13260" && omarchy-hw-intel-ptl
+6 -40
View File
@@ -5,48 +5,14 @@
backlight_path="${OMARCHY_BACKLIGHT_PATH:-/sys/class/backlight}" backlight_path="${OMARCHY_BACKLIGHT_PATH:-/sys/class/backlight}"
# The platform package may name backlights to skip and to prefer (see
# docs/file-layout.md).
displays_conf=/usr/share/omarchy-platform/displays.conf
skips=()
prefers=()
if [[ -r $displays_conf ]]; then
# \r in IFS: a file saved with CRLF line ends reads the same.
while IFS=$' \t\r' read -r directive argument extra || [[ -n $directive ]]; do
[[ -n $argument && -z $extra && $argument != */* && $argument != . && $argument != .. ]] || continue
case $directive in
backlight-skip) skips+=("$argument") ;;
backlight-prefer) prefers+=("$argument") ;;
esac
done <"$displays_conf"
fi
usable() {
local skip
# The Touch Bar on T2 Macs registers a backlight that never drives the display panel.
[[ -n $1 && $1 != appletb_backlight ]] || return 1
for skip in "${skips[@]}"; do
# shellcheck disable=SC2053 # a skip is a glob
[[ $1 == $skip ]] && return 1
done
return 0
}
# Start with the first possible output, then refine to the most likely given an order heuristic. # Start with the first possible output, then refine to the most likely given an order heuristic.
device=""
while IFS= read -r name; do
if usable "$name"; then
device="$name"
break
fi
done < <(ls -1 "$backlight_path" 2>/dev/null)
# gmux comes first: apple-gmux only registers when the kernel has already picked it, and on
# dual-GPU Macs the GPU's own PWM stops driving the panel once that GPU suspends. The
# platform's preferred backlights follow, then the GPU and ACPI ones.
# Glob the candidates in the loop list: [[ ]] does not do pathname expansion. # Glob the candidates in the loop list: [[ ]] does not do pathname expansion.
for candidate in "$backlight_path"/gmux_backlight "${prefers[@]/#/$backlight_path/}" "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do # The Touch Bar on T2 Macs registers a backlight that never drives the display panel.
if [[ -e $candidate ]] && usable "${candidate##*/}"; then device="$(ls -1 "$backlight_path" 2>/dev/null | grep -vx appletb_backlight | head -n1)"
# gmux comes first: apple-gmux only registers when the kernel has already picked it, and on
# dual-GPU Macs the GPU's own PWM stops driving the panel once that GPU suspends.
for candidate in "$backlight_path"/gmux_backlight "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do
if [[ -e $candidate ]]; then
device="${candidate##*/}" device="${candidate##*/}"
break break
fi fi
+2 -26
View File
@@ -2,39 +2,15 @@
# omarchy:summary=Returns true when running on a laptop (has a lid or laptop chassis). # omarchy:summary=Returns true when running on a laptop (has a lid or laptop chassis).
acpi_lid="${OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid}"
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
dmi_chassis="${OMARCHY_DMI_CHASSIS_TYPE_PATH:-/sys/class/dmi/id/chassis_type}"
shopt -s nullglob
acpi_states=("$acpi_lid"/*/state)
input_switches=("$input_class"/*/capabilities/sw)
shopt -u nullglob
# A lid switch is the definitive signal for clamshell-capable hardware. # A lid switch is the definitive signal for clamshell-capable hardware.
for state in "${acpi_states[@]}"; do for state in /proc/acpi/button/lid/*/state; do
[[ -e $state ]] && exit 0 [[ -e $state ]] && exit 0
done done
# Without an ACPI lid (Apple Silicon, for one), any input device that reports a
# lid switch (SW_LID, bit 0 of its switch capabilities, the last word of the
# bitmap) counts. The switch's name varies: Apple's SMC input device carries
# the same name on a Mac mini, where it reports no lid.
for switches in "${input_switches[@]}"; do
[[ -r $switches ]] || continue
read -r -a words <"$switches" || true
(( ${#words[@]} )) || continue
word=${words[-1]}
[[ $word =~ ^[0-9a-fA-F]+$ ]] || continue
(( 16#$word & 1 )) && exit 0
done
# Fall back to the DMI chassis type for laptops that don't expose an ACPI lid # Fall back to the DMI chassis type for laptops that don't expose an ACPI lid
# button. 8=Portable 9=Laptop 10=Notebook 14=Sub Notebook 30=Tablet # button. 8=Portable 9=Laptop 10=Notebook 14=Sub Notebook 30=Tablet
# 31=Convertible 32=Detachable. # 31=Convertible 32=Detachable.
chassis_type="" case $(< /sys/class/dmi/id/chassis_type 2>/dev/null) in
read -r chassis_type <"$dmi_chassis" 2>/dev/null || true
case $chassis_type in
8 | 9 | 10 | 14 | 30 | 31 | 32) exit 0 ;; 8 | 9 | 10 | 14 | 30 | 31 | 32) exit 0 ;;
esac esac
+4 -40
View File
@@ -3,45 +3,9 @@
# omarchy:summary=Returns true when the laptop lid is closed # omarchy:summary=Returns true when the laptop lid is closed
# omarchy:hidden=true # omarchy:hidden=true
acpi_lid="${OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid}" for state in /proc/acpi/button/lid/*/state; do
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}" [[ -r $state ]] || continue
[[ $(< "$state") == *"closed"* ]] && exit 0
shopt -s nullglob done
acpi_states=("$acpi_lid"/*/state)
input_switches=("$input_class"/*/capabilities/sw)
shopt -u nullglob
# An input device that reports a lid switch: SW_LID, bit 0 of the last word
# of its switch bitmap.
has_lid_switch() {
local switches word
local -a words
for switches in "${input_switches[@]}"; do
[[ -r $switches ]] || continue
read -r -a words <"$switches" || true
(( ${#words[@]} )) || continue
word=${words[-1]}
[[ $word =~ ^[0-9a-fA-F]+$ ]] || continue
(( 16#$word & 1 )) && return 0
done
return 1
}
if (( ${#acpi_states[@]} )); then
# An ACPI lid reports its own state.
for state in "${acpi_states[@]}"; do
[[ -r $state ]] || continue
[[ $(<"$state") == *"closed"* ]] && exit 0
done
elif has_lid_switch; then
# Elsewhere (Apple Silicon's SMC, for one) only logind reads the switch. Ask
# it only then: it answers "open" when it tracks no lid. This runs in sudo's
# and polkit's PAM stack where fingerprint login is set up, so a stalled
# logind must not hold up every sudo.
lid_state=$(busctl --timeout=2 get-property org.freedesktop.login1 \
/org/freedesktop/login1 org.freedesktop.login1.Manager LidClosed 2>/dev/null)
[[ $lid_state == "b true" ]] && exit 0
fi
exit 1 exit 1
-202
View File
@@ -1,202 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Print the hardware platform: x86, aarch64 or aarch64-apple.
# Platform names are the CPU architecture, then the hardware family when it
# needs handling of its own: x86, aarch64 (every ARM machine without one) and
# aarch64-apple (Apple Silicon). A later family is aarch64-<family>, so
# "aarch64*" always means ARM (AGENTS.md, Platforms).
#
# ARM laptops have no DMI, so the board identity is the root node of the device
# tree the firmware hands the kernel: "apple,<model>" from Asahi's m1n1 on every
# Apple Silicon Mac, "qcom,<soc>" on Snapdragon laptops, which are plain aarch64
# here and must never pass for a Mac. Each compatible string is a NUL-separated
# list, and only a token's vendor prefix counts.
#
# An image is built away from the machine it will run on, so while this root is
# being built rather than booted, the image-target manifest the builder wrote
# names the platform and the build host's device tree is never read.
#
# Fixture roots let unprivileged tests fake the device tree, PID 1 and the image
# root. Naming any of them makes the whole view a fixture: nothing live is read,
# not even the device tree when only the image root is named. A privileged
# caller must not let its environment pick the platform at all.
# Privileged mode (-p) keeps exported functions and BASH_ENV out of this shell,
# so a launch without it is refused. Root then starts over in an empty
# environment and reads only the live paths with a fixed PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
if (( EUID == 0 )) && [[ ${1:-} != --clean-environment ]]; then
exec /usr/bin/env -i PATH=/usr/bin /bin/bash -p -- "${BASH_SOURCE[0]}" --clean-environment "$@"
fi
if (( EUID == 0 )); then
export PATH=/usr/bin
fixtures=""
else
fixtures=${OMARCHY_PROC_ROOT:-}${OMARCHY_SYS_ROOT:-}${OMARCHY_IMAGE_ROOT:-}
fi
if [[ -z $fixtures ]]; then
proc=/proc
sources=(/proc/device-tree/compatible /sys/firmware/devicetree/base/compatible)
image_root=""
image_dir=/var/lib/omarchy/image
image_owner=0
else
proc=${OMARCHY_PROC_ROOT:-}
sources=()
[[ -z ${OMARCHY_PROC_ROOT:-} ]] || sources+=("$OMARCHY_PROC_ROOT/device-tree/compatible")
[[ -z ${OMARCHY_SYS_ROOT:-} ]] || sources+=("$OMARCHY_SYS_ROOT/firmware/devicetree/base/compatible")
image_root=${OMARCHY_IMAGE_ROOT:-}
image_dir=${image_root:+$image_root/var/lib/omarchy/image}
image_owner=$EUID
fi
manifest=$image_dir/target
contradiction() {
echo "Error: contradictory platform identity: $1" >&2
exit 1
}
if ! machine=$(uname -m) || [[ -z $machine ]]; then
echo "Error: cannot read the CPU architecture" >&2
exit 1
fi
# A booted system is one systemd runs as PID 1 from this root. An image build
# is told apart by what it shows: no /run/systemd/system (a root with a /run of
# its own), PID 1's root being another (a chroot on the build host, even with
# the host's /run bound in, as arch-chroot does), or PID 1 being something other
# than systemd (a PID namespace whose PID 1 is the build). What cannot be seen,
# such as PID 1's root to a normal user or /proc/1 behind hidepid, never makes a
# build: a booted system always answers from its hardware, so a build runs as
# root. A caller in a private PID namespace looks like a build too, so nothing
# that asks the detector runs with PrivatePIDs=.
booted() {
local comm
[[ -d $image_root/run/systemd/system ]] || return 1
if [[ -n $proc && -e $proc/1/root ]]; then
[[ $proc/1/root -ef $image_root/ ]] || return 1
fi
if [[ -n $proc && -r $proc/1/comm ]]; then
read -r comm <"$proc/1/comm" && [[ $comm == "systemd" ]] || return 1
fi
}
# Owned by root (the test's own user for a fixture), not a symlink, and not
# writable by anyone else.
trusted() {
local owner mode
[[ -e $1 && ! -L $1 ]] || return 1
read -r owner mode < <(stat -c '%u %a' -- "$1") || return 1
[[ $owner =~ ^[0-9]+$ && $mode =~ ^[0-7]+$ ]] && (( owner == image_owner && (8#$mode & 8#022) == 0 ))
}
# The manifest: format=1, platform= one of the platform names, comments and
# unknown keys ignored, and any other line refused. Image builders written before
# the names settled say apple-silicon, generic-aarch64 or generic, which still
# read as aarch64-apple, aarch64 and x86. The detector reads it itself, so it
# stands alone wherever a package ships a copy of it.
image_platform() {
local line key value format="" platform=""
if [[ ! -d $image_dir || ! -f $manifest ]] || ! trusted "$image_dir" || ! trusted "$manifest"; then
echo "Error: the image manifest $manifest is not a root-owned regular file" >&2
return 1
fi
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && $line != \#* ]] || continue
if [[ $line != *=* ]]; then
echo "Error: the image manifest $manifest is malformed: $line" >&2
return 1
fi
key=${line%%=*}
value=${line#*=}
case $key in
format) format=$value ;;
platform) platform=$value ;;
esac
done <"$manifest"
if [[ $format != "1" ]]; then
echo "Error: the image manifest $manifest is not format=1" >&2
return 1
fi
case $platform in
apple-silicon) platform=aarch64-apple ;;
generic-aarch64) platform=aarch64 ;;
generic) platform=x86 ;;
esac
if [[ ! $platform =~ ^(x86|aarch64|aarch64-apple)$ ]]; then
echo "Error: the image manifest $manifest names no known platform: ${platform:-none}" >&2
return 1
fi
echo "$platform"
}
if [[ -n $image_dir ]] && [[ -e $manifest || -L $manifest ]] && ! booted; then
platform=$(image_platform) || exit 1
if [[ $platform == "x86" ]]; then
[[ $machine != "aarch64" ]] || contradiction "$manifest names $platform hardware but the CPU is $machine"
else
[[ $machine == "aarch64" ]] || contradiction "$manifest names $platform hardware but the CPU is $machine"
fi
echo "$platform"
exit 0
fi
vendor=""
vendor_source=""
for source in "${sources[@]}"; do
[[ -r $source ]] || continue
apple=0
qcom=0
mapfile -d '' -t tokens <"$source"
for token in "${tokens[@]}"; do
[[ $token == "apple,"* ]] && apple=1
[[ $token == "qcom,"* ]] && qcom=1
done
if (( apple && qcom )); then
contradiction "$source names both Apple and Qualcomm hardware"
elif (( apple )); then
found=apple
elif (( qcom )); then
found=qcom
else
found=none
fi
if [[ -n $vendor && $vendor != "$found" ]]; then
contradiction "$vendor_source says $vendor but $source says $found"
fi
vendor=$found
vendor_source=$source
done
case $vendor in
apple | qcom)
[[ $machine == "aarch64" ]] || contradiction "$vendor_source names $vendor hardware but the CPU is $machine"
if [[ $vendor == "apple" ]]; then
echo aarch64-apple
else
echo aarch64
fi
;;
*)
if [[ $machine == "aarch64" ]]; then
echo aarch64
else
echo x86
fi
;;
esac
+1 -26
View File
@@ -2,30 +2,5 @@
# omarchy:summary=Print the detected Hyprland touchpad or trackpad device name # omarchy:summary=Print the detected Hyprland touchpad or trackpad device name
mice=$(hyprctl devices -j | jq -r '.mice[].name') device=$(hyprctl devices -j | jq -r '[.mice[] | .name | select(test("touchpad|trackpad"; "i"))] | first // empty')
device=$(grep -iE -m1 'touchpad|trackpad' <<<"$mice")
# Some touchpads carry neither word (Apple Silicon's is "apple-mtp-multi-touch"),
# so ask udev which mouse is one. The name alone can't say: some touchscreens'
# mouse interfaces are called multi-touch too.
if [[ -z $device ]]; then
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
for event in "$input_class"/event*; do
[[ -r $event/device/name ]] || continue
# Hyprland's name for a device: the kernel's, lowercased, with spaces,
# commas and newlines as dashes, and -1, -2 and so on after a name another
# device already took.
name=$(<"$event/device/name")
name=${name,,}
name=${name//[ ,$'\n']/-}
udevadm info --query=property "$event" 2>/dev/null | grep -Fxq 'ID_INPUT_TOUCHPAD=1' || continue
while IFS= read -r mouse; do
if [[ $mouse == "$name" || ( $mouse == "$name"-* && ${mouse#"$name"-} =~ ^[0-9]+$ ) ]]; then
device=$mouse
break 2
fi
done <<<"$mice"
done
fi
[[ -n $device ]] && echo "$device" [[ -n $device ]] && echo "$device"
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# omarchy:summary=Detect whether the CPU is x86_64.
[[ $(uname -m) == "x86_64" ]]
-1
View File
@@ -10,7 +10,6 @@ sync_clamshell() {
( (
flock -n 9 || exit 0 flock -n 9 || exit 0
omarchy-hyprland-monitor-clamshell omarchy-hyprland-monitor-clamshell
omarchy-system-lid-inhibit
) 9>"$LOCK" ) 9>"$LOCK"
} }
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
# omarchy:summary=Configure, inspect, or switch input methods
# omarchy:args=<status|cycle [back]|configure --seed|--defaults>
# omarchy:hidden=true
set -euo pipefail
if [[ ${1:-} == "cycle" ]]; then
direction=${2:-next}
if [[ $direction != "next" && $direction != "back" ]]; then
echo "Usage: omarchy-input-method cycle [back]" >&2
exit 1
fi
# The shell's reader retains a choice made while nothing has focus.
if [[ $(omarchy-shell shell cycleInput "$direction") != "ok" ]]; then
echo "The shell's input reader is not running" >&2
exit 1
fi
else
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
fi
-22
View File
@@ -8,8 +8,6 @@ set -e
source "$OMARCHY_PATH/install/helpers/browser-policy.sh" source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
opt_path="${OMARCHY_OPT_PATH:-/opt}"
setup_chromium_policy_directory() { setup_chromium_policy_directory() {
browser_policy_setup_dir "$1" browser_policy_setup_dir "$1"
} }
@@ -19,17 +17,6 @@ announce_browser_installed() {
echo "$1 browser installed. Make it the default via Setup > Defaults > Browser." echo "$1 browser installed. Make it the default via Setup > Defaults > Browser."
} }
# The widevine package registers its CDM for Chromium and Firefox only; Brave
# looks for one in its own install directory.
link_widevine_cdm() {
local cdm="$opt_path/WidevineCdm/chromium"
local install_dir="$opt_path/$1"
if [[ -d $cdm && -d $install_dir && ! -e $install_dir/WidevineCdm ]]; then
as_root ln -sfn "$cdm" "$install_dir/WidevineCdm"
fi
}
copy_chromium_flags() { copy_chromium_flags() {
mkdir -p ~/.config mkdir -p ~/.config
cp -f "$OMARCHY_PATH/config/chromium-flags.conf" "$1" cp -f "$OMARCHY_PATH/config/chromium-flags.conf" "$1"
@@ -50,7 +37,6 @@ chromium)
setup_chromium_policy_directory /etc/chromium/policies/managed setup_chromium_policy_directory /etc/chromium/policies/managed
copy_chromium_flags ~/.config/chromium-flags.conf copy_chromium_flags ~/.config/chromium-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install chromium
announce_browser_installed "Chromium" announce_browser_installed "Chromium"
;; ;;
chrome) chrome)
@@ -60,7 +46,6 @@ chrome)
setup_chromium_policy_directory /etc/opt/chrome/policies/managed setup_chromium_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf copy_chromium_flags ~/.config/chrome-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install chrome
announce_browser_installed "Chrome" announce_browser_installed "Chrome"
;; ;;
edge) edge)
@@ -70,7 +55,6 @@ edge)
setup_chromium_policy_directory /etc/opt/edge/policies/managed setup_chromium_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install edge
announce_browser_installed "Edge" announce_browser_installed "Edge"
;; ;;
brave) brave)
@@ -79,9 +63,7 @@ brave)
setup_chromium_policy_directory /etc/brave/policies/managed setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf copy_chromium_flags ~/.config/brave-flags.conf
link_widevine_cdm brave-bin
omarchy-theme-set-browser omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install brave
announce_browser_installed "Brave" announce_browser_installed "Brave"
;; ;;
brave-origin) brave-origin)
@@ -90,9 +72,7 @@ brave-origin)
setup_chromium_policy_directory /etc/brave/policies/managed setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf copy_chromium_flags ~/.config/brave-origin-flags.conf
link_widevine_cdm brave-origin-bin
omarchy-theme-set-browser omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install brave-origin
announce_browser_installed "Brave Origin" announce_browser_installed "Brave Origin"
;; ;;
firefox) firefox)
@@ -101,7 +81,6 @@ firefox)
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_wayland setup_firefox_wayland
omarchy-lifecycle-dispatch post-install firefox
announce_browser_installed "Firefox" announce_browser_installed "Firefox"
;; ;;
zen) zen)
@@ -110,7 +89,6 @@ zen)
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_wayland setup_firefox_wayland
omarchy-lifecycle-dispatch post-install zen
announce_browser_installed "Zen" announce_browser_installed "Zen"
;; ;;
*) *)
@@ -1,27 +0,0 @@
#!/bin/bash
# omarchy:summary=Install and configure Superwhisper dictation
# omarchy:requires-sudo=true
set -e
omarchy-pkg-add superwhisper-bin
bash /usr/share/superwhisper/setup-user
# Existing profiles may still use Right Alt, which conflicts with Omarchy.
# Keep a recording shortcut active while moving away from that binding.
shortcuts=$(superwhisper shortcuts show)
if [[ $(jq -r '.shortcuts.hold' <<< "$shortcuts") == "RightAlt" ]]; then
if [[ -z $(jq -r '.shortcuts.toggle' <<< "$shortcuts") ]]; then
superwhisper shortcuts set hold "RightSuper" >/dev/null
else
superwhisper shortcuts set hold none >/dev/null
fi
fi
# Superwhisper requires a native recording shortcut; keep it separate from
# Omarchy's push-to-talk keys.
superwhisper shortcuts set toggle "Alt+Space" >/dev/null
superwhisper shortcuts set hold none >/dev/null
omarchy-dictation-use superwhisper
hyprctl reload >/dev/null
omarchy-notification-send -g  "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
superwhisper settings
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash
# omarchy:summary=Install and configure Voxtype dictation
# omarchy:requires-sudo=true
set -e
voxtype_lib_path="${OMARCHY_VOXTYPE_LIB_PATH:-/usr/lib/voxtype}"
if omarchy-cmd-missing voxtype; then
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
omarchy-pkg-add wtype voxtype-bin
else
exit 0
fi
fi
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
mkdir -p "$config_home/voxtype"
if [[ ! -e $config_home/voxtype/config.toml ]]; then
cp "$OMARCHY_PATH/default/voxtype/config.toml" "$config_home/voxtype/"
# A Vulkan runtime is not enough: aarch64 voxtype-bin ships no Vulkan backend.
if omarchy-hw-vulkan && [[ -x $voxtype_lib_path/voxtype-vulkan ]]; then
voxtype setup gpu --enable || true
fi
fi
# Setup reuses the configured model and downloads it only when missing.
# Retry these steps even when an earlier attempt already installed the binary.
voxtype setup --download --no-post-install
voxtype setup systemd
systemctl --user enable --now voxtype.service
omarchy-dictation-use voxtype
hyprctl reload >/dev/null
omarchy-notification-send -g  "Voxtype Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
-1
View File
@@ -15,7 +15,6 @@ echo "Installing Steam..."
# GPU. # GPU.
omarchy-install-gaming-gpu-lib32 omarchy-install-gaming-gpu-lib32
omarchy-pkg-add steam omarchy-pkg-add steam
omarchy-lifecycle-dispatch post-install steam
echo "" echo ""
echo "Steam will start automatically now. This might take a while..." echo "Steam will start automatically now. This might take a while..."
+1 -3
View File
@@ -15,9 +15,7 @@
# against a half-written tree, and that failed reload leaves a second engine # against a half-written tree, and that failed reload leaves a second engine
# generation behind that turns the next restart's IPC kill into a crash. # generation behind that turns the next restart's IPC kill into a crash.
run_shell() { run_shell() {
local startup_background QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
startup_background=$(readlink -f "$HOME/.local/state/omarchy/current/background" 2>/dev/null)
OMARCHY_STARTUP_BACKGROUND="$startup_background" QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" & systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
shell_pid=$! shell_pid=$!
-200
View File
@@ -1,200 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Run a fixed lifecycle operation through the platform's own packages
# omarchy:args=[--resolve] <operation> [arguments...]
# omarchy:requires-sudo=true
# omarchy:hidden=true
# Provisioning, factory reset, update, boot, system and user setup and app
# installs call a fixed set of operations here. A platform that needs its own
# handling registers directories of root-owned entrypoints below; on every other
# platform each operation is a no-op and the caller keeps its generic path. The
# contract is docs/lifecycle-dispatch.md.
#
# --resolve prints the entrypoint an operation would run, or nothing when it is
# a no-op here, so a caller can choose between a platform implementation and
# its own generic path.
#
# Root runs this from provisioning, factory reset and update, and it decides
# what runs as root. -p keeps a root caller's environment from running code in
# this shell before the first line (bash then ignores BASH_ENV and exported
# functions), so a launch without it is refused.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
set -euo pipefail
operations=(
provision-prepare provision-commit provision-verify
reset-prepare reset-verify reset-commit reset-rollback
update-verify update-takeover
luks-slots
setup-boot setup-system setup-user
post-install pre-remove
)
# Run as the user, never as root, with that user's session.
user_operations=(setup-user post-install pre-remove)
usage() {
echo "Usage: omarchy-lifecycle-dispatch [--resolve] <operation> [arguments...]" >&2
echo "Operations: ${operations[*]}" >&2
exit 2
}
listed() {
local item=$1 candidate
shift
for candidate in "$@"; do
[[ $candidate == "$item" ]] && return 0
done
return 1
}
resolve=0
if [[ ${1:-} == "--resolve" ]]; then
resolve=1
shift
fi
operation=${1:-}
[[ -n $operation ]] || usage
shift
listed "$operation" "${operations[@]}" || usage
# A privileged caller must not let its environment choose what runs as root:
# root resolves only the fixed paths below, and the detector beside this file
# ignores its fixture roots. Fixture roots exist for unprivileged tests.
fixed_path=/usr/local/sbin:/usr/local/bin:/usr/bin
detect=("$(PATH=$fixed_path dirname -- "${BASH_SOURCE[0]}")/omarchy-hw-platform")
if (( EUID == 0 )); then
export PATH=$fixed_path
detect=(/usr/bin/env -i PATH="$fixed_path" "${detect[@]}")
root=""
else
root=${OMARCHY_LIFECYCLE_ROOT:-}
root=${root%/}
if [[ -n $root && $root != /* ]]; then
echo "Error: OMARCHY_LIFECYCLE_ROOT must be an absolute path" >&2
exit 1
fi
fi
if ! platform=$("${detect[@]}"); then
echo "Error: cannot determine the hardware platform for $operation" >&2
exit 1
fi
# x86 and plain aarch64 machines (Snapdragon laptops among them) have no
# registered package: every operation is a no-op there and callers keep their
# generic path.
# Apple Silicon's boot operations belong to omarchy-mac-boot, and its system and
# user setup and app-install hooks to omarchy-mac (both omacom/omarchy-mac-pkgs).
# Provisioning and factory reset have no other path on a Mac, only update-verify
# checks what an update left in its boot chain, its boot checks prove the kept
# LUKS slots that luks-slots records, and hardware setup has no other way to run
# the Mac's boot setup (setup-boot), so those operations are required. So is
# update-takeover: the Mac boot chain and its image own files no package does,
# and a Mac whose boot package can't vouch for a takeover keeps them. Setup and
# the app-install hooks are optional: a Mac without omarchy-mac keeps Omarchy's
# generic setup and installs. Moving an operation to required also changes
# apple_optional in test/shell.d/lifecycle-dispatch-test.sh and the Apple column
# and registration table in docs/lifecycle-dispatch.md.
case $platform:$operation in
aarch64-apple:setup-system | aarch64-apple:setup-user | aarch64-apple:post-install | aarch64-apple:pre-remove)
implementation=/usr/lib/omarchy/mac
package=omarchy-mac
required=()
;;
aarch64-apple:*)
implementation=/usr/lib/omarchy/mac-boot
package=omarchy-mac-boot
required=(
provision-prepare provision-commit provision-verify
reset-prepare reset-verify reset-commit reset-rollback
update-verify update-takeover
luks-slots
setup-boot
)
;;
*)
exit 0
;;
esac
# Checked once the platform registers the operation, so an installer run with
# sudo still works where the hook is a no-op.
if listed "$operation" "${user_operations[@]}" && (( EUID == 0 )); then
echo "Error: $operation runs as the user, never as root" >&2
exit 1
fi
# Every path from the entrypoint up to / (or the fixture root) must be a real
# file or directory owned by root, or by the unprivileged caller inside a
# fixture root, and writable by nobody else.
trusted() {
local path=$1 top=${root:-/} owner mode
[[ -f $path && -x $path ]] || return 1
while true; do
[[ ! -L $path ]] || return 1
read -r owner mode < <(stat -c '%u %a' -- "$path") || return 1
if (( owner != 0 )); then
[[ -n $root ]] && (( EUID != 0 && owner == EUID )) || return 1
fi
(( (8#$mode & 8#022) == 0 )) || return 1
[[ $path != "$top" ]] || return 0
[[ $path != "/" ]] || return 1
path=$(dirname -- "$path")
done
}
entrypoint=$root$implementation/$operation
# The package's own pacman record tells a machine without it from one whose
# copy predates the operation.
installed_version() {
local record
for record in "$root/var/lib/pacman/local/$package"-[0-9]*/; do
[[ -d $record ]] || continue
record=${record%/}
printf '%s\n' "${record##*/"$package"-}"
return 0
done
return 1
}
# 3 is a machine without the platform's boot package at all, which a caller may
# tell apart from a failure; 1 is everything else.
if [[ ! -e $entrypoint && ! -L $entrypoint ]]; then
if listed "$operation" "${required[@]}"; then
if version=$(installed_version); then
echo "Error: $operation on $platform needs $implementation/$operation, which $package $version does not provide; update $package" >&2
exit 1
fi
echo "Error: $operation on $platform needs $package, which provides $implementation/$operation; it is not installed" >&2
exit 3
fi
exit 0
fi
if ! trusted "$entrypoint"; then
echo "Error: refusing $implementation/$operation: it must be a root-owned executable file in root-owned directories, writable by no one else" >&2
exit 1
fi
if (( resolve )); then
printf '%s\n' "$entrypoint"
exit 0
fi
# A user operation also gets the user's home and session, when there is one,
# and nothing else.
environment=(PATH="$fixed_path")
if listed "$operation" "${user_operations[@]}"; then
for name in HOME USER XDG_CONFIG_HOME XDG_STATE_HOME XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS WAYLAND_DISPLAY OMARCHY_PATH; do
[[ -z ${!name:-} ]] || environment+=("$name=${!name}")
done
fi
exec /usr/bin/env -i "${environment[@]}" "$entrypoint" "$@"
+3 -26
View File
@@ -9,25 +9,12 @@ declare -A LUA_BIND_KEY_MAP
declare -A LUA_BIND_DISPATCHER_MAP declare -A LUA_BIND_DISPATCHER_MAP
declare -A LUA_BIND_ARG_MAP declare -A LUA_BIND_ARG_MAP
# A platform package can name keys the way its keyboards print them: one
# "<keysym> <name>" per line (on a MacBook, XF86MonBrightnessUp is F2), in the
# fixed platform root, which no environment variable moves.
key_names_file=/usr/share/omarchy-platform/key-names
# Hyprland reports XKB keycodes for code: bindings. Resolve them to symbols # Hyprland reports XKB keycodes for code: bindings. Resolve them to symbols
# via the compiled keymap, with a small fallback for common keys so the menu # via the compiled keymap, with a small fallback for common keys so the menu
# remains readable if xkbcli cannot resolve a symbol. # remains readable if xkbcli cannot resolve a symbol.
parse_keycodes() { parse_keycodes() {
awk -v key_names="$key_names_file" ' awk '
BEGIN { BEGIN {
while ((getline line < key_names) > 0) {
# Blanks around a line and a CRLF line end read the same, as in
# displays.conf.
gsub(/^[ \t\r]+|[ \t\r]+$/, "", line)
if (split(line, name_parts, /[ \t]+/) == 2 && name_parts[1] ~ /^[A-Za-z0-9_]+$/ && name_parts[2] ~ /^[A-Za-z0-9_]+$/) key_name[name_parts[1]] = name_parts[2]
}
close(key_names)
split("10=1 11=2 12=3 13=4 14=5 15=6 16=7 17=8 18=9 19=0 20=MINUS 21=EQUAL 59=COMMA 60=PERIOD 61=SLASH", fallbacks, " ") split("10=1 11=2 12=3 13=4 14=5 15=6 16=7 17=8 18=9 19=0 20=MINUS 21=EQUAL 59=COMMA 60=PERIOD 61=SLASH", fallbacks, " ")
for (i in fallbacks) { for (i in fallbacks) {
separator = index(fallbacks[i], "=") separator = index(fallbacks[i], "=")
@@ -72,13 +59,6 @@ parse_keycodes() {
symbol = mouse_symbol[code] symbol = mouse_symbol[code]
if (symbol == "") symbol = "mouse:" code if (symbol == "") symbol = "mouse:" code
sub("mouse:" code, symbol) sub("mouse:" code, symbol)
} else if ((first = index($0, ",")) > 0) {
# Only the key (the second field) is renamed: the description, the
# dispatcher and its argument are left as they are.
rest = substr($0, first + 1)
second = index(rest, ",")
key = second > 0 ? substr(rest, 1, second - 1) : rest
if (key in key_name) $0 = substr($0, 1, first) key_name[key] (second > 0 ? substr(rest, second) : "")
} }
print print
@@ -101,9 +81,7 @@ build_lua_bind_cache() {
LUA_BIND_DISPATCHER_MAP["$cache_key"]="$dispatcher" LUA_BIND_DISPATCHER_MAP["$cache_key"]="$dispatcher"
LUA_BIND_ARG_MAP["$cache_key"]="$arg" LUA_BIND_ARG_MAP["$cache_key"]="$arg"
done < <( done < <(
# -E keeps LUA_INIT and LUA_PATH from the environment out of the scan, which lua <<'LUA'
# loads the user's config.
lua -E - <<'LUA'
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 } local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }
local function split_keys(keys) local function split_keys(keys)
@@ -555,9 +533,8 @@ output_binding_records_uncached() {
keybindings_cache_key() { keybindings_cache_key() {
{ {
printf 'v15\n' printf 'v14\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:' hyprctl devices 2>/dev/null | grep -F 'active keymap:'
cat "$key_names_file" 2>/dev/null
hyprctl binds 2>/dev/null hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }' } | sha256sum | awk '{ print $1 }'
} }
+4 -46
View File
@@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Pick one or multiple options from a menu # omarchy:summary=Pick one option from a menu
# omarchy:group=menu # omarchy:group=menu
# omarchy:name=select # omarchy:name=select
# omarchy:args=prompt [option...] [-- --multiple --selected <value> --on-change <command-json> --change-key <key> --width <pixels> --maxheight <pixels>] # omarchy:args=prompt [option...] [-- menu args...]
# omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400 # omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400
# An option may lead with an icon, as "<glyph><TAB><label>", and may trail a # An option may lead with an icon, as "<glyph><TAB><label>", and may trail a
@@ -12,12 +12,6 @@
# alone; an option with a subtext returns "<label><TAB><subtext>", so callers # alone; an option with a subtext returns "<label><TAB><subtext>", so callers
# with same-named rows get the subtext back as the stable key. # with same-named rows get the subtext back as the stable key.
# With --multiple, clicking/Enter toggles rows and Apply returns a JSON array.
# --on-change takes a JSON command array; each toggle runs it with the selection
# JSON appended, applies immediately, and needs no Apply row. A successful
# command may print a JSON array of actual selections to update the checkmarks.
# --selected takes a returned value (label plus any subtext), and can be repeated.
set -euo pipefail set -euo pipefail
if (( $# < 1 )); then if (( $# < 1 )); then
@@ -31,43 +25,12 @@ shift
options=() options=()
menu_width="" menu_width=""
menu_maxheight="" menu_maxheight=""
multiple=0
on_change="[]"
change_key=""
selected=()
while (( $# > 0 )); do while (( $# > 0 )); do
if [[ $1 == "--" ]]; then if [[ $1 == "--" ]]; then
shift shift
while (( $# > 0 )); do while (( $# > 0 )); do
case "$1" in case "$1" in
--on-change)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --on-change requires a JSON command array" >&2
exit 1
fi
on_change="$1"
;;
--change-key)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --change-key requires a value" >&2
exit 1
fi
change_key="$1"
;;
--multiple)
multiple=1
;;
--selected)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --selected requires a value" >&2
exit 1
fi
selected+=("$1")
;;
--width) --width)
shift shift
if (( $# == 0 )); then if (( $# == 0 )); then
@@ -110,25 +73,20 @@ rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file"' EXIT trap 'rm -f "$selection_file" "$done_file"' EXIT
options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}") options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}")
selected_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${selected[@]}")
payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e ' payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e '
my $payload = { my $payload = {
mode => "select", mode => "select",
prompt => decode("UTF-8", $ARGV[0]), prompt => decode("UTF-8", $ARGV[0]),
options => decode_json($ARGV[1]), options => decode_json($ARGV[1]),
multiple => $ARGV[6] ? JSON::PP::true : JSON::PP::false,
selected => decode_json($ARGV[7]),
onChange => decode_json($ARGV[8]),
changeKey => $ARGV[9],
selectionFile => decode("UTF-8", $ARGV[2]), selectionFile => decode("UTF-8", $ARGV[2]),
doneFile => decode("UTF-8", $ARGV[3]) doneFile => decode("UTF-8", $ARGV[3])
}; };
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // ""); $payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // ""); $payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
print encode_json($payload) print encode_json($payload)
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json" "$on_change" "$change_key") ' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null || exit 2 omarchy-shell shell summon omarchy.menu "$payload" >/dev/null
while [[ ! -e $done_file ]]; do while [[ ! -e $done_file ]]; do
sleep 0.05 sleep 0.05
-53
View File
@@ -1,53 +0,0 @@
#!/bin/bash
# omarchy:summary=Print the default package set for this computer's platform, or for the named one: the base list plus its architecture's and platform's additions.
# omarchy:args=[x86|aarch64|aarch64-apple]
# omarchy:hidden=true
set -eo pipefail
platform=${1:-}
if [[ -z $platform ]]; then
platform=$(omarchy-hw-platform)
fi
# Image builders written before the platform names settled pass the old ones.
case $platform in
apple-silicon) platform=aarch64-apple ;;
generic-aarch64) platform=aarch64 ;;
generic) platform=x86 ;;
esac
case $platform in
x86) lists=(base) ;;
aarch64) lists=(base aarch64) ;;
aarch64-apple) lists=(base aarch64 "$platform") ;;
*)
echo "Error: unknown platform '$platform'" >&2
exit 1
;;
esac
# The base list always exists. An architecture or platform with nothing to add
# has no list of its own.
files=()
for list in "${lists[@]}"; do
file="$OMARCHY_PATH/install/omarchy-$list.packages"
if [[ $list == "base" || -f $file ]]; then
files+=("$file")
fi
done
names() {
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$@"
}
# One name per line, first occurrence kept, in list order. An aarch64 platform
# leaves out the base packages that only x86_64 builds.
if [[ $platform == "x86" ]]; then
names "${files[@]}" | awk '!seen[$0]++'
else
names "${files[@]}" | awk -v skips=<(names "$OMARCHY_PATH/install/omarchy-x86_64-only.packages") '
BEGIN { while ((getline name < skips) > 0) skip[name] = 1 }
!skip[$0] && !seen[$0]++'
fi
+2 -4
View File
@@ -68,6 +68,8 @@ run_first_run_step() {
fi fi
} }
run_first_run_step "install Voxtype post-update hook" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
run_first_run_step "install fingerprint setup post-update hook" \ run_first_run_step "install fingerprint setup post-update hook" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook" omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
run_first_run_step "install agent setup post-update hook" \ run_first_run_step "install agent setup post-update hook" \
@@ -75,10 +77,6 @@ run_first_run_step "install agent setup post-update hook" \
run_first_run_step "enable user systemd units" \ run_first_run_step "enable user systemd units" \
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh" bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
# Finalization ran it too, maybe without a session; the platform finishes what
# needs one here.
run_first_run_step "run the platform's user setup" \
omarchy-lifecycle-dispatch setup-user
run_first_run_step "set GNOME theme" \ run_first_run_step "set GNOME theme" \
bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh" bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh"
run_first_run_step "set GTK primary paste" \ run_first_run_step "set GTK primary paste" \
-162
View File
@@ -1,162 +0,0 @@
#!/bin/bash
# omarchy:summary=Finish the hardware setup an image build deferred to this machine
# omarchy:requires-sudo=true
# omarchy:hidden=true
# Started by omarchy-provision-hardware.service on every boot while an image's
# deferred steps remain (see install/helpers/image-target.sh), and safe to call
# from a platform's own first boot as well: runs are serialised. The first run
# retires the image's build manifest, so from then on this machine is itself.
# Steps run in order and each one that succeeds leaves the queue, so a failure
# stops the run and the next run resumes at that step. Once the queue is empty
# the initramfs is rebuilt if any step changed its configuration or asked for
# the rebuild, and the service is disabled. With nothing deferred this does
# nothing. A failed step, or a failure to make the pacman keyring the build
# asked for, exits 75 (it stays queued for the next run); anything else that
# stops the run, such as an untrusted manifest or queue, exits 1.
set -euo pipefail
fail() {
echo "Error: $*" >&2
exit 1
}
if (( EUID == 0 )); then
export OMARCHY_PATH=/usr/share/omarchy
export PATH=/usr/share/omarchy/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
elif [[ -z ${OMARCHY_IMAGE_ROOT:-} || -z ${OMARCHY_PATH:-} ]]; then
fail "omarchy-provision-hardware must run as root"
fi
export OMARCHY_INSTALL=$OMARCHY_PATH/install
export OMARCHY_INSTALL_USER=""
source "$OMARCHY_INSTALL/helpers/logging.sh"
source "$OMARCHY_INSTALL/helpers/image-target.sh"
omarchy_image_init
export OMARCHY_INSTALL_LOG_FILE=$omarchy_image_root/var/log/omarchy-install.log
queue_present() {
[[ -e $omarchy_image_queue || -L $omarchy_image_queue ]]
}
# Anything left for the first boot: the manifest, a queue, or the keyring an
# image build asked for, which is owed even when no step was queued.
work_present() {
omarchy_image_manifest_present || queue_present || [[ -e $omarchy_image_keyring_request ]]
}
work_present || exit 0
omarchy_image_lock || exit 1
# Another run may have finished while this one waited for the lock.
work_present || exit 0
if omarchy_image_manifest_present; then
omarchy_image_read_manifest || exit 1
mv -f "$omarchy_image_manifest" "$omarchy_image_manifest.booted" || fail "could not retire $omarchy_image_manifest"
live_platform=$(omarchy-hw-platform) || live_platform=unknown
echo "First boot of an image built for $omarchy_image_platform, on $live_platform hardware"
fi
remaining=()
if queue_present; then
[[ -f $omarchy_image_queue ]] && omarchy_image_trusted "$omarchy_image_queue" ||
fail "$omarchy_image_queue is not a root-owned regular file"
mapfile -t queued <"$omarchy_image_queue"
for step in "${queued[@]}"; do
[[ -z $step ]] || remaining+=("$step")
done
fi
# What the boot image is built from: mkinitcpio's configuration, the module
# options its modconf hook copies in (leaves such as nvidia.sh write them), and
# the kernel command line Limine's entry tool adds (leaves such as intel/fred.sh
# add to it).
initramfs_inputs() {
local file
for file in "$omarchy_image_root/etc/mkinitcpio.conf" "$omarchy_image_root/etc/mkinitcpio.conf.d/"* \
"$omarchy_image_root/etc/modprobe.d/"* "$omarchy_image_root/usr/lib/modprobe.d/"* \
"$omarchy_image_root/etc/default/limine" "$omarchy_image_root/etc/limine-entry-tool.d/"*; do
[[ -e $file ]] || continue
printf '%s\0' "$file"
cat -- "$file"
done | cksum
}
# Recorded before the first step and kept until a rebuild lands, so a step that
# changed the initramfs and then failed or lost power still gets one.
if [[ ! -f $omarchy_image_initramfs_baseline ]]; then
baseline=$(mktemp "$omarchy_image_dir/.initramfs-inputs.XXXXXX") &&
initramfs_inputs >"$baseline" && chmod 0644 "$baseline" && mv -f "$baseline" "$omarchy_image_initramfs_baseline" ||
fail "could not record the initramfs inputs"
fi
mkdir -p "$(dirname "$OMARCHY_INSTALL_LOG_FILE")"
# A step that would build the boot image itself (a platform's boot setup) asks
# for this run's rebuild instead, so the image is built once, after the last
# step. The request stays on disk until that rebuild lands.
export OMARCHY_IMAGE_BOOT_REBUILD=$omarchy_image_boot_rebuild
# Tells a step it runs on the booted machine, after boot already brought up the
# targets its units hang off (see install/hardware/bluetooth.sh).
export OMARCHY_IMAGE_DEFERRED_HARDWARE=1
# An image build asks for this machine's own pacman keyring (see
# install/post-install/pacman.sh), made before any step installs a package. Any
# keyring the build or an interrupted run left is replaced; the request stays
# until a keyring is complete.
if [[ -e $omarchy_image_keyring_request ]]; then
echo "Creating this machine's pacman keyring"
keyring=$omarchy_image_root/etc/pacman.d/gnupg
if ! { rm -rf "$keyring" && pacman-key --gpgdir "$keyring" --init &&
pacman-key --gpgdir "$keyring" --populate; } </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1; then
echo "Error: could not create the pacman keyring (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it" >&2
exit 75
fi
rm -f "$omarchy_image_keyring_request"
fi
while (( ${#remaining[@]} )); do
step=${remaining[0]}
[[ $step == install/hardware/*.sh && $step != *..* ]] || fail "Deferred step is not a hardware setup step: $step"
script=$OMARCHY_PATH/$step
if [[ -L $script ]]; then
fail "Deferred step is a symlink: $step"
elif [[ ! -e $script ]]; then
echo "Skipping deferred hardware step $step: this Omarchy no longer ships it"
else
echo "Running deferred hardware step $step"
if ! run_logged "$script"; then
echo "Error: Deferred hardware step failed: $step (see $OMARCHY_INSTALL_LOG_FILE); the next run resumes here" >&2
exit 75
fi
fi
remaining=("${remaining[@]:1}")
omarchy_image_write_queue "${remaining[@]}" || fail "could not update $omarchy_image_queue"
done
if [[ -e $omarchy_image_boot_rebuild || $(initramfs_inputs) != "$(<"$omarchy_image_initramfs_baseline")" ]]; then
echo "Rebuilding the initramfs for the deferred hardware setup"
if omarchy-cmd-present limine-mkinitcpio; then
limine-mkinitcpio </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
else
mkinitcpio -P </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
fi || fail "Initramfs rebuild failed (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it"
# The new image must be on disk before the queue that owes it is cleared.
sync || fail "could not flush the rebuilt initramfs to disk"
fi
rm -f "$omarchy_image_initramfs_baseline" "$omarchy_image_boot_rebuild"
# Disabled, not deleted: a platform first boot that runs this while the
# service's start job waits would otherwise fail that job at the next
# daemon-reload. The emptied queue keeps the unit from running.
rm -f "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit"
rm -f "$omarchy_image_queue"
if (( EUID == 0 )); then
systemctl daemon-reload 2>/dev/null || true
fi
echo "Deferred hardware setup is complete"
+89 -264
View File
@@ -9,9 +9,7 @@
# behind. Asks for the user (the configurator's Step 2), creates it with the # behind. Asks for the user (the configurator's Step 2), creates it with the
# groups system setup recorded, finalizes it offline from the stashed Node # groups system setup recorded, finalizes it offline from the stashed Node
# tarball, re-keys LUKS from the throwaway install passphrase to the user's # tarball, re-keys LUKS from the throwaway install passphrase to the user's
# password, and hands off to SDDM. A platform whose boot package owns its boot # password, and hands off to SDDM.
# chain prepares it, drops the install key's unlock from it and records the
# kept key slot through omarchy-lifecycle-dispatch.
set -euo pipefail set -euo pipefail
@@ -35,9 +33,6 @@ fi
export PATH="$OMARCHY_PATH/bin:$PATH" export PATH="$OMARCHY_PATH/bin:$PATH"
REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state
source "$OMARCHY_PATH/install/provisioning/luks-rekey.sh"
# The shared setup form — the same questions, prompts, and validation the ISO # The shared setup form — the same questions, prompts, and validation the ISO
# configurator's user step asks. The ISO vendors this very file out of the # configurator's user step asks. The ISO vendors this very file out of the
# runtime package it bundles, so an install and the first boot that finishes it # runtime package it bundles, so an install and the first boot that finishes it
@@ -584,27 +579,14 @@ greeter_screen() {
# The keyboard step the ISO configurator runs — deferred to first boot for OEM # The keyboard step the ISO configurator runs — deferred to first boot for OEM
# installs, so the machine's owner picks their own layout. Applied immediately # installs, so the machine's owner picks their own layout. Applied immediately
# (live VT + persisted) so the password typed next, and the LUKS re-key below, # (live VT + persisted) so the password typed next, and the LUKS re-key below,
# use the chosen layout. A layout that did not take is asked for again: the # use the chosen layout.
# password form must never be typed in a layout the disk unlock won't use.
# Three failures in a row mean the machine can't set a layout at all (a
# missing or broken localectl, say), so the attempt fails and setup's own
# retry-or-console screen takes over instead of asking forever.
keyboard_form() { keyboard_form() {
local status failures=0 local status
while true; do while true; do
step "Let's setup your keyboard..." step "Let's setup your keyboard..."
omarchy_prompt_keyboard && status=0 || status=$? omarchy_prompt_keyboard && status=0 || status=$?
if ((status == 0)); then ((status == 0)) && break
apply_keyboard "$keyboard" && break
if ((++failures >= 3)); then
log_step "no keyboard layout could be set after $failures attempts"
notice "Setup could not set a keyboard layout on this machine." 4
return 1
fi
notice "Could not set the ${keyboard_label:-$keyboard} keyboard layout. Choose it again." 4
continue
fi
# Esc means "back", and nothing precedes the first screen, so re-ask. # Esc means "back", and nothing precedes the first screen, so re-ask.
((status == OMARCHY_FORM_BACK)) && continue ((status == OMARCHY_FORM_BACK)) && continue
@@ -616,7 +598,7 @@ keyboard_form() {
confirm_reboot && exec systemctl reboot confirm_reboot && exec systemctl reboot
done done
omarchy_persist_input_selection apply_keyboard "$keyboard"
} }
confirm_reboot() { confirm_reboot() {
@@ -627,73 +609,21 @@ confirm_reboot() {
gum confirm --affirmative "Yes, reboot" --negative "No, keep setting up" "Reboot this machine?" gum confirm --affirmative "Yes, reboot" --negative "No, keep setting up" "Reboot this machine?"
} }
# XKB settings for the form's keymaps that kbd-model-map lacks, as its rows for # Load the layout on the live VT and persist it for the installed system.
# the nearest keymap give them, so the desktop types what the console does: # systemd-firstboot writes both the console KEYMAP and the XKB layout Hyprland
# kbd's cz is QWERTY and bg-cp1251 phonetic. Non-Latin layouts keep a US layout # reads, matching what the ISO's configure_keyboard does at install time. A
# to switch to, so the shared password stays typeable. # keymap localectl doesn't know keeps the default rather than failing (defensive;
keyboard_xkb_settings() { # the picker no longer offers any such layout).
local layout variant="" options=terminate:ctrl_alt_bksp
local switch=terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
case $1 in
colemak) layout=us variant=colemak ;;
bg-cp1251) layout=bg,us variant=phonetic, options=$switch ;;
cz) layout=cz,us variant=qwerty, options=$switch ;;
de_CH-latin1) layout=ch ;;
kyrgyz) layout=kg,us options=$switch ;;
no-latin1) layout=no ;;
pl) layout=pl ;;
ua) layout=ua,us options=$switch ;;
*) return 1 ;;
esac
printf 'XKBLAYOUT=%s\nXKBMODEL=pc105\n' "$layout"
[[ -z $variant ]] || printf 'XKBVARIANT=%s\n' "$variant"
printf 'XKBOPTIONS=%s\n' "$options"
}
# Load the layout on the live VT and persist it for the installed system, or
# fail. systemd-firstboot writes both the console KEYMAP and the XKB layout
# Hyprland reads, matching what the ISO's configure_keyboard does at install
# time. The boot files ask for the disk password in the layout vconsole.conf
# names, so a keymap localectl doesn't know, a failed loadkeys, or a file that
# does not read back as the chosen keymap with an XKB layout is a failure.
apply_keyboard() { apply_keyboard() {
local keymap=$1 keymaps saved local keymap="$1"
[[ $(tty 2>/dev/null) == /dev/tty* ]] && loadkeys "$keymap" 2>/dev/null || true
if [[ $(tty 2>/dev/null) == /dev/tty* ]] && ! loadkeys "$keymap" >>"$LOG_FILE" 2>&1; then if localectl --no-pager list-keymaps 2>/dev/null | grep -qix "$keymap"; then
log_step "loadkeys could not load keymap $keymap" systemd-firstboot --keymap="$keymap" --force >>"$LOG_FILE" 2>&1 || \
return 1 localectl set-keymap "$keymap" >>"$LOG_FILE" 2>&1 || \
fi log_step "could not persist keymap $keymap"
else
if ! keymaps=$(localectl --no-pager list-keymaps 2>>"$LOG_FILE"); then log_step "keymap $keymap unknown to localectl; keeping the default"
log_step "localectl could not list keymaps to check $keymap"
return 1
fi
if ! grep -qxF -- "$keymap" <<<"$keymaps"; then
log_step "keymap $keymap unknown to localectl"
return 1
fi
if ! systemd-firstboot --keymap="$keymap" --force >>"$LOG_FILE" 2>&1 &&
! localectl set-keymap "$keymap" >>"$LOG_FILE" 2>&1; then
log_step "could not persist keymap $keymap"
return 1
fi
# systemd-firstboot writes an XKB layout only for keymaps in kbd-model-map,
# and several the form offers are not, so Hyprland would come up in US.
if ! grep -q '^XKBLAYOUT=.' /etc/vconsole.conf 2>/dev/null; then
keyboard_xkb_settings "$keymap" >>/etc/vconsole.conf 2>>"$LOG_FILE" ||
log_step "no XKB layout known for keymap $keymap"
fi
# A clean shell, so nothing this process exported stands in for the file.
saved=$(env -i /bin/bash --noprofile --norc -c \
'. /etc/vconsole.conf && [[ -n ${XKBLAYOUT:-} ]] && printf %s "${KEYMAP:-}"' 2>>"$LOG_FILE") || saved=""
if [[ $saved != "$keymap" ]]; then
log_step "/etc/vconsole.conf does not read back as keymap $keymap with an XKB layout"
return 1
fi fi
} }
@@ -784,7 +714,7 @@ luks_device() {
local src part local src part
src=$(findmnt -no SOURCE / | sed 's/\[.*//') src=$(findmnt -no SOURCE / | sed 's/\[.*//')
[[ -n $src ]] || return 1 [[ -n $src ]] || return 1
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}') part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
[[ -n $part ]] && { echo "$part"; return 0; } [[ -n $part ]] && { echo "$part"; return 0; }
return 1 return 1
} }
@@ -892,24 +822,13 @@ configure_hostname() {
# step, overwriting the placeholder (UTC) the deferred install seeded. # step, overwriting the placeholder (UTC) the deferred install seeded.
configure_timezone() { configure_timezone() {
[[ -n ${timezone:-} ]] || return 0 [[ -n ${timezone:-} ]] || return 0
if ! timedatectl set-timezone "$timezone" 2>&1; then if timedatectl set-timezone "$timezone" 2>&1; then
# Fallback only if the zone file exists, so a bad zone can't point return 0
# /etc/localtime at nothing and break every localtime read afterward.
[[ -e /usr/share/zoneinfo/$timezone ]] &&
ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime 2>&1 || true
fi
configure_wireless_regdom
}
# The install's hardware step found only the UTC placeholder, which names no
# country, so the wireless regulatory domain comes from the owner's timezone
# here. No reboot follows setup, so the radios get it now the way udev applies
# it at every boot.
configure_wireless_regdom() {
bash "$OMARCHY_PATH/install/hardware/set-wireless-regdom.sh" 2>&1 || return 0
if omarchy-cmd-present set-wireless-regdom; then
set-wireless-regdom 2>&1 || true
fi fi
# Fallback only if the zone file exists, so a bad zone can't point
# /etc/localtime at nothing and break every localtime read afterward.
[[ -e /usr/share/zoneinfo/$timezone ]] &&
ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime 2>&1 || true
} }
finalize_user() { finalize_user() {
@@ -932,99 +851,16 @@ finalize_user() {
"$OMARCHY_PATH/bin/omarchy-provision-user" --force --first-install "$OMARCHY_PATH/bin/omarchy-provision-user" --force --first-install
} }
# During the provisioning window the staged key rides in the UKI's initramfs,
# named on the kernel cmdline, so boot unlocks unattended.
PROVISIONING_UNLOCK_FILES=(
/etc/omarchy/provisioning.key
/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
)
limine_auto_unlock_present() {
local file
for file in "${PROVISIONING_UNLOCK_FILES[@]}"; do
[[ -e $file ]] && return 0
done
return 1
}
# Rebuild the UKI without the embedded keyfile. A failed rebuild restores the
# auto-unlock, so the retry still boots unattended.
limine_auto_unlock_drop() {
rm -f "${PROVISIONING_UNLOCK_FILES[@]}"
reset_limine_config
if limine-update >>"$LOG_FILE" 2>&1; then
return 0
fi
# Once the staged key is destroyed there is no auto-unlock to restore: the
# boot files already ask for the owner's password. The rebuild is still
# retried for the boot entries.
if [[ -f $PROVISIONING_DIR/luks-key ]]; then
log_step "limine-update failed during re-key; restoring auto-unlock for retry"
install -Dm600 "$PROVISIONING_DIR/luks-key" /etc/omarchy/provisioning.key
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
echo 'FILES+=(/etc/omarchy/provisioning.key)' >/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
else
log_step "limine-update failed during re-key; the staged key is gone, so no auto-unlock is restored"
fi
limine-update >>"$LOG_FILE" 2>&1 || true
return 1
}
# A platform whose boot package implements provision-commit and provision-verify
# stages the unlock in its own boot chain and owns it; everywhere else it rides
# in the Limine UKI above. Deciding fails closed: the unlock then counts as
# present and cannot be dropped.
UNLOCK_OWNER=""
unlock_owner() {
[[ -z ${UNLOCK_OWNER:-} ]] || return 0
local commit verify
commit=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve provision-commit) || return 1
verify=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve provision-verify) || return 1
if [[ -n $commit && -n $verify ]]; then
UNLOCK_OWNER=platform
elif [[ -z $commit && -z $verify ]]; then
UNLOCK_OWNER=limine
else
log_step "the platform implements only one of provision-commit and provision-verify"
return 1
fi
}
luks_auto_unlock_present() {
unlock_owner || return 0
if [[ $UNLOCK_OWNER == "platform" ]]; then
! "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-verify >>"$LOG_FILE" 2>&1
else
limine_auto_unlock_present
fi
}
luks_auto_unlock_drop() {
unlock_owner || return 1
if [[ $UNLOCK_OWNER == "platform" ]]; then
"$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-commit >>"$LOG_FILE" 2>&1
else
limine_auto_unlock_drop
fi
}
# The platform records the slot the re-key keeps, and no recovery slot; a no-op
# where nothing does.
luks_record_slots() {
"$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots "owner=$1" "recovery=" >>"$LOG_FILE" 2>&1
}
# Move the LUKS volume from the throwaway install passphrase to the user's # Move the LUKS volume from the throwaway install passphrase to the user's
# password and retire every other slot (throwaway + any seller keys a reset # password: add the user's key, kill every other slot (throwaway + any seller
# left behind). luks_rekey journals each step, so an interrupted attempt # keys a reset left behind), then rebuild the UKI without the embedded
# resumes where it stopped. # auto-unlock keyfile.
#
# Failing here must be LOUD (abort the attempt, offer retry): silently keeping
# the staged auto-unlock keyfile would leave the disk effectively unencrypted
# forever.
rekey_luks() { rekey_luks() {
luks_rekey_pending || return 0 [[ -f $PROVISIONING_DIR/luks-key ]] || return 0
local device local device
if ! device=$(luks_device) || [[ ! -e $device ]]; then if ! device=$(luks_device) || [[ ! -e $device ]]; then
@@ -1033,32 +869,61 @@ rekey_luks() {
return 1 return 1
fi fi
luks_rekey "$device" if ! cryptsetup open --test-passphrase --key-file "$PROVISIONING_DIR/luks-key" "$device" 2>>"$LOG_FILE"; then
} log_step "staged LUKS key does not unlock $device"
say --foreground 1 "The staged LUKS key no longer unlocks $device."
# The re-key's fingerprint of the console keyboard layout the boot files ask for return 1
# the disk password in: a hash of vconsole.conf's keymap and XKB settings, in
# file order since a later duplicate wins, so the journal never names the layout.
luks_boot_layout() {
if [[ ! -f /etc/vconsole.conf ]]; then
echo none
return
fi fi
awk '/^[ \t]*(KEYMAP|KEYMAP_TOGGLE|XKB[A-Z_]*)=/ { sub(/^[ \t]+/, ""); print }' /etc/vconsole.conf |
sha256sum | cut -d' ' -f1
}
# A re-key interrupted after the staged key was retired can only finish with # Add the user's key (a retry with a different password just adds another
# the password the disk already holds. # slot; all but the current one are killed once the rebuild succeeds).
rekey_accepts_password() { cryptsetup luksAddKey --key-file "$PROVISIONING_DIR/luks-key" "$device" <(printf '%s' "$password")
local -
set +x # Rebuild the no-auto-unlock UKI FIRST, keeping the throwaway key and slot as
local device # a fallback. Only once that succeeds do we kill the other slots and destroy
[[ -f $REKEY_STATE ]] || return 0 # the staged key — so a limine-update failure leaves a recoverable,
if ! device=$(luks_device) || [[ ! -e $device ]]; then # still-auto-unlocking state to retry, never a disk locked to a password the
return 0 # user may have just changed.
rm -f /etc/omarchy/provisioning.key \
/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf \
/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
reset_limine_config
if ! limine-update >>"$LOG_FILE" 2>&1; then
log_step "limine-update failed during re-key; restoring auto-unlock for retry"
install -Dm600 "$PROVISIONING_DIR/luks-key" /etc/omarchy/provisioning.key
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
echo 'FILES+=(/etc/omarchy/provisioning.key)' >/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
limine-update >>"$LOG_FILE" 2>&1 || true
return 1
fi fi
luks_rekey_accepts_password "$device"
local new_slot slot other_slots
new_slot=$(cryptsetup open --test-passphrase --verbose --key-file <(printf '%s' "$password") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' | head -1)
# Retiring the throwaway/seller slots must be all-or-nothing: if we can't
# identify the user's slot or a kill fails, keep the staged key and retry —
# never shred it while a slot the seller knows still unlocks the disk.
if [[ -z $new_slot ]]; then
log_step "could not identify the user's LUKS slot after re-key; keeping the staged key for retry"
say --foreground 1 "Could not confirm the LUKS re-key; will retry."
return 1
fi
if ! other_slots=$(cryptsetup luksDump "$device" | awk '/^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }'); then
log_step "luksDump failed while retiring slots; keeping the staged key for retry"
say --foreground 1 "Could not enumerate LUKS slots; will retry."
return 1
fi
for slot in $other_slots; do
[[ $slot == "$new_slot" ]] && continue
if ! cryptsetup luksKillSlot -q --key-file <(printf '%s' "$password") "$device" "$slot"; then
log_step "failed to kill LUKS slot $slot; keeping the staged key for retry"
say --foreground 1 "Could not remove the throwaway LUKS key; will retry."
return 1
fi
done
shred -u "$PROVISIONING_DIR/luks-key" 2>/dev/null || rm -f "$PROVISIONING_DIR/luks-key"
} }
# Start the ESP's limine.conf over from the shipped template and drop foreign # Start the ESP's limine.conf over from the shipped template and drop foreign
@@ -1125,7 +990,7 @@ reset_limine_config() {
cleanup_oem_state() { cleanup_oem_state() {
# Keep groups + packages: omarchy-system-factory-reset stages the bundled Node # Keep groups + packages: omarchy-system-factory-reset stages the bundled Node
# tarball from these live copies when the factory snapshot predates it. # tarball from these live copies when the factory snapshot predates it.
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" "$REKEY_STATE" rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user"
rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service
systemctl daemon-reload 2>/dev/null || true systemctl daemon-reload 2>/dev/null || true
@@ -1152,7 +1017,7 @@ run_provisioning() {
touch "$FINALIZE_WARNING_FLAG" touch "$FINALIZE_WARNING_FLAG"
fi fi
if luks_rekey_pending; then if [[ -f $PROVISIONING_DIR/luks-key ]]; then
log_step "re-keying LUKS to the user's password" log_step "re-keying LUKS to the user's password"
echo rekey >"$STATE_FILE" echo rekey >"$STATE_FILE"
rekey_luks rekey_luks
@@ -1169,51 +1034,16 @@ run_provisioning() {
limine-update limine-update
fi fi
if luks_staged_unlock_remains; then
log_step "refusing to finish setup while the staged LUKS key or its auto-unlock remains"
return 1
fi
if omarchy-pkg-present usbguard; then
log_step "enrolling the owner's USB devices"
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
usb_authorization_provision_owner "$username"
fi
log_step "enrolling the owner's Thunderbolt accessories"
/usr/bin/omarchy-thunderbolt-authorization-admin owner
log_step "cleaning up provisioning state" log_step "cleaning up provisioning state"
cleanup_oem_state cleanup_oem_state
log_step "first-boot setup complete" log_step "first-boot setup complete"
} }
# Where the platform's boot package owns the boot chain, it must be ready to
# finish setup, and able to record the kept key slot, before the owner is asked
# anything.
platform_ready() {
local error
if error=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-prepare 2>&1 >>"$LOG_FILE") &&
error=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve luks-slots 2>&1 >/dev/null); then
return 0
fi
error=${error:-"The boot package could not prepare this machine for setup."}
log_step "the boot package is not ready for setup: $error"
clear_logo
echo
say --foreground 1 "$error"
sleep 5
return 1
}
run_setup() { run_setup() {
local status local status
platform_ready || return 1
while true; do while true; do
keyboard_form || return 1 keyboard_form
user_form && status=0 || status=$? user_form && status=0 || status=$?
if ((status != 0)); then if ((status != 0)); then
# Esc unwinds to the keyboard step. Ctrl+C is the only other way out of a # Esc unwinds to the keyboard step. Ctrl+C is the only other way out of a
@@ -1221,12 +1051,7 @@ run_setup() {
((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot ((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot
continue continue
fi fi
confirm_form || continue confirm_form && break
if ! rekey_accepts_password; then
notice "Setup already set the disk password. Enter that same password." 4
continue
fi
break
done done
touch "$LOG_FILE" touch "$LOG_FILE"
+17 -15
View File
@@ -24,29 +24,31 @@ omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1 omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo omarchy_security_enable_no_update_sudo
channel="${1:-}" channel="${1:-stable}"
if [[ -n $channel && $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Invalid channel: $channel" >&2 echo "Invalid channel: $channel" >&2
exit 2 exit 2
fi fi
source "$OMARCHY_PATH/install/helpers/pacman.sh" # Render the channel templates plus this machine's regional additions as the
if ! platform=$(omarchy-hw-platform); then # caller, so root only copies finished files. The scratch directory goes on
echo "Cannot tell this machine's platform. No repository configuration changed." >&2 # every exit, including a failed render, a refused sudo prompt, or a signal;
exit 1 # revoking the sudo timestamp stays the last thing the exit path does.
fi rendered=$(mktemp -d)
templates=$(omarchy_pacman_templates "$platform") || exit 1 cleanup_refresh() {
[[ -n $channel ]] || channel=$(omarchy_pacman_default_channel "$platform") || exit 1 local status=$?
if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channel ]]; then rm -rf "$rendered"
echo "Omarchy has no $channel channel for $platform. No repository configuration changed." >&2 omarchy_security_exit_with_revoked_sudo "$status"
exit 1 }
fi trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel" echo "Setting channel to $channel"
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
rm -rf "$rendered"
# Allow user customization of /etc/pacman.conf before the upgrade runs, so # Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable # custom repositories and IgnorePkg entries shape the downgrade-capable
+6 -8
View File
@@ -1,20 +1,18 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Reinstall all default Omarchy packages from the stable channel, or on aarch64 the edge channel # omarchy:summary=Reinstall all default Omarchy packages from the stable channel
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
set -e set -e
# Reinstall all default Omarchy packages from the platform's default channel # Reinstall all default Omarchy packages from the stable channel and downgrade any packages that are too new.
# (stable, or edge on aarch64) and downgrade any packages that are too new.
# Set the package repository to the platform's default channel # Set the package repository to the stable mirrors
omarchy-refresh-pacman omarchy-refresh-pacman
# Downgrade any packages to that channel's setup # Downgrade any packages to the stable setup
omarchy-update-pacman -Suu --noconfirm omarchy-update-pacman -Suu --noconfirm
# Ensure this platform's default packages are installed # Ensure all packages are installed
default_packages=$(omarchy-pkg-defaults) mapfile -t packages < <(grep -v '^#' "$OMARCHY_PATH/install/omarchy-base.packages" | grep -v '^$')
mapfile -t packages <<<"$default_packages"
omarchy-update-pacman -Syu --noconfirm --needed "${packages[@]}" omarchy-update-pacman -Syu --noconfirm --needed "${packages[@]}"
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove Superwhisper dictation and its desktop integration
# omarchy:requires-sudo=true
set -e
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
systemctl --user disable --now superwhisper.service 2>/dev/null || true
omarchy-shell -q shell setPluginEnabled superwhisper.panel false >/dev/null
omarchy-pkg-drop superwhisper-bin
systemctl --user daemon-reload
# Remove the package setup's links, preserving independently installed copies
# and the user's preferences, history, and models.
unlink_package_link() {
if [[ -L $1 && $(readlink "$1") == "$2" ]]; then
rm "$1"
fi
}
unlink_package_link "$HOME/.local/bin/superwhisper" /usr/bin/superwhisper
unlink_package_link "$data_home/superwhisper/app/current" /opt/superwhisper
unlink_package_link "$config_home/omarchy/plugins/superwhisper-panel" /opt/superwhisper/assets/omarchy-plugin/superwhisper-panel
for agent in .agents .claude .codex .pi/agent; do
unlink_package_link "$HOME/$agent/skills/superwhisper" /opt/superwhisper/assets/agent-skill/superwhisper
done
backend_config="$config_home/omarchy/defaults/dictation"
if [[ -f $backend_config && $(< "$backend_config") == "superwhisper" ]]; then
rm "$backend_config"
fi
omarchy-shell -q shell rescanPlugins
hyprctl reload >/dev/null
if systemctl --user -q is-active omarchy-fcitx5.service; then
systemctl --user restart omarchy-fcitx5.service
fi
-1
View File
@@ -5,7 +5,6 @@
set -e set -e
omarchy-lifecycle-dispatch pre-remove steam
omarchy-pkg-drop steam omarchy-pkg-drop steam
rm -rf \ rm -rf \
@@ -1,10 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore Bolt's default authorization behavior
set -euo pipefail
gum confirm "Restore Bolt's previous policy? New accessories may be authorized automatically when IOMMU protection is active." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin disable
systemctl --user disable --now omarchy-thunderbolt-authorization.service
echo "Previous Bolt policy restored. Saved Omarchy trust is retained for re-enabling."
@@ -1,9 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore saved Thunderbolt firmware boot permissions
set -euo pipefail
gum confirm "Restore the previous Thunderbolt firmware boot allowlists? Device approval will remain enabled." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-disable
echo "Previous firmware boot permissions restored and verified."
@@ -1,89 +0,0 @@
#!/bin/bash
# omarchy:summary=Stop asking before new USB devices can access the system
# omarchy:args=[--boot-only] [--yes]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
boot_only=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot-only) boot_only=1 ;;
-h | --help)
echo "Usage: omarchy-remove-security-usb-authorization [--boot-only] [--yes]"
echo
echo "Disables Omarchy's USB approval prompts and restores the kernel's"
echo "normal default-allow behavior."
echo "Use --boot-only to keep USBGuard enabled after the system starts."
exit 0
;;
*)
echo "omarchy-remove-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
if (( boot_only )); then
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB authorization from boot"
echo
echo "USBGuard will continue blocking unknown devices after its service starts."
echo "Devices can reach kernel drivers earlier during boot."
echo
gum confirm "Disable USB authorization from kernel boot?" || exit 130
fi
sudo omarchy-usb-authorization-boot disable
echo
echo "Boot-time USB authorization is disabled and takes effect after reboot."
exit 0
fi
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB device authorization"
echo
echo "New USB devices will be allowed to bind to kernel drivers without approval."
echo
gum confirm "Disable USB device authorization?" || exit 130
fi
# Rebuild a permissive boot image before stopping USBGuard. If rebuilding
# fails, the command stops here and leaves the complete policy enabled.
sudo omarchy-usb-authorization-boot disable
if omarchy-pkg-present usbguard; then
# Keep the watcher and its IPC access until blocking has actually stopped.
# In particular, a failed stop must not strand a running daemon without UI.
sudo systemctl disable --now usbguard.service
if systemctl is-active --quiet usbguard.service || systemctl is-enabled --quiet usbguard.service; then
echo "USBGuard is still active or enabled; approval prompts have been preserved." >&2
exit 1
fi
sudo omarchy-usb-authorization-restore-default
sudo usbguard remove-user "$USER"
fi
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
rm -f "$unit"
systemctl --user daemon-reload
if [[ -d $state_dir/requests ]]; then
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
rmdir "$state_dir/requests" 2>/dev/null || true
fi
rm -f "$state_dir/package-installed-by-omarchy" \
"$state_dir/policy-generated-by-omarchy" \
"$state_dir/service-enabled-by-omarchy"
rmdir "$state_dir" 2>/dev/null || true
echo "USB device authorization is disabled."
+1 -22
View File
@@ -2,30 +2,9 @@
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings. # omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
set -euo pipefail
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's # A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
# instance would exit on arrival and the stale process would keep serving the # instance would exit on arrival and the stale process would keep serving the
# old ~/.XCompose -- a restart that reports success and changes nothing. # old ~/.XCompose -- a restart that reports success and changes nothing.
systemctl --user stop omarchy-fcitx5.service systemctl --user stop omarchy-fcitx5.service
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true pkill -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then
break
fi
sleep 0.1
done
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
# An activation concurrent with stop may have restarted the unit.
systemctl --user stop omarchy-fcitx5.service
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then break; fi
sleep 0.1
done
fi
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
echo "Fcitx did not stop; input service was not restarted." >&2
exit 1
fi
systemctl --user start omarchy-fcitx5.service systemctl --user start omarchy-fcitx5.service
+3 -8
View File
@@ -10,18 +10,13 @@ fi
omarchy_security_require_privileged_bash_startup() { omarchy_security_require_privileged_bash_startup() {
[[ $- == *p* ]] || return 1 [[ $- == *p* ]] || return 1
# The caller is this check's parent, under the number this /proc gives it.
# arch-chroot runs the installer in a new PID namespace beneath the outer
# /proc, where $$ names no process, or someone else's.
/usr/bin/env -i /usr/bin/bash -p -c ' /usr/bin/env -i /usr/bin/bash -p -c '
stat=$(</proc/self/stat) || exit 1 mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
read -r _ caller _ <<<"${stat##*) }" executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
mapfile -d "" -t argv <"/proc/$caller/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$caller/exe") || exit 1
[[ $executable == "/usr/bin/bash" && [[ $executable == "/usr/bin/bash" &&
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) && ( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
${argv[1]:-} == "-p" ]] ${argv[1]:-} == "-p" ]]
' omarchy-bash-startup ' omarchy-bash-startup "$$"
} }
omarchy_security_sanitize_bash_environment() { omarchy_security_sanitize_bash_environment() {
-8
View File
@@ -3,14 +3,6 @@
# omarchy:summary=Add or remove an EFI boot entry for the Omarchy UKI, allowing the system to boot directly # omarchy:summary=Add or remove an EFI boot entry for the Omarchy UKI, allowing the system to boot directly
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
# Apple Silicon boots m1n1, then U-Boot, which finds the bootloader on its own
# and creates its boot entries itself, so there is no direct-boot entry to add
# or remove.
if omarchy-hw-aarch64-apple; then
echo "Error: Direct boot is not supported on Apple Silicon: U-Boot finds the bootloader itself" >&2
exit 1
fi
if [[ ! -d /sys/firmware/efi ]]; then if [[ ! -d /sys/firmware/efi ]]; then
echo "Error: System is not booted in UEFI mode" >&2 echo "Error: System is not booted in UEFI mode" >&2
exit 1 exit 1
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# omarchy:summary=Select input methods for Japanese, Korean, or Chinese typing
# omarchy:args=[mozc|hangul|pinyin|chewing]
# omarchy:examples=omarchy setup input | omarchy setup input mozc
# omarchy:requires-sudo=true
set -euo pipefail
if (( $# > 1 )); then
echo "Usage: omarchy setup input [mozc|hangul|pinyin|chewing]" >&2
exit 1
fi
method=${1:-}
if [[ -z $method ]]; then
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" input
exit
fi
case "$method" in
mozc | hangul | pinyin | chewing) ;;
*) echo "Unknown input method: $method" >&2; exit 1 ;;
esac
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
omarchy-pkg-add "${packages[@]}"
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" add "$method"
echo "Input method added. Use Super+I or the Japanese/Korean input key to switch; existing custom Fcitx keys are preserved."
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# omarchy:summary=Select the keyboard layouts available for switching
set -euo pipefail
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" keyboard
@@ -1,42 +0,0 @@
#!/bin/bash
# omarchy:summary=Require approval for new Thunderbolt PCIe accessories
# omarchy:args=[--quiet|status|recover]
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
if [[ ${1:-} == "status" ]]; then
tb_status
elif [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin recover
elif [[ $# == 0 || ( $# == 1 && $1 == "--quiet" ) ]]; then
if [[ ${1:-} != "--quiet" ]]; then
if [[ -f $TB_PENDING ]]; then tb_status; fi
echo "First setup, including a deferred setup, trusts connected accessories. Re-enabling a completed setup preserves saved trust."
gum confirm "Require approval for new Thunderbolt accessories?" || exit 0
fi
# System units and rules are shipped by omarchy-settings.
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
sudo systemctl daemon-reload
action=enable
if [[ ${1:-} == "--quiet" ]]; then
action=migrate
fi
sudo /usr/bin/omarchy-thunderbolt-authorization-admin "$action"
systemctl --user daemon-reload
systemctl --user enable --now omarchy-thunderbolt-authorization.service
if [[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]; then
for ((attempt=0; attempt<10; attempt++)); do
if tb_public_snapshot >/dev/null 2>&1; then break; fi
sleep 1
done
tb_status
elif [[ -f $TB_PENDING ]]; then
tb_status
fi
else
echo "Usage: omarchy-setup-security-thunderbolt-authorization [--quiet|status|recover]" >&2
exit 1
fi
@@ -1,23 +0,0 @@
#!/bin/bash
# omarchy:summary=Clear supported Thunderbolt firmware boot allowlists
# omarchy:args=[recover]
set -euo pipefail
if [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-recover
elif [[ $# == 0 ]]; then
gum style --bold "Thunderbolt at Boot"
echo "This trusts connected accessories for use after Linux starts, then clears the firmware boot allowlist."
echo "Trusted Thunderbolt keyboards, storage, and network adapters may be unavailable at disk unlock."
echo "You must be able to unlock and recover without Thunderbolt accessories."
echo "All controllers must be connected and support a boot allowlist in user or secure mode."
echo "Older snapshots or another operating system may restore firmware boot permissions."
gum confirm "Clear firmware boot access with these recovery limits?" || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-enable
echo "Firmware boot allowlists cleared and verified. Current connections were not disconnected."
else
echo "Usage: omarchy-setup-security-thunderbolt-boot [recover]" >&2
exit 1
fi
@@ -1,191 +0,0 @@
#!/bin/bash
# omarchy:summary=Ask before new USB devices can access the system
# omarchy:args=[--boot] [--yes] [--review-existing]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
from_boot=0
review_existing=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot) from_boot=1 ;;
--review-existing) review_existing=1 ;;
-h | --help)
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes] [--review-existing]"
echo
echo "Trusts USB devices that are connected now, then blocks new devices"
echo "until you approve them from an Omarchy notification."
echo
echo "--boot also blocks USB devices from the start of kernel boot. Connected"
echo "devices are added to USBGuard's policy, but remain unavailable before"
echo "USBGuard starts, including at an encrypted-disk password prompt."
echo "Older snapshots without USBGuard keep all USB disabled after startup,"
echo "including keyboards, network adapters, and storage. Use --boot only if"
echo "disk unlock and recovery work without any USB devices."
echo
echo "--review-existing reviews connected devices with older port-bound approvals."
echo "Each portable approval requires confirmation; existing rules are preserved."
exit 0
;;
*)
echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
# Boot-time blocking goes into Limine's own configuration, which a Mac's boot
# package owns, so it is not offered there.
if (( from_boot )) && omarchy-hw-aarch64-apple; then
echo "USB authorization from boot is not available on Apple Silicon yet; USB device authorization after login still works." >&2
exit 2
fi
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
rules_file=/etc/usbguard/rules.conf
daemon_config=/etc/usbguard/usbguard-daemon.conf
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
if (( review_existing )); then
# Older Omarchy rules have no per-rule ownership marker. They cannot safely
# be distinguished from intentionally restricted administrator rules.
(( ! assume_yes && ! from_boot )) || { echo "Review requires individual confirmation; do not combine it with --yes or --boot." >&2; exit 2; }
policy=$(usbguard list-rules)
declare -A reviewed=()
# Keep stdin attached to the terminal for gum (and any sudo prompt).
mapfile -t policy_lines <<<"$policy"
for line in "${policy_lines[@]}"; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
rule=${line#*: }
[[ $rule == allow\ * ]] || continue
portable=$(usb_authorization_portable_rule "$rule") || continue
[[ ${portable% label *} != "$rule" ]] || continue
devices=$(usbguard list-devices "$rule")
mapfile -t candidates <<<"$devices"
for device in "${candidates[@]}"; do
[[ $device =~ ^([0-9]+):[[:space:]](allow[[:space:]].*)$ ]] || continue
id=${BASH_REMATCH[1]} current=${BASH_REMATCH[2]}
[[ ! -v reviewed[$id] ]] || continue
reviewed[$id]=1
echo
printf '%s\n' "$current" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo "This device has a location-bound approval. Details are supplied by the device."
if gum confirm "Trust this device on any USB port or hub?"; then
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "$current"
fi
done
fi
done
echo "Review complete. Disconnected devices can be approved when reconnected."
exit 0
fi
# The watcher runs in the session's environment, which may differ from a
# command invoked in a checkout. Fail before enabling blocking on mismatched
# or incomplete installations, including dangling dev-link unit symlinks.
session_environment=$(systemctl --user show-environment)
grep -Fqx -- "OMARCHY_PATH=$OMARCHY_PATH" <<<"$session_environment" || {
echo "The session and this command use different Omarchy installations. Update the session before enabling USB protection." >&2
exit 1
}
for required in "$unit_source" /usr/share/omarchy/install/helpers/usb-authorization-policy.sh; do
[[ -r $required ]] || { echo "USB authorization installation is incomplete: $required" >&2; exit 1; }
done
for required in "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-review" \
/usr/bin/omarchy-usb-authorization-approve; do
[[ -r $required && -x $required ]] || { echo "USB authorization command is unavailable: $required" >&2; exit 1; }
done
if (( ! assume_yes )); then
if (( from_boot )); then
gum style --foreground 212 --bold "USB authorization from boot"
else
gum style --foreground 212 --bold "USB device authorization"
fi
echo
echo "Devices connected now will be trusted. Disconnect anything you do not trust."
echo "New USB devices will remain blocked until you approve them."
if (( from_boot )); then
echo
echo "Boot-time protection blocks all USB devices until USBGuard starts."
echo "A USB keyboard will not work at an encrypted-disk password prompt, even"
echo "when it is connected now."
echo
echo "Older snapshots without USBGuard keep ALL USB disabled after startup,"
echo "including keyboards, network adapters, and storage."
echo "Continue only if disk unlock and recovery work without any USB devices."
fi
echo
if (( from_boot )); then
gum confirm "Enable USB authorization from kernel boot?" || exit 130
else
gum confirm "Enable USB device authorization?" || exit 130
fi
fi
omarchy-pkg-add usbguard
usb_authorization_require_secure_settings "$daemon_config"
mkdir -p "$state_dir" "${unit%/*}"
chmod 700 "$state_dir"
if ! usb_authorization_rules_present "$rules_file"; then
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX")
trap 'rm -f "$policy"' EXIT
chmod 600 "$policy"
usb_authorization_generate_policy "$policy"
sudo install -Dm600 -o root -g root "$policy" "$rules_file"
touch "$state_dir/policy-generated-by-omarchy"
fi
# This user can inspect USBGuard events and approve the device named by an
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
sudo usbguard add-user "$USER" \
--devices=list,listen,modify \
--policy=list \
--exceptions=listen
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# Install a real file: a link into a checkout can become dangling on a branch
# switch. Start the waiting watcher before USBGuard can block new devices.
unit_temporary=$(mktemp "${unit}.XXXXXXXXXX")
install -m644 "$unit_source" "$unit_temporary"
mv -fT "$unit_temporary" "$unit"
systemctl --user daemon-reload
systemctl --user enable omarchy-usb-authorization.service
systemctl --user restart omarchy-usb-authorization.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
sudo systemctl enable usbguard.service
# add-user ACL changes are loaded only when the daemon starts, so restart an
# existing service as well as starting a fresh one.
sudo systemctl restart usbguard.service
systemctl is-active --quiet usbguard.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
if (( from_boot )); then
# The fixed helper independently verifies each full live device snapshot.
usb_authorization_trust_present_devices
sudo omarchy-usb-authorization-boot enable
fi
echo
echo "USB device authorization is enabled."
echo "Devices connected during setup remain trusted; new devices require approval."
echo "To review older port-bound approvals, run: omarchy-setup-security-usb-authorization --review-existing"
if (( from_boot )); then
echo "Boot-time authorization is enabled and takes effect after reboot."
fi
+47 -194
View File
@@ -1,7 +1,7 @@
#!/bin/bash -p #!/bin/bash -p
# omarchy:summary=Toggle passwordless sudo for the current user. # omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES|permanent|--active|--disable] # omarchy:args=[MINUTES]
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
@@ -22,6 +22,7 @@ fi
set -euo pipefail set -euo pipefail
readonly DEFAULT_MINUTES=15
readonly MAX_MINUTES=1440 readonly MAX_MINUTES=1440
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
@@ -33,7 +34,7 @@ readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3 readonly STATUS_INACTIVE=3
usage() { usage() {
echo "Usage: omarchy-sudo-passwordless [MINUTES|permanent|--active|--disable]" >&2 echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2 echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
exit 1 exit 1
} }
@@ -42,10 +43,6 @@ valid_minutes() {
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES)) [[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
} }
valid_duration() {
[[ $1 == "permanent" ]] || valid_minutes "$1"
}
valid_uid() { valid_uid() {
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294)) [[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
} }
@@ -99,39 +96,14 @@ with_root_lock() {
} }
rule_file() { rule_file() {
local uid=$((10#$1)) printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
if [[ ${2:-} == "permanent" ]]; then
printf '/etc/sudoers.d/99-omarchy-permanent-nopasswd-%s' "$uid"
else
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$uid"
fi
} }
# The sudoers rule is the only grant record. A missing file is distinct from # The sudoers rule is the only grant record. A missing file is distinct from
# an unreadable, unsafe, or administrator-modified file. # an unreadable, unsafe, or administrator-modified file.
read_grant() { read_grant() {
local file permanent contents local file contents
file=$(rule_file "$1") file=$(rule_file "$1")
permanent=$(rule_file "$1" permanent)
if [[ -e $permanent || -L $permanent ]]; then
verify_root_path "$permanent" && [[ -f $permanent ]] || return 2
contents=$(/usr/bin/cat -- "$permanent") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOPASSWD:\ ALL$ ]] || return 2
GRANT_NAME=${BASH_REMATCH[1]}
GRANT_DEADLINE=""
valid_account_name "$GRANT_NAME" || return 2
if [[ -e $file || -L $file ]]; then
# A SIGKILL between publication and removal can leave both policies.
# Only accept a matching generated timed rule. Permanent access already
# exists, so old expiry callbacks must not revoke it; disable removes
# both files, and the next duration change finishes their replacement.
verify_root_path "$file" && [[ -f $file ]] || return 2
contents=$(/usr/bin/cat -- "$file") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOTAFTER=([0-9]{14}Z)\ NOPASSWD:\ ALL$ ]] || return 2
[[ ${BASH_REMATCH[1]} == "$GRANT_NAME" ]] || return 2
fi
return 0
fi
[[ -e $file || -L $file ]] || return "$STATUS_INACTIVE" [[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
verify_root_path "$file" && [[ -f $file ]] || return 2 verify_root_path "$file" && [[ -f $file ]] || return 2
contents=$(/usr/bin/cat -- "$file") || return 2 contents=$(/usr/bin/cat -- "$file") || return 2
@@ -149,9 +121,6 @@ classify_generated_rule() {
[[ -f $file && ! -L $file ]] || return 1 [[ -f $file && ! -L $file ]] || return 1
contents=$(/usr/bin/cat -- "$file") || return 2 contents=$(/usr/bin/cat -- "$file") || return 2
suffix=${file##*/99-omarchy-nopasswd-} suffix=${file##*/99-omarchy-nopasswd-}
if [[ $file == */99-omarchy-permanent-nopasswd-* ]]; then
suffix=${file##*/99-omarchy-permanent-nopasswd-}
fi
# The legacy command wrote the caller's unvalidated name into both the # The legacy command wrote the caller's unvalidated name into both the
# filename and the rule. That exact relationship is its fingerprint, so an # filename and the rule. That exact relationship is its fingerprint, so an
@@ -169,21 +138,14 @@ classify_generated_rule() {
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]] valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
} }
cleanup_rule_file() { cleanup_uid_locked() {
local file=$1 local file
file=$(rule_file "$1")
[[ -e $file || -L $file ]] || return 0 [[ -e $file || -L $file ]] || return 0
verify_root_path "$file" && classify_generated_rule "$file" || return 1 verify_root_path "$file" && classify_generated_rule "$file" || return 1
/usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]] /usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
} }
cleanup_uid_locked() {
local file duration
for duration in temporary permanent; do
file=$(rule_file "$1" "$duration")
cleanup_rule_file "$file" || return 1
done
}
# The generated prefix is reserved: boot cleanup and the package hook already # The generated prefix is reserved: boot cleanup and the package hook already
# remove everything in it, and the legacy writer could produce a rule whose # remove everything in it, and the legacy writer could produce a rule whose
# body differs from its filename. Nothing unrecognized may stay live there, but # body differs from its filename. Nothing unrecognized may stay live there, but
@@ -301,7 +263,6 @@ migrate_locked() {
expire_locked() { expire_locked() {
local status now local status now
if read_grant "$1"; then if read_grant "$1"; then
[[ -n $GRANT_DEADLINE ]] || return 0
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2 now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
[[ $now < $GRANT_DEADLINE ]] && return 0 [[ $now < $GRANT_DEADLINE ]] && return 0
cleanup_uid_locked "$1" cleanup_uid_locked "$1"
@@ -320,7 +281,6 @@ status_locked() {
resolve_account "$1" || return 2 resolve_account "$1" || return 2
if read_grant "$1"; then if read_grant "$1"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2 [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
[[ -n $GRANT_DEADLINE ]] || return 0
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2 now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
if [[ $now < $GRANT_DEADLINE ]]; then if [[ $now < $GRANT_DEADLINE ]]; then
return 0 return 0
@@ -340,11 +300,7 @@ finish_enable() {
if cleanup_uid_locked "$uid"; then if cleanup_uid_locked "$uid"; then
[[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true [[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
else else
if [[ -n $timer ]]; then echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
else
echo "Could not revoke passwordless sudo. Administrator cleanup is required." >&2
fi
fi fi
fi fi
[[ -z $pending ]] || /usr/bin/rm -f -- "$pending" [[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
@@ -353,15 +309,9 @@ finish_enable() {
enable_locked() ( enable_locked() (
local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
resolve_account "$uid" && valid_duration "$minutes" || return 1 resolve_account "$uid" && valid_minutes "$minutes" || return 1
verify_root_path /etc/sudoers.d || return 1 verify_boot_cleanup && verify_root_path /etc/sudoers.d || return 1
if [[ $minutes != "permanent" ]]; then file=$(rule_file "$uid")
verify_boot_cleanup || {
echo "Temporary sudo cleanup is unavailable or a settings package transaction is incomplete. Check the omarchy-settings installation." >&2
return 1
}
fi
file=$(rule_file "$uid" "$minutes")
if read_grant "$uid"; then if read_grant "$uid"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1 [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
else else
@@ -370,69 +320,39 @@ enable_locked() (
fi fi
trap finish_enable EXIT trap finish_enable EXIT
omarchy_security_install_signal_exit_traps omarchy_security_install_signal_exit_traps
now=$(/usr/bin/date +%s) || return 1
expires=$((now + 10#$minutes * 60))
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1 pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
if [[ $minutes == "permanent" ]]; then /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
/usr/bin/printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$ACCOUNT_NAME" >"$pending" || return 1
else
now=$(/usr/bin/date +%s) || return 1
expires=$((now + 10#$minutes * 60))
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
fi
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1 /usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1 /usr/sbin/visudo -cf "$pending" >/dev/null || return 1
if [[ $minutes != "permanent" ]]; then token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1 [[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1 timer="omarchy-nopasswd-expire-$uid-$token"
timer="omarchy-nopasswd-expire-$uid-$token" /usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \
/usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \ --timer-property=AccuracySec=1s --unit="$timer" \
--timer-property=AccuracySec=1s --unit="$timer" \ -- "$INSTALLED_SELF" __expire "$uid" || return 1
-- "$INSTALLED_SELF" __expire "$uid" || return 1 /usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
/usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
fi
# The temporary filename contains a dot, so sudo ignores it. Rename within # The temporary filename contains a dot, so sudo ignores it. Rename within
# sudoers.d publishes the complete validated policy in one operation. # sudoers.d publishes the complete validated policy in one operation.
/usr/bin/mv -fT -- "$pending" "$file" || return 1 /usr/bin/mv -fT -- "$pending" "$file" || return 1
pending="" pending=""
# Keep access uninterrupted during renewal. When changing duration type, now=$(/usr/bin/date +%s) || return 1
# remove the other rule only after its replacement is published. The lock (( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
# keeps status and expiry callbacks from observing both rules together.
if [[ $minutes == "permanent" ]]; then
cleanup_rule_file "$(rule_file "$uid")" || return 1
else
cleanup_rule_file "$(rule_file "$uid" permanent)" || return 1
fi
if [[ $minutes != "permanent" ]]; then
now=$(/usr/bin/date +%s) || return 1
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
fi
) )
root_dispatch() { root_dispatch() {
local action="$1" status local action="$1"
shift shift
case "$action" in case "$action" in
__status) __status)
(($# == 1)) && verify_sudo_caller "$1" || return 2 (($# == 1)) && verify_sudo_caller "$1" || return 2
if with_root_lock status_locked "$1"; then with_root_lock status_locked "$1"
return 0
else
status=$?
# Reserve status 1 for sudo refusing noninteractive authentication.
# Every internal inspection failure, including lock failures, is 2.
(( status == STATUS_INACTIVE )) || status=2
return "$status"
fi
;; ;;
__enable) __enable)
(($# == 2)) && verify_sudo_caller "$1" && valid_duration "$2" || return 1 (($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
if with_root_lock enable_locked "$1" "$2"; then with_root_lock enable_locked "$1" "$2"
return 0
else
status=$?
echo "Could not enable passwordless sudo. Check existing sudoers policy and cleanup support." >&2
return "$status"
fi
;; ;;
__disable) __disable)
(($# == 1)) && verify_sudo_caller "$1" || return 1 (($# == 1)) && verify_sudo_caller "$1" || return 1
@@ -462,14 +382,6 @@ root_dispatch() {
esac esac
} }
show_enabled() {
if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
fi
}
case "${1:-}" in case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete) __status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
action=$1 action=$1
@@ -479,30 +391,9 @@ case "${1:-}" in
;; ;;
esac esac
# Prefer matched policy tags to executing a privileged command, avoiding
# authentication/session logs under the default listing policy. Restrictive
# listpw settings require the root-status fallback. Neither path prompts or
# trusts cached credentials as evidence of passwordless access.
if [[ ${1:-} == "--active" ]]; then
(($# == 1)) || usage
uid=$(/usr/bin/id -u)
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
/usr/bin/grep -q '!authenticate' <<<"$policy"
else
# listpw=always can require authentication to list a passwordless grant.
# With a command, -k ignores cached credentials without invalidating them.
status=0
/usr/bin/sudo -kn -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
# An internal inspection error still proves that sudo ran the helper
# without authentication. Keep the warning visible for unsafe policy.
(( status == 0 || status == 2 )) || exit 1
fi
exit
fi
(($# <= 1)) || usage (($# <= 1)) || usage
minutes=${1:-} minutes=${1:-$DEFAULT_MINUTES}
(($# == 0)) || [[ $minutes == "--disable" ]] || valid_duration "$minutes" || usage valid_minutes "$minutes" || usage
uid=$(/usr/bin/id -u) uid=$(/usr/bin/id -u)
valid_uid "$uid" || { valid_uid "$uid" || {
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2 echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
@@ -520,76 +411,38 @@ omarchy_security_install_sudo_cleanup_traps
exit 1 exit 1
} }
# Inspect policy without authenticating. Only inspect the root-owned grant echo "Toggle passwordless sudo..."
# when policy already permits it without a password; otherwise the eventual if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
# enable action is the sole authentication boundary. enable_locked validates if (($# == 0)); then
# any existing policy before publishing, including expired or modified rules. /usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
status=$STATUS_INACTIVE echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then else
if /usr/bin/grep -q '!authenticate' <<<"$policy"; then /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
status=0 echo "Passwordless sudo expiry updated. It will automatically disable in ${minutes} minutes."
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" || status=$?
fi fi
else else
# A listing password is independent of command authorization. An active status=$?
# grant can still be inspected/revoked without prompting under listpw=always. if (( status != STATUS_INACTIVE )); then
status=0
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
if (( status == 1 )) && [[ $minutes != "--disable" ]]; then
# No noninteractive inspection is available. Offer the confirmed enable
# flow; its single authenticated action validates existing policy itself.
status=$STATUS_INACTIVE
fi
fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
show_enabled
else
if (( status != 0 && status != STATUS_INACTIVE )); then
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2 echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
exit 1 exit 1
fi fi
if [[ -z $minutes ]]; then
choice=$(/usr/bin/gum choose --header "How long should passwordless sudo stay enabled?" "15 minutes" "1 Hour" "1 Day" "Permanently") || exit 130
case "$choice" in
"15 minutes") minutes=15 ;;
"1 Hour") minutes=60 ;;
"1 Day") minutes=1440 ;;
"Permanently") minutes=permanent ;;
*) exit 130 ;;
esac
fi
if [[ $minutes == "permanent" ]]; then
duration="permanently"
else
duration="for ${minutes} minutes"
fi
echo "" echo ""
echo "⚠️ WARNING: This will allow ANY process running as your user to" echo "⚠️ WARNING: This will allow ANY process running as your user to"
echo "execute ANY command as root WITHOUT a password $duration." echo "execute ANY command as root WITHOUT a password for ${minutes} minutes."
echo "" echo ""
echo "This is useful for AI agents that need to run sudo commands," echo "This is useful for AI agents that need to run sudo commands,"
echo "but it significantly weakens the security of your system." echo "but it significantly weakens the security of your system."
echo "Anyone or anything with access to your user account gets full root." echo "Anyone or anything with access to your user account gets full root."
echo "" echo ""
if [[ $minutes == "permanent" ]]; then echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "Passwordless sudo will remain enabled across reboots until you disable it." echo "including if the machine reboots before the deadline."
else echo "Run this command again to disable it early."
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "including if the machine reboots before the deadline."
fi
echo "Run this command again to disable it."
echo "" echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then if /usr/bin/gum confirm "Enable passwordless sudo for ${minutes} minutes? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes" /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo "" echo ""
show_enabled echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
else else
echo "Aborted. No changes made." echo "Aborted. No changes made."
fi fi
+7 -195
View File
@@ -17,9 +17,7 @@
# The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged # The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged
# here. On encrypted installs the LUKS volume is re-keyed to a throwaway # here. On encrypted installs the LUKS volume is re-keyed to a throwaway
# passphrase with an auto-unlock keyfile for the provisioning window; first-boot setup # passphrase with an auto-unlock keyfile for the provisioning window; first-boot setup
# re-keys it to the new owner's password and removes the keyfile. A platform # re-keys it to the new owner's password and removes the keyfile.
# whose boot chain the Limine UKI path can't drive rebuilds, verifies and
# unlocks it through omarchy-lifecycle-dispatch.
# #
# Honest limitations: # Honest limitations:
# - On unencrypted disks this is deletion, not forensic erasure. fstrim helps # - On unencrypted disks this is deletion, not forensic erasure. fstrim helps
@@ -31,25 +29,17 @@
set -euo pipefail set -euo pipefail
PROVISIONING_DIR=/var/lib/omarchy/provisioning PROVISIONING_DIR=/var/lib/omarchy/provisioning
PACKAGED_PATH=/usr/bin/omarchy-system-factory-reset
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
TOP_MNT=/run/omarchy-system-factory-reset/top TOP_MNT=/run/omarchy-system-factory-reset/top
NEXT_NAME=@omarchy-reset-next NEXT_NAME=@omarchy-reset-next
LOG_FILE=/var/log/omarchy-system-factory-reset.log LOG_FILE=/var/log/omarchy-system-factory-reset.log
# Never replace a newer checkout reset with a different installed implementation.
if ! /usr/bin/cmp -s -- "${BASH_SOURCE[0]}" "$PACKAGED_PATH"; then
echo "Factory reset differs from the installed command. Install the matching Omarchy package before resetting." >&2
exit 1
fi
# Self-elevate so the menu entry (and a bare shell invocation) need no sudo # Self-elevate so the menu entry (and a bare shell invocation) need no sudo
# prefix. The caller's gum theme vars are forwarded as `env` arguments rather # prefix. The caller's gum theme vars are forwarded as `env` arguments rather
# than via `sudo -E`, so styling survives even under an env_reset sudoers. # than via `sudo -E`, so styling survives even under an env_reset sudoers.
# Always elevate the packaged copy, including when invoked from a dev checkout.
if (( EUID != 0 )); then if (( EUID != 0 )); then
mapfile -t gum_env < <(env | grep '^GUM_' || true) mapfile -t gum_env < <(env | grep '^GUM_' || true)
exec sudo env "${gum_env[@]}" "$PACKAGED_PATH" "$@" exec sudo env "${gum_env[@]}" "$0" "$@"
fi fi
export PATH="$OMARCHY_PATH/bin:$PATH" export PATH="$OMARCHY_PATH/bin:$PATH"
@@ -72,51 +62,6 @@ root_is_btrfs_at() {
[[ $(findmnt -no FSTYPE /) == btrfs ]] && findmnt -no OPTIONS / | grep -q "subvol=/@\(,\|$\)" [[ $(findmnt -no FSTYPE /) == btrfs ]] && findmnt -no OPTIONS / | grep -q "subvol=/@\(,\|$\)"
} }
DISPATCH="$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch"
# A platform whose boot package implements the reset operations rebuilds,
# verifies and unlocks the factory root's boot chain itself; everywhere else
# the Limine UKI path below does. Deciding fails closed.
RESET_BOOT=""
reset_boot_owner() {
[[ -z $RESET_BOOT ]] || return 0
local operation path found=0 missing=0
for operation in reset-prepare reset-verify reset-commit reset-rollback; do
path=$("$DISPATCH" --resolve "$operation") || return 1
if [[ -n $path ]]; then
found=$((found + 1))
else
missing=$((missing + 1))
fi
done
if (( missing == 0 )); then
RESET_BOOT=platform
elif (( found == 0 )); then
RESET_BOOT=generic
else
echo "Error: the boot package implements only some of the factory reset operations" >&2
return 1
fi
}
# Runs a reset operation with its output in the log. A failure keeps the
# operation's last line for the error shown.
RESET_BOOT_ERROR=""
reset_boot_op() {
local output status=0
output=$("$DISPATCH" "$@" 2>&1) || status=$?
[[ -z $output ]] || printf '%s\n' "$output" >>"$LOG_FILE"
if (( status == 0 )); then
return 0
fi
RESET_BOOT_ERROR=$(tail -n 1 <<<"$output")
RESET_BOOT_ERROR=${RESET_BOOT_ERROR:-"$1 failed"}
return 1
}
# Resolve the crypto_LUKS partition backing the root, or return non-zero if # Resolve the crypto_LUKS partition backing the root, or return non-zero if
# the root is not on LUKS. Prefers the cmdline cryptdevice= spec (archinstall # the root is not on LUKS. Prefers the cmdline cryptdevice= spec (archinstall
# writes PARTUUID=, the pre-mounted path UUID=), and falls back to walking the # writes PARTUUID=, the pre-mounted path UUID=), and falls back to walking the
@@ -136,7 +81,7 @@ luks_device() {
local src part local src part
src=$(findmnt -no SOURCE / | sed 's/\[.*//') src=$(findmnt -no SOURCE / | sed 's/\[.*//')
[[ -n $src ]] || return 1 [[ -n $src ]] || return 1
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}') part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
[[ -n $part ]] && { echo "$part"; return 0; } [[ -n $part ]] && { echo "$part"; return 0; }
return 1 return 1
} }
@@ -154,11 +99,6 @@ generate_passphrase() {
} }
cleanup() { cleanup() {
# Before the switch the old root still boots: the boot package gives it back
# its boot state before the reset's throwaway slot is revoked.
if [[ ${reset_committed:-0} == 0 && ${RESET_BOOT_PREPARED:-0} == 1 ]]; then
reset_boot_op reset-rollback || echo "Error: $RESET_BOOT_ERROR (see $LOG_FILE)" >&2
fi
if mountpoint -q "$TOP_MNT" 2>/dev/null; then if mountpoint -q "$TOP_MNT" 2>/dev/null; then
if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then
btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true
@@ -166,9 +106,6 @@ cleanup() {
umount -R "$TOP_MNT" 2>/dev/null || true umount -R "$TOP_MNT" 2>/dev/null || true
fi fi
rmdir "$TOP_MNT" 2>/dev/null || true rmdir "$TOP_MNT" 2>/dev/null || true
if [[ ${reset_committed:-0} == 0 ]]; then
revoke_reset_luks
fi
} }
trap cleanup EXIT trap cleanup EXIT
@@ -190,54 +127,8 @@ confirm_reset() {
[[ $typed == "reset" ]] || fail "Reset not confirmed." [[ $typed == "reset" ]] || fail "Reset not confirmed."
} }
# The slot the reset's throwaway key opens, or nothing. Restricting cryptsetup
# to a token type no token has keeps an enrolled token from answering for it.
reset_luks_slot() {
local -
set +x
LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only \
--key-file <(printf '%s' "$RESET_THROWAWAY") "$RESET_LUKS_DEVICE" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' | head -1 || true
}
# Add the throwaway slot where the platform's boot package owns the unlock.
# Called once reset-verify passed, so a failed rebuild adds no credential.
# reset-commit writes the boot-time key only once the factory root is active:
# a power loss before the switch leaves the old root asking for its password,
# never unlocked unattended.
add_reset_luks_slot() {
local -
set +x
[[ -n ${RESET_LUKS_DEVICE:-} && -n ${RESET_LUKS_AUTH:-} && -n ${RESET_THROWAWAY:-} ]] || fail "the reset is missing its throwaway passphrase"
RESET_LUKS_ADDED=1
{ cryptsetup luksAddKey --key-file <(printf '%s' "$RESET_LUKS_AUTH") "$RESET_LUKS_DEVICE" <(printf '%s' "$RESET_THROWAWAY") ; } 2>>"$LOG_FILE" ||
fail "could not add the throwaway LUKS keyslot"
RESET_LUKS_SLOT=$(reset_luks_slot)
[[ -n $RESET_LUKS_SLOT ]] || fail "could not identify the throwaway LUKS keyslot"
}
# Only the slot this attempt added goes, found by its key if the add was not
# confirmed. The platform's boot-time key is written only after activation, so
# a failed reset has none to revoke.
revoke_reset_luks() {
local -
set +x
[[ ${RESET_LUKS_ADDED:-0} == 1 && -n ${RESET_LUKS_AUTH:-} ]] || return 0
[[ -n ${RESET_LUKS_SLOT:-} ]] || RESET_LUKS_SLOT=$(reset_luks_slot)
if [[ -n $RESET_LUKS_SLOT ]]; then
{ cryptsetup luksKillSlot -q --key-file <(printf '%s' "$RESET_LUKS_AUTH") "$RESET_LUKS_DEVICE" "$RESET_LUKS_SLOT" ; } 2>>"$LOG_FILE" || true
else
echo "Could not find the reset's throwaway LUKS slot on $RESET_LUKS_DEVICE to revoke; check its keyslots" >>"$LOG_FILE"
fi
RESET_LUKS_ADDED=0
RESET_LUKS_SLOT=""
}
# Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the # Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the
# provisioning window. $1 is the root of the staged factory system. Where the # provisioning window. $1 is the root of the staged factory system.
# platform's boot package owns the unlock, only the staged key is written here:
# reset-prepare stages the unlock with the rest of the boot files, and the
# throwaway slot follows their verification.
stage_luks_rekey() { stage_luks_rekey() {
local next="$1" device passphrase local next="$1" device passphrase
@@ -249,29 +140,19 @@ stage_luks_rekey() {
local current local current
while true; do while true; do
current=$(gum input --password --placeholder "Current disk encryption passphrase" --prompt "Passphrase> ") || exit 1 current=$(gum input --password --placeholder "Current disk encryption passphrase" --prompt "Passphrase> ") || exit 1
if cryptsetup open --test-passphrase --token-type passphrase-only --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then if cryptsetup open --test-passphrase --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
break break
fi fi
gum style --foreground 1 "That passphrase does not unlock $device. Try again." gum style --foreground 1 "That passphrase does not unlock $device. Try again."
done done
passphrase=$(generate_passphrase) passphrase=$(generate_passphrase)
reset_boot_owner || fail "could not tell how this machine's boot chain is reset" cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")
RESET_LUKS_AUTH=$current
RESET_LUKS_DEVICE=$device
RESET_THROWAWAY=$passphrase
if [[ $RESET_BOOT == "generic" ]]; then
# Revoked like the platform's slot if the reset fails before activation.
RESET_LUKS_ADDED=1
cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")
fi
install -d -m 755 "$next$PROVISIONING_DIR" install -d -m 755 "$next$PROVISIONING_DIR"
printf '%s' "$passphrase" >"$next$PROVISIONING_DIR/luks-key" printf '%s' "$passphrase" >"$next$PROVISIONING_DIR/luks-key"
chmod 600 "$next$PROVISIONING_DIR/luks-key" chmod 600 "$next$PROVISIONING_DIR/luks-key"
[[ $RESET_BOOT == "generic" ]] || return 0
install -d -m 755 "$next/etc/omarchy" install -d -m 755 "$next/etc/omarchy"
printf '%s' "$passphrase" >"$next/etc/omarchy/provisioning.key" printf '%s' "$passphrase" >"$next/etc/omarchy/provisioning.key"
chmod 600 "$next/etc/omarchy/provisioning.key" chmod 600 "$next/etc/omarchy/provisioning.key"
@@ -354,12 +235,6 @@ verify_limine_hashes() {
rebuild_next_boot() { rebuild_next_boot() {
local next="$1" local next="$1"
reset_boot_owner || fail "could not tell how this machine's boot chain is reset"
if [[ $RESET_BOOT == "platform" ]]; then
rebuild_next_boot_platform "$next"
return
fi
local esp_source esp_mount local esp_source esp_mount
esp_mount=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $2; exit }' "$next/etc/fstab") esp_mount=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $2; exit }' "$next/etc/fstab")
esp_source=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $1; exit }' "$next/etc/fstab") esp_source=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $1; exit }' "$next/etc/fstab")
@@ -422,23 +297,6 @@ rebuild_next_boot() {
umount "$next$esp_mount" umount "$next$esp_mount"
} }
# The boot package stages the factory root's boot files and unlock, then proves
# they boot it; reset-rollback undoes both if the reset fails before the switch.
# The throwaway slot is added only once they are proven.
rebuild_next_boot_platform() {
local next="$1"
local -a luks=()
[[ -z ${RESET_LUKS_DEVICE:-} ]] || luks=("$RESET_LUKS_DEVICE")
RESET_BOOT_PREPARED=1
log "Rebuilding boot files from the factory system (this can take a minute)"
reset_boot_op reset-prepare "$next" "${luks[@]}" || fail "$RESET_BOOT_ERROR (see $LOG_FILE)"
log "Verifying that the factory system boots"
reset_boot_op reset-verify "$next" || fail "$RESET_BOOT_ERROR (see $LOG_FILE)"
[[ -z ${RESET_LUKS_DEVICE:-} ]] || add_reset_luks_slot
}
# Both the staged system and the retained baseline must lose the old hashes. # Both the staged system and the retained baseline must lose the old hashes.
scrub_factory_accounts() { scrub_factory_accounts() {
local root="$1" user users local root="$1" user users
@@ -462,12 +320,6 @@ scrub_factory_accounts() {
sanitize_factory_baseline() { sanitize_factory_baseline() {
local factory="$1" local factory="$1"
btrfs property set -ts "$factory" ro false btrfs property set -ts "$factory" ro false
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
btrfs property set -ts "$factory" ro true
fail "could not reset Thunderbolt authorization in the factory baseline"
fi
fi
if ! scrub_factory_accounts "$factory"; then if ! scrub_factory_accounts "$factory"; then
btrfs property set -ts "$factory" ro true btrfs property set -ts "$factory" ro true
@@ -493,14 +345,6 @@ stage_full_reset() {
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] || [[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
fail "the factory snapshot predates provisioning support; cannot reset from it" fail "the factory snapshot predates provisioning support; cannot reset from it"
# A normal install's factory snapshot includes the original owner's USB
# policy and enabled daemon. Keep input usable during the new owner's setup;
# provisioning enrolls their devices before enabling USBGuard again.
if [[ -f $next/usr/lib/systemd/system/usbguard.service ]]; then
systemctl --root="$next" disable usbguard.service >>"$LOG_FILE" 2>&1 ||
fail "could not disable USBGuard in the factory system (see $LOG_FILE)"
fi
log "Scrubbing machine identity from the factory system" log "Scrubbing machine identity from the factory system"
systemd-id128 new >"$next/etc/machine-id" systemd-id128 new >"$next/etc/machine-id"
rm -f "$next"/etc/ssh/ssh_host_* rm -f "$next"/etc/ssh/ssh_host_*
@@ -516,13 +360,6 @@ stage_full_reset() {
scrub_factory_accounts "$next" || scrub_factory_accounts "$next" ||
fail "could not remove account credentials from the factory system (see $LOG_FILE)" fail "could not remove account credentials from the factory system (see $LOG_FILE)"
# Leave the new owner's input available before interactive enrollment. Clear
# inherited accessory identities in both roots; never change the live daemon.
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
fail "could not reset Thunderbolt authorization in the staged root"
fi
# @factory itself survives the wipe as the baseline for future resets. If it # @factory itself survives the wipe as the baseline for future resets. If it
# came from a normal install it still holds the seller's account and # came from a normal install it still holds the seller's account and
# /etc/shadow, which the new wheel user could mount and read — and a second # /etc/shadow, which the new wheel user could mount and read — and a second
@@ -551,30 +388,9 @@ stage_full_reset() {
log "Activating the factory system" log "Activating the factory system"
local old="@omarchy-old-$(date +%s)" local old="@omarchy-old-$(date +%s)"
mv "$top/@" "$top/$old" mv "$top/@" "$top/$old"
# Until the factory root is at @ the reset is not committed: cleanup has the mv "$next" "$top/@"
# platform's boot package roll its boot state back to the old root and
# revokes the throwaway slot, so the old root goes back to @ first.
if ! mv "$next" "$top/@"; then
if mv "$top/$old" "$top/@"; then
[[ $RESET_BOOT == "platform" ]] ||
fail "could not activate the factory system; the current root is back at @, but its boot files were already rebuilt from the factory system: run 'sudo limine-update' before rebooting (see $LOG_FILE)"
fail "could not activate the factory system; the current root is back at @ (see $LOG_FILE)"
fi
# Neither root is at @. Keep the staged one as well for recovery.
swap_done=1
fail "could not activate the factory system or put the current root back; from a live USB, mount the Btrfs top level, rename $old to @, and rebuild its boot files if it does not boot"
fi
# From here the old root is gone, so the reset's unlock must survive.
reset_committed=1
swap_done=1 swap_done=1
sync sync
# The boot package that prepared the reset gets the key on standard input,
# never in arguments. A failure costs the first boot one prompt for the
# current disk password.
if [[ ${RESET_BOOT_PREPARED:-0} == 1 ]] && ! reset_boot_op reset-commit < <(printf '%s' "${RESET_THROWAWAY:-}"); then
log "$RESET_BOOT_ERROR"
fi
} }
# A reset is a clone of @factory, so a machine without one has nothing to # A reset is a clone of @factory, so a machine without one has nothing to
@@ -612,10 +428,6 @@ main() {
require_factory_snapshot require_factory_snapshot
# Decided before anything is asked: a platform whose boot package is missing
# stops here, with the dispatcher's error naming the package.
reset_boot_owner || fail "this machine's boot package cannot factory-reset it"
confirm_reset confirm_reset
# The running system's limine-snapper-sync must not rewrite the ESP's # The running system's limine-snapper-sync must not rewrite the ESP's
+2 -3
View File
@@ -11,9 +11,8 @@
# omarchy-system-sleep-lock then usually finds the session already secure. # omarchy-system-sleep-lock then usually finds the session already secure.
# #
# A docked lid close does not suspend (HandleLidSwitchDocked defaults to # A docked lid close does not suspend (HandleLidSwitchDocked defaults to
# ignore, and omarchy-system-lid-inhibit covers displays logind does not count), # ignore), so it must not lock either: that is clamshell mode, still in use on
# so it must not lock either: that is clamshell mode, still in use on the # the external display.
# external display.
if omarchy-hw-laptop-closed && ! omarchy-hw-external-monitors; then if omarchy-hw-laptop-closed && ! omarchy-hw-external-monitors; then
omarchy-system-lock >/dev/null 2>&1 || true omarchy-system-lock >/dev/null 2>&1 || true
fi fi
-100
View File
@@ -1,100 +0,0 @@
#!/bin/bash
# omarchy:summary=Keep logind from suspending on lid close while a display it misses is in use
# omarchy:group=system
# omarchy:hidden=true
# systemd-logind treats a closed lid as docked only when an external display is
# on a connector type it lists (HDMI, DisplayPort, DVI, VGA...), and USB-C
# displays on Apple Silicon arrive on "USB" connectors instead. Left alone,
# logind suspends a laptop still in use on one. Holding its handle-lid-switch
# inhibitor while such a display is in use leaves the lid to Omarchy's
# clamshell handling, as HandleLidSwitchDocked=ignore would. Once released, a
# lid that is still shut suspends at once: logind rechecks a closed lid after
# every event.
UNIT="omarchy-lid-inhibit.service"
drm_path="${OMARCHY_DRM_PATH:-/sys/class/drm}"
cgroup_path="${OMARCHY_CGROUP_PATH:-/proc/self/cgroup}"
# Connected connectors of the external types logind does not count, by the name
# Hyprland gives them (card2-USB-2 is USB-2). DVI-A and Composite are on its
# list, but a missing comma there fuses them into one entry.
uncounted_outputs() {
local status connector
for status in "$drm_path"/card*-*/status; do
[[ -r $status ]] || continue
connector=${status%/status}
connector=${connector##*/}
case $connector in
card*-USB-* | card*-DVI-A-* | card*-Composite-*) ;;
*) continue ;;
esac
[[ $(<"$status") == "connected" ]] && printf '%s\n' "${connector#card*-}"
done
}
held() {
systemctl --user is-active --quiet "$UNIT"
}
in_use=0
outputs=$(uncounted_outputs)
# In use means enabled in Hyprland: a display switched off in the monitor config
# should not keep a closed laptop awake, while one blanked by DPMS still should,
# and the connector's own "enabled" flag cannot tell the two apart on every
# driver. A compositor that does not answer, as while it reconfigures outputs
# around a lid close, changes nothing: releasing then would suspend a closed lid.
if [[ -n $outputs ]]; then
monitors=$(hyprctl monitors all -j 2>/dev/null) || exit 0
[[ -n $monitors ]] || exit 0
jq -e --arg outputs "$outputs" \
'($outputs | split("\n")) as $names | any(.[]; .disabled == false and (.name | IN($names[])))' \
<<<"$monitors" >/dev/null 2>&1
case $? in
0) in_use=1 ;;
1) ;;
*) exit 0 ;;
esac
fi
# Only where logind would ignore a docked lid is there anything to stand in for.
# An unanswered query changes nothing, for the same reason as above.
wanted=0
if (( in_use )) && omarchy-hw-laptop; then
docked_action=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager HandleLidSwitchDocked 2>/dev/null) || exit 0
[[ -n $docked_action ]] || exit 0
[[ $docked_action == 's "ignore"' ]] && wanted=1
fi
# Bound to the user unit of the watcher that keeps it in step, so a watcher that
# is gone cannot leave the lid inhibited until logout. systemd-run unquotes
# property values, so the backslashes in an escaped unit name are doubled.
watcher_binding() {
local cgroup="" unit
read -r cgroup 2>/dev/null <"$cgroup_path"
unit=${cgroup##*/}
if [[ $cgroup == */user@*.service/* && ( $unit == *.scope || $unit == *.service ) ]]; then
unit=${unit//\\/\\\\}
printf '%s\n' "--property=BindsTo=$unit" "--property=After=$unit"
fi
}
if (( wanted )); then
if ! held; then
mapfile -t bind < <(watcher_binding)
systemd-run --user --quiet --collect --unit="$UNIT" \
--description="Omarchy clamshell lid inhibitor" \
--property=PartOf=graphical-session.target "${bind[@]}" \
systemd-inhibit --no-ask-password --what=handle-lid-switch --mode=block \
--who=Omarchy --why="External display in use" sleep infinity
fi
elif held; then
systemctl --user stop "$UNIT"
fi
-201
View File
@@ -1,201 +0,0 @@
#!/bin/bash
# omarchy:summary=Play the current background's matching theme intro
# omarchy:hidden=true
set -euo pipefail
state_home="$HOME/.local/state"
current_dir="$state_home/omarchy/current"
current_background_link="$current_dir/background"
marker="$state_home/omarchy/background-intro.session-id"
toggle="$state_home/omarchy/toggles/background-intros-off"
video_extensions=(mp4 m4v mov webm mkv avi)
resolve_intro() {
local background
local stem
local extension
local intro
background=$(readlink -f "${1:-$current_background_link}" 2>/dev/null) || return 1
[[ -f $background ]] || return 1
case ${background,,} in
*.jpg | *.jpeg | *.png | *.bmp | *.webp) ;;
*) return 1 ;;
esac
stem=${background##*/}
stem=${stem%.*}
for extension in "${video_extensions[@]}"; do
intro="${background%/*}/intros/$stem.$extension"
if [[ -f $intro ]]; then
printf '%s\n' "$intro"
return 0
fi
done
return 1
}
# Preparation is speculative: the selected staged clip is compared again by OWE.
prepare_theme_intro() {
local theme="$1" background remembered name source intro
local -a names=()
[[ -n $theme && $theme != .* && $theme != */* ]] || return 0
[[ ! -f $toggle ]] && ! animations_off || return 0
owe warm >/dev/null 2>&1 || return 0
[[ $(cat "$current_dir/theme.name" 2>/dev/null) != "$theme" ]] || return 0
remembered=$(cat "$state_home/omarchy/theme-backgrounds/$theme" 2>/dev/null || true)
if [[ ${remembered%/*} == "$current_dir/theme/backgrounds" ]]; then
name=${remembered##*/}
elif [[ -f $remembered ]]; then
background="$remembered"
else
mapfile -d '' -t names < <(
find -L "$HOME/.config/omarchy/backgrounds/$theme" -maxdepth 1 -type f -print0 2>/dev/null | sort -z
)
if (( ${#names[@]} > 0 )); then
background=${names[0]}
else
mapfile -d '' -t names < <(
find -L "$OMARCHY_PATH/themes/$theme/backgrounds" "$HOME/.config/omarchy/themes/$theme/backgrounds" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.bmp' -o -iname '*.webp' \) -printf '%f\0' 2>/dev/null | sort -zu
)
(( ${#names[@]} > 0 )) || return 0
name=${names[0]}
fi
fi
if [[ -n ${name:-} ]]; then
for source in "$HOME/.config/omarchy/themes/$theme" "$OMARCHY_PATH/themes/$theme"; do
if [[ -f $source/backgrounds/$name ]]; then
background="$source/backgrounds/$name"
break
fi
done
fi
[[ -n ${background:-} ]] || return 0
intro=$(resolve_intro "$background" || true)
[[ -z $intro ]] || owe intro-prepare "$intro" >/dev/null 2>&1 || true
}
animations_off() {
[[ $(hyprctl -j getoption animations:enabled 2>/dev/null | jq -r '.bool' 2>/dev/null) == "false" ]]
}
theme_switch=false
intro_prepared=false
expected_background=""
expected_theme=""
case ${1:-} in
"") ;;
--prepare-theme)
prepare_theme_intro "${2:-}"
exit 0
;;
--resolve-only)
resolve_intro "${2:-}" || true
exit 0
;;
--resolve-startup)
if [[ ! -f $toggle ]] && ! animations_off; then
resolve_intro || true
fi
exit 0
;;
--theme-switch)
theme_switch=true
expected_background=$(readlink -f "${2:-}" 2>/dev/null) || exit 1
expected_theme=${3:-}
intro_prepared=${4:-false}
[[ -n $expected_theme ]] || exit 1
;;
*)
echo "Usage: omarchy-theme-bg-boot-intro [--resolve-only [background]|--resolve-startup|--prepare-theme theme|--theme-switch background theme-id [prepared]]" >&2
exit 1
;;
esac
if [[ $theme_switch == false ]]; then
mkdir -p "$(dirname "$marker")"
exec 9>"$state_home/omarchy/background-intro.lock"
flock 9
session_id=${OMARCHY_SESSION_ID:-${HYPRLAND_INSTANCE_SIGNATURE:-}}
[[ -n $session_id ]] || exit 0
if [[ -f $marker && $(<"$marker") == "$session_id" ]]; then
exit 0
fi
marker_staged=$(mktemp "$state_home/omarchy/.background-intro.session-id.XXXXXX")
trap 'rm -f "$marker_staged"' EXIT
printf '%s\n' "$session_id" >"$marker_staged"
mv -f "$marker_staged" "$marker"
trap - EXIT
exec 9>&-
fi
selection_current() {
[[ $(readlink -f "$current_background_link") == "$expected_background" &&
$(stat -Lc '%d:%i' "$current_dir/theme" 2>/dev/null) == "$expected_theme" ]]
}
if [[ $theme_switch == true ]] && ! selection_current; then
exit 0
fi
if [[ -f $toggle ]] || animations_off; then
[[ $theme_switch == false ]] || exit 1
exit 0
fi
intro=$(resolve_intro || true)
if [[ -z $intro ]]; then
[[ $theme_switch == false ]] || exit 1
exit 0
fi
if [[ $theme_switch == true && $intro_prepared != "true" ]]; then
owe refresh >/dev/null || exit 1
elif [[ $theme_switch == false ]]; then
deadline=$((SECONDS + 5))
while ! timeout 1 owe status >/dev/null 2>&1; do
if (( SECONDS >= deadline )); then
exit 0
fi
sleep 0.2
done
(( SECONDS < deadline )) || exit 0
fi
# The asynchronous outgoing cover must reach the compositor before playback
# can hide the shell background. Older shells have no readiness method.
if [[ $theme_switch == true ]]; then
deadline=$((SECONDS + 3))
while true; do
cover_status=$(omarchy-shell shell themeIntroCoverStatus "$expected_theme" 2>/dev/null || true)
case $cover_status in
loading)
(( SECONDS < deadline )) || exit 1
sleep 0.01
;;
error) exit 1 ;;
superseded) exit 0 ;;
*) break ;;
esac
done
fi
if [[ $theme_switch == true ]] && ! selection_current; then
exit 0
fi
# A selection or setting changed while OWE was starting no longer permits this intro.
if [[ ! -f $toggle && $intro == "$(resolve_intro || true)" ]] && ! animations_off; then
if [[ $theme_switch == true && $intro_prepared == "true" ]]; then
owe intro --start first-frame --refresh "$intro"
else
owe intro --start first-frame "$intro"
fi
elif [[ $theme_switch == true ]]; then
exit 1
fi
-34
View File
@@ -1,34 +0,0 @@
#!/bin/bash
# omarchy:summary=Enable or disable background intros
# omarchy:args=<enable|disable|toggle|status>
# omarchy:examples=omarchy theme bg intro toggle | omarchy theme bg intro disable
set -euo pipefail
toggle="$HOME/.local/state/omarchy/toggles/background-intros-off"
case ${1:-} in
enable)
rm -f "$toggle"
omarchy-notification-send "Background Intros" "Enabled"
;;
disable)
mkdir -p "$(dirname "$toggle")"
touch "$toggle"
omarchy-notification-send "Background Intros" "Disabled"
;;
toggle)
if [[ -f $toggle ]]; then
omarchy-theme-bg-intro enable
else
omarchy-theme-bg-intro disable
fi
;;
status)
[[ ! -f $toggle ]]
;;
*)
echo "Usage: omarchy-theme-bg-intro <enable|disable|toggle|status>" >&2
exit 1
;;
esac
+6 -84
View File
@@ -50,13 +50,6 @@ shell_ipc() {
timeout 2 omarchy-shell "$@" >/dev/null 2>&1 timeout 2 omarchy-shell "$@" >/dev/null 2>&1
} }
wait_for_theme_intro() {
local method="$1" token="$2" pending="$3" deadline=$((SECONDS + 3))
while (( SECONDS < deadline )) && [[ $(omarchy-shell shell "$method" "$token" 2>/dev/null) == "$pending" ]]; do
sleep 0.02
done
}
is_video_path() { is_video_path() {
[[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]] [[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]]
} }
@@ -77,18 +70,10 @@ snapshot_background_path() {
} }
snapshot_current_background() { snapshot_current_background() {
local current_background snapshot local current_background
# An interrupted intro is still moving; its final still would flash as a current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
# cover. Capture the frame already playing instead, when OWE supports it. snapshot_background_path "$current_background" "previous"
mkdir -p "$BACKGROUND_TRANSITION_CACHE"
snapshot="$BACKGROUND_TRANSITION_CACHE/previous-$$.ppm"
if owe render "$(jq -nc --arg path "$snapshot" '{cmd: "snapshot", path: $path}')" >/dev/null 2>&1; then
echo "$snapshot"
else
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
snapshot_background_path "$current_background" "previous"
fi
} }
theme_background_state_file() { theme_background_state_file() {
@@ -186,7 +171,7 @@ set_theme_background_link() {
} }
set_theme_background() { set_theme_background() {
local new_background new_background_snapshot theme_id intro_prepared=false local new_background new_background_snapshot
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
if ! choose_theme_background; then if ! choose_theme_background; then
@@ -197,37 +182,6 @@ set_theme_background() {
fi fi
new_background="$CHOSEN_THEME_BACKGROUND" new_background="$CHOSEN_THEME_BACKGROUND"
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
if [[ -n $INTRO_PREPARE_PID ]] && wait "$INTRO_PREPARE_PID"; then
intro_prepared=true
fi
theme_id=$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH")
THEME_INTRO_TOKEN="$theme_id"
shell_ipc shell prepareThemeIntro "$OLD_BACKGROUND_SNAPSHOT" "$theme_id" "$colors_payload" "$shell_payload" || \
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
(
if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" &&
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
shell_ipc background setInstant "$new_background" || true
fi
fi
shell_ipc shell finishThemeIntro "$theme_id" || true
wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
# Reloading Hyprland interrupts video presentation. Wait for playback,
# then serialize with theme selection so an old intro cannot reload
# the compositor during the next theme's video.
(
flock 9
if [[ $(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
omarchy-restart-hyprctl
fi
) 9>"$THEME_SET_LOCK"
[[ -z $OLD_BACKGROUND_SNAPSHOT ]] || rm -f "$OLD_BACKGROUND_SNAPSHOT"
) 9>&- &
return
fi
new_background_snapshot="" new_background_snapshot=""
if [[ -n $PREPARED_BACKGROUND_SNAPSHOT && $PREPARED_BACKGROUND == "$new_background" ]]; then if [[ -n $PREPARED_BACKGROUND_SNAPSHOT && $PREPARED_BACKGROUND == "$new_background" ]]; then
new_background_snapshot="$PREPARED_BACKGROUND_SNAPSHOT" new_background_snapshot="$PREPARED_BACKGROUND_SNAPSHOT"
@@ -383,12 +337,6 @@ fi
exec 9>"$THEME_SET_LOCK" exec 9>"$THEME_SET_LOCK"
flock 9 flock 9
# A current video frame only represents the screen after its opening fade.
# Finish that short reveal before capturing the next outgoing cover.
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
wait_for_theme_intro themeIntroStatus "$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null)" "pending"
fi
PREVIOUS_THEME_NAME=$(cat "$CURRENT_THEME_NAME_PATH" 2>/dev/null || true) PREVIOUS_THEME_NAME=$(cat "$CURRENT_THEME_NAME_PATH" 2>/dev/null || true)
remember_current_theme_background remember_current_theme_background
@@ -417,24 +365,11 @@ OLD_BACKGROUND_SNAPSHOT=""
PREPARED_BACKGROUND="" PREPARED_BACKGROUND=""
PREPARED_BACKGROUND_SNAPSHOT="" PREPARED_BACKGROUND_SNAPSHOT=""
BACKGROUND_TRANSITION_SNAPSHOTS=true BACKGROUND_TRANSITION_SNAPSHOTS=true
THEME_BACKGROUND_INTRO=""
INTRO_PREPARE_PID=""
THEME_INTRO_TOKEN=""
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
# Resolve the staged choice while the old theme still exists. Video changes # Resolve the staged choice while the old theme still exists. Video changes
# switch directly to the durable path in QML, so neither side needs a copy. # switch directly to the durable path in QML, so neither side needs a copy.
if choose_staged_theme_background; then if choose_staged_theme_background; then
if [[ $PREVIOUS_THEME_NAME != "$THEME_NAME" && ! -f $HOME/.local/state/omarchy/toggles/background-intros-off ]] && if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
[[ $(hyprctl -j getoption animations:enabled 2>/dev/null | jq -r '.bool') == "true" ]]; then
THEME_BACKGROUND_INTRO=$(omarchy-theme-bg-boot-intro --resolve-only "$STAGED_THEME_BACKGROUND")
fi
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
# OWE retains the opened clip across the staged directory's rename.
owe intro-prepare "$THEME_BACKGROUND_INTRO" >/dev/null 2>&1 9>&- &
INTRO_PREPARE_PID=$!
shell_ipc shell prepareThemeIntro "$OLD_BACKGROUND_SNAPSHOT" "$(stat -Lc '%d:%i' "$NEXT_THEME_PATH")" "" "" || true
elif background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background) OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
# Hand the shell the next background now, so it decodes while the # Hand the shell the next background now, so it decodes while the
@@ -454,12 +389,6 @@ fi
# Generate dynamic configs # Generate dynamic configs
omarchy-theme-set-templates omarchy-theme-set-templates
# Replacing the theme's still can cancel an active OWE intro even before the
# background link changes. Present its cover before replacing either path.
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
wait_for_theme_intro themeIntroCoverStatus "$(stat -Lc '%d:%i' "$NEXT_THEME_PATH")" "loading"
fi
# Swap next theme in as current # Swap next theme in as current
rm -rf "$CURRENT_THEME_PATH" rm -rf "$CURRENT_THEME_PATH"
mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH" mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"
@@ -489,6 +418,7 @@ flock -u 9
post_theme_commands=( post_theme_commands=(
omarchy-restart-terminal omarchy-restart-terminal
omarchy-restart-hyprctl
omarchy-restart-btop omarchy-restart-btop
omarchy-restart-opencode omarchy-restart-opencode
omarchy-restart-helix omarchy-restart-helix
@@ -507,14 +437,6 @@ post_theme_commands=(
) )
if [[ $THEME_HEADLESS != "1" ]]; then if [[ $THEME_HEADLESS != "1" ]]; then
# Let the wallpaper and bar finish their reveal before app retints compete
# with decoding and the shell's frame rendering. An intro reloads Hyprland
# after playback; ordinary theme changes reload it with the app retints.
if [[ -n $THEME_INTRO_TOKEN ]]; then
wait_for_theme_intro themeIntroStatus "$THEME_INTRO_TOKEN" "pending"
else
post_theme_commands+=(omarchy-restart-hyprctl)
fi
run_parallel "${post_theme_commands[@]}" run_parallel "${post_theme_commands[@]}"
# Call hook on theme set # Call hook on theme set
+2 -7
View File
@@ -29,8 +29,6 @@ if [[ -n $OMARCHY_THEME_SYNC_FROM ]] || ! omarchy-toggle-enabled herdr-theme-syn
fi fi
theme=$(cat "$HOME/.local/state/omarchy/current/theme.name" 2>/dev/null) theme=$(cat "$HOME/.local/state/omarchy/current/theme.name" 2>/dev/null)
control_dir="$XDG_RUNTIME_DIR/omarchy-theme-sync"
mkdir -m 700 -p "$control_dir"
# Runs on the remote machine under a login shell, so OMARCHY_PATH and PATH are set there. # Runs on the remote machine under a login shell, so OMARCHY_PATH and PATH are set there.
# SSH carries no desktop session, so the environment comes from the live Hyprland instance. # SSH carries no desktop session, so the environment comes from the live Hyprland instance.
@@ -68,13 +66,10 @@ read -r -d '' REMOTE_SCRIPT <<'EOF'
} </dev/null } </dev/null
EOF EOF
# Reuse each authenticated connection for ten idle minutes, so an SSH agent like # One machine at a time, so an SSH agent like 1Password asks for approval at most once.
# 1Password does not need to approve every theme change. Connect one machine at a time.
# SSH joins its arguments into a shell command, so the theme travels on stdin as a quoted assignment. # SSH joins its arguments into a shell command, so the theme travels on stdin as a quoted assignment.
for target in $targets; do for target in $targets; do
result=$(printf 'theme=%q from=%q\n%s\n' "$theme" "$(hostname)" "$REMOTE_SCRIPT" | result=$(printf 'theme=%q from=%q\n%s\n' "$theme" "$(hostname)" "$REMOTE_SCRIPT" |
timeout 60 ssh -o BatchMode=yes -o ConnectTimeout=5 \ timeout 60 ssh -o BatchMode=yes -o ConnectTimeout=5 "$target" bash -l -s 2>&1 | tail -n 1)
-o ControlMaster=auto -o ControlPersist=600 -o "ControlPath=$control_dir/%C" \
"$target" bash -l -s 2>&1 | tail -n 1)
echo "$(date '+%F %T') $target: ${result:-unreachable}" echo "$(date '+%F %T') $target: ${result:-unreachable}"
done done
@@ -1,17 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Manage system Thunderbolt authorization settings
# omarchy:hidden=true
# This root entrypoint uses installed code and rejects unsafe Bash startup.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
source /usr/share/omarchy/install/helpers/thunderbolt-setup.sh
tb_admin "$@"
@@ -1,25 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Approve one verified Thunderbolt connection
# omarchy:hidden=true
# pkexec permits this exact program for an active local wheel user. Suppress
# startup injection before reading input; it cannot dispatch general root work.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
[[ $# == 2 && ( $1 == "true" || $1 == "false" ) ]] || exit 64
(( ${#2} <= 16384 )) || exit 64
jq -e 'type == "object"' <<< "$2" >/dev/null
exec 9> "$TB_LOCK"
flock -x 9
result=$(tb_approve "$2" "$1" true)
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$result"
printf '%s\n' "$result"
@@ -1,8 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Check whether Thunderbolt firmware boot access is restricted
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
tb_public_snapshot | jq -e .boot_protection >/dev/null
@@ -1,16 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Restore trusted Thunderbolt devices and publish their status
# omarchy:hidden=true
# Keep root startup independent of a caller's environment or checkout.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
tb_daemon
@@ -1,10 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval review
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
[[ $# == 1 ]] || exit 64
tb_review "$1"
@@ -1,9 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval watch
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
tb_watch
+2 -16
View File
@@ -127,7 +127,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-system-pkgs omarchy-update-system-pkgs
omarchy-migrate omarchy-migrate
omarchy-update-orphan-pkgs -y omarchy-update-orphan-pkgs
omarchy-update-analyze-logs omarchy-update-analyze-logs
omarchy-update-status omarchy-update-status
@@ -148,15 +148,6 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp omarchy_security_revoke_sudo_timestamp
# The last sudo-capable step: the boot files every package step left, AUR
# packages' initramfs rebuilds included, must be shown to boot the updated
# system. It follows third-party build code, so it starts cold and, like AUR,
# authorizes one command through the no-update wrapper without a reusable
# timestamp. Until it passes, the update neither finishes nor offers a reboot.
boot_verified=1
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-boot || boot_verified=0
omarchy_security_revoke_sudo_timestamp
# The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it # The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it
# needs no privilege because the held command already dropped to this user. # needs no privilege because the held command already dropped to this user.
# Release it before offering a reboot: a confirmed reboot can terminate this # Release it before offering a reboot: a confirmed reboot can terminate this
@@ -165,10 +156,5 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-stay-awake stop omarchy-update-stay-awake stop
update_stay_awake_stopped=1 update_stay_awake_stopped=1
if (( boot_verified )); then "$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
else
echo -e "\e[31m\nThe update is not finished: its boot files did not pass verification (see above). Do not reboot until omarchy update completes.\e[0m" >&2
exit 1
fi
fi fi
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
# omarchy:summary=Verify the platform's boot chain after the update's package changes
# omarchy:hidden=true
# omarchy:requires-sudo=true
# Proves that the boot files the package steps left boot the updated system;
# omarchy update neither finishes nor offers a reboot until this passes. It is
# the update-verify lifecycle dispatch operation: a no-op where the platform
# registers no boot package, x86 included, and the platform's own entrypoint
# says what failed.
set -e
if (( $# )); then
echo "Usage: omarchy-update-boot" >&2
exit 2
fi
operation=update-verify
# Resolving as the user keeps sudo, and its password prompt, out of updates
# with nothing to run. Root resolves again from its own fixed paths before
# anything runs.
errors=$(mktemp)
trap 'rm -f "$errors"' EXIT
status=0
resolved=$(omarchy-lifecycle-dispatch --resolve "$operation" 2>"$errors") || status=$?
# 3: the platform's boot package is not installed at all. Such a machine
# predates it and boots a chain the package does not manage, so verification
# warns instead of failing every update until the package is installed.
if (( status == 3 )); then
echo -e "\e[33m\n$(sed 's/^Error: //' "$errors")\nThe boot files were not verified. This machine predates its platform's boot package; updates verify them once it is installed.\e[0m" >&2
elif (( status != 0 )); then
cat "$errors" >&2
exit "$status"
elif [[ -n $resolved ]]; then
sudo omarchy-lifecycle-dispatch "$operation"
fi
+1 -25
View File
@@ -22,32 +22,8 @@ fi
# Ensure we have the latest archlinux-keyring, maintainer keys might have changed # Ensure we have the latest archlinux-keyring, maintainer keys might have changed
# Always reinstall, as the keyring can be updated without a package version bump. # Always reinstall, as the keyring can be updated without a package version bump.
# An aarch64 machine's Arch Linux ARM repositories are signed with keys of
# their own, so its keyring is kept current the same way.
keyrings=(archlinux-keyring)
if omarchy-hw-aarch64 && omarchy-pkg-present archlinuxarm-keyring; then
keyrings+=(archlinuxarm-keyring)
fi
# A platform package names the signing keyrings of the repositories it adds,
# one package per line. Only installed ones are refreshed, so this adds no
# repository the machine didn't already trust; a refreshed keyring may carry
# that repository's new keys. A line that isn't one keyring package name is
# skipped with a warning, so a mistake in the list can't stop every update.
platform_keyrings=/usr/share/omarchy-platform/keyrings
if [[ -f $platform_keyrings ]]; then
while read -r keyring extra || [[ -n $keyring ]]; do
[[ -n $keyring && $keyring != \#* ]] || continue
if [[ -n $extra || ! $keyring =~ ^[a-z0-9][a-z0-9@._+-]*-keyring$ ]]; then
echo "Warning: skipping ${keyring}${extra:+ $extra} in $platform_keyrings: not a keyring package name" >&2
continue
fi
if omarchy-pkg-present "$keyring" && [[ " ${keyrings[*]} " != *" $keyring "* ]]; then
keyrings+=("$keyring")
fi
done <"$platform_keyrings"
fi
echo -e "\e[32m\nUpdate Arch signing keys\e[0m" echo -e "\e[32m\nUpdate Arch signing keys\e[0m"
sudo pacman -Sy --noconfirm "${keyrings[@]}" >/dev/null 2> >(grep -vE '^warning: [a-z0-9@._+-]+-keyring-[^ ]+ is up to date -- reinstalling$' >&2) sudo pacman -Sy --noconfirm archlinux-keyring >/dev/null 2> >(grep -vE '^warning: archlinux-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
# Say "correct" only once the key verifiably is: before the failure checks # Say "correct" only once the key verifiably is: before the failure checks
# above, a failed recv or reinstall still ended here with exit 0. # above, a failed recv or reinstall still ended here with exit 0.
+3 -5
View File
@@ -1,8 +1,6 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Review and optionally remove orphaned system packages after updates # omarchy:summary=Review and optionally remove orphaned system packages after updates
# omarchy:args=[-y]
# omarchy:examples=omarchy update orphan-pkgs | omarchy update orphan-pkgs -y
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
set -e set -e
@@ -14,18 +12,18 @@ echo -e "\e[32m\nOrphan system packages\e[0m"
printf ' %s\n' "${orphans[@]}" printf ' %s\n' "${orphans[@]}"
echo echo
if [[ ${1:-} != "-y" && ( ! -t 0 || ! -t 1 ) ]]; then if [[ ! -t 0 || ! -t 1 ]]; then
echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them." echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them."
echo echo
exit 0 exit 0
fi fi
if [[ ${1:-} != "-y" ]] && ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then if ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then
echo "Keeping orphaned packages." echo "Keeping orphaned packages."
echo echo
exit 0 exit 0
fi fi
echo -e "\e[32m\nRemoving orphan system packages\e[0m" echo -e "\e[32m\nRemoving orphan system packages\e[0m"
omarchy-pkg-drop "${orphans[@]}" sudo pacman -Rns "${orphans[@]}"
echo echo
@@ -98,17 +98,6 @@ mapfile -t leftovers < <(
# the moved files inactive for nothing. # the moved files inactive for nothing.
((${#leftovers[@]} == $(grep -c ' exists in filesystem' "$errors"))) || exit 1 ((${#leftovers[@]} == $(grep -c ' exists in filesystem' "$errors"))) || exit 1
# A platform whose boot package owns the boot chain may also have files no
# package owns that it can't do without (written by its image or installer).
# Its boot package vouches for the takeover or refuses it, and the files stay.
# Where the platform registers none, nothing resolves and nothing asks for root.
takeover=$(omarchy-lifecycle-dispatch --resolve update-takeover) || exit 1
if [[ -n $takeover ]] && ! sudo omarchy-lifecycle-dispatch update-takeover "${leftovers[@]}"; then
echo -e "\e[33m\nThis platform keeps these files, so the upgrade can't take them over:\e[0m" >&2
printf ' %s\n' "${leftovers[@]}" >&2
exit 1
fi
echo -e "\e[33m\nTaking over files pacman doesn't own yet:\e[0m" echo -e "\e[33m\nTaking over files pacman doesn't own yet:\e[0m"
for path in "${leftovers[@]}"; do for path in "${leftovers[@]}"; do
sudo mkdir -p "$replaced${path%/*}" sudo mkdir -p "$replaced${path%/*}"
-7
View File
@@ -807,11 +807,6 @@ install_omarchy_quattro_packages() {
if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then
log "Installing Omarchy quattro default package set" log "Installing Omarchy quattro default package set"
mapfile -t base_packages < <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-base.packages) mapfile -t base_packages < <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-base.packages)
# Some base packages have no aarch64 build.
if [[ $(uname -m) != "x86_64" && -f /usr/share/omarchy/install/omarchy-x86_64-only.packages ]]; then
mapfile -t base_packages < <(printf '%s\n' "${base_packages[@]}" |
grep -vxF -f <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-x86_64-only.packages))
fi
if (( ${#base_packages[@]} )); then if (( ${#base_packages[@]} )); then
as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}" as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}"
mark_packages_explicit "${base_packages[@]}" mark_packages_explicit "${base_packages[@]}"
@@ -1807,8 +1802,6 @@ copy_missing_config_defaults() {
while IFS= read -r -d '' source_path; do while IFS= read -r -d '' source_path; do
rel="${source_path#$source_dir/}" rel="${source_path#$source_dir/}"
is_retired_config_file "$rel" && continue is_retired_config_file "$rel" && continue
# The input migration preserves working shortcuts in existing IME profiles.
[[ $rel == "fcitx5/config" ]] && continue
target_path="$target_dir/$rel" target_path="$target_dir/$rel"
[[ -e $target_path || -L $target_path ]] && continue [[ -e $target_path || -L $target_path ]] && continue
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash -p
# omarchy:hidden=true
# omarchy:summary=Save portable trust for one verified USB device
# This narrow Polkit boundary accepts a live device snapshot, never a policy or
# file path. Privileged Bash suppresses startup code from the caller's environment.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset USBGUARD_DAEMON_CONF USBGUARD_DEBUG
source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh
(( $# == 2 )) || exit 64
# Omarchy persists to this file. Refuse custom daemon storage rather than
# mistaking a stale copy of a rule for a successful persistent decision.
rules_file=$(awk -F= '/^[[:space:]]*RuleFile[[:space:]]*=/ { value=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value; exit }' /etc/usbguard/usbguard-daemon.conf)
[[ $rules_file == "/etc/usbguard/rules.conf" ]] || exit 1
exec 8>/run/omarchy-usb-authorization.lock
flock -x 8
usb_authorization_permanent "$1" "$2"
-827
View File
@@ -1,827 +0,0 @@
#!/bin/bash
# omarchy:summary=Manage USB authorization from the start of kernel boot
# omarchy:args=<enable|disable>
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
drop_in=/etc/limine-entry-tool.d/usb-authorization.conf
setting='KERNEL_CMDLINE[default]+=" usbcore.authorized_default=0"'
limine_conf=/boot/limine.conf
limine_defaults=/etc/default/limine
boot_path=/boot
kernel_parameter=usbcore.authorized_default=0
snapshot_begin='# Omarchy USB authorization begin'
snapshot_setting='SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0'
snapshot_end='# Omarchy USB authorization end'
usb_authorization_cmdline_has_parameter() {
local cmdline="$1" parameter="$2" word
for word in $cmdline; do
[[ $word == "$parameter" ]] && return 0
done
return 1
}
usb_authorization_secure_boot_enabled() {
local value variable
for variable in /sys/firmware/efi/efivars/SecureBoot-*; do
[[ -r $variable ]] || continue
value=$(od -An -t u1 -j 4 -N 1 "$variable")
value=${value//[[:space:]]/}
[[ $value == "1" ]] && return 0
done
return 1
}
usb_authorization_verify_boot_entries() {
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}"
local secure_boot="${5:-auto}"
local actual_hash embedded_cmdline expected_hash image mode path paths section
if [[ $secure_boot == "auto" ]]; then
if usb_authorization_secure_boot_enabled; then
secure_boot=enabled
else
secure_boot=disabled
fi
fi
if [[ ! -r $config ]]; then
echo "Cannot verify the generated Limine boot entries in $config." >&2
return 1
fi
paths=$(mktemp /tmp/omarchy-usb-boot-paths.XXXXXXXXXX)
section=$(mktemp /tmp/omarchy-usb-boot-cmdline.XXXXXXXXXX)
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
function contains_parameter(value, count, fields, field_index) {
count = split(value, fields, /[[:space:]]+/)
for (field_index = 1; field_index <= count; field_index++) {
if (fields[field_index] == parameter) return 1
}
return 0
}
function finish_entry(found) {
if (!target_os || !is_kernel) return
entries++
if (has_cmdline) {
found = contains_parameter(cmdline)
if ((expected == "enabled" && !found) || (expected == "disabled" && found)) bad = 1
}
if (path == "") {
bad = 1
} else {
print (has_cmdline ? "external" : "embedded") "\t" path
}
}
/^\/[^/]/ {
finish_entry()
target_os = 0
is_kernel = 0
has_cmdline = 0
cmdline = ""
path = ""
next
}
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
target_os = index($0, "machine-id=" machine_id) > 0
next
}
/^[[:space:]]*\/\// {
finish_entry()
is_kernel = 0
has_cmdline = 0
cmdline = ""
path = ""
next
}
/^[[:space:]]*comment:[[:space:]]*kernel-id=/ {
is_kernel = target_os
next
}
is_kernel && /^[[:space:]]*path:[[:space:]]*/ {
path = $0
sub(/^[[:space:]]*path:[[:space:]]*/, "", path)
next
}
is_kernel && /^[[:space:]]*cmdline:[[:space:]]*/ {
has_cmdline = 1
cmdline = $0
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", cmdline)
next
}
END {
finish_entry()
if (entries == 0) bad = 1
exit bad
}
' "$config" >"$paths"; then
rm -f "$paths" "$section"
echo "The generated Limine kernel entries do not all have USB authorization $expected." >&2
return 1
fi
while IFS=$'\t' read -r mode path; do
if [[ $path != 'boot():/'* ]]; then
rm -f "$paths" "$section"
echo "Cannot verify the kernel image referenced by Limine: $path" >&2
return 1
fi
image="$esp/${path#'boot():/'}"
image=${image%%#*}
if [[ ! -r $image ]]; then
rm -f "$paths" "$section"
echo "Cannot verify the kernel image referenced by Limine: $image" >&2
return 1
fi
if [[ $path == *'#'* ]]; then
expected_hash=${path##*#}
actual_hash=$(b2sum "$image")
actual_hash=${actual_hash%% *}
if [[ $actual_hash != "$expected_hash" ]]; then
rm -f "$paths" "$section"
echo "The verification hash for $image does not match the generated Limine entry." >&2
return 1
fi
fi
: >"$section"
if ! objcopy -O binary --only-section=.cmdline "$image" "$section" 2>/dev/null; then
[[ $mode == "external" ]] && continue
rm -f "$paths" "$section"
echo "Cannot inspect the embedded kernel command line in $image." >&2
return 1
fi
embedded_cmdline=$(tr '\0' '\n' <"$section")
# Without Secure Boot, Limine's external command line takes precedence.
# Under Secure Boot, systemd-stub uses a nonempty embedded .cmdline.
if [[ $mode == "external" && $secure_boot == "disabled" ]]; then
continue
fi
# A signed UKI can ignore Limine's external cmdline and use its embedded
# .cmdline instead. If one exists, both representations must agree.
if [[ -z $embedded_cmdline ]]; then
if [[ $mode == "external" || $expected == "disabled" ]]; then
continue
fi
rm -f "$paths" "$section"
echo "The generated kernel image $image has no verifiable kernel command line." >&2
return 1
fi
if [[ $expected == "enabled" ]]; then
if ! usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
rm -f "$paths" "$section"
echo "The generated kernel image $image does not enable boot-time USB authorization." >&2
return 1
fi
elif usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
rm -f "$paths" "$section"
echo "The generated kernel image $image still enables boot-time USB authorization." >&2
return 1
fi
done <"$paths"
rm -f "$paths" "$section"
}
usb_authorization_rebuild_and_verify() {
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}" secure_boot="${5:-auto}"
if ! limine-mkinitcpio; then
return 1
fi
# limine-mkinitcpio-hook can swallow an individual kernel build failure and
# still return success, so its exit status alone is not a safety boundary.
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id" "$secure_boot"
}
usb_authorization_snapshot_block_status() {
local config="$1"
[[ -r $config ]] || return 2
awk -v begin="$snapshot_begin" -v setting="$snapshot_setting" -v end="$snapshot_end" '
$0 == begin { begin_count++; state = 1; next }
$0 == setting { setting_count++; if (state == 1) state = 2; next }
$0 == end { end_count++; if (state == 2) valid_count++; state = 0; next }
{ state = 0 }
END {
if (begin_count == 0 && setting_count == 0 && end_count == 0) exit 1
if (begin_count == 1 && setting_count == 1 && end_count == 1 && valid_count == 1) exit 0
exit 2
}
' "$config"
}
usb_authorization_enable_snapshot_setting() {
local config="$1" status temporary
if usb_authorization_snapshot_block_status "$config"; then
return 0
else
status=$?
fi
if (( status != 1 )); then
echo "Refusing to modify the USB authorization block in $config." >&2
return 1
fi
temporary=$(mktemp "${config}.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$temporary"
{
cat "$config"
printf '\n%s\n%s\n%s\n' "$snapshot_begin" "$snapshot_setting" "$snapshot_end"
} >"$temporary"
mv -f "$temporary" "$config"
}
usb_authorization_disable_snapshot_setting() {
local config="$1" status temporary
if usb_authorization_snapshot_block_status "$config"; then
status=0
else
status=$?
fi
(( status == 1 )) && return 0
if (( status != 0 )); then
echo "Refusing to modify the USB authorization block in $config." >&2
return 1
fi
temporary=$(mktemp "${config}.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$temporary"
awk -v begin="$snapshot_begin" '
$0 == begin { skip = 2; next }
skip > 0 { skip--; next }
{ print }
' "$config" >"$temporary"
mv -f "$temporary" "$config"
}
usb_authorization_rewrite_boot_cmdlines() {
local expected="$1" config="$2" machine_id="${3:-$(</etc/machine-id)}"
local backup temporary
backup=$(mktemp "${config}.omarchy-usb-backup.XXXXXXXXXX")
temporary=$(mktemp "${config}.omarchy-usb-new.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$backup"
cp --preserve=mode,ownership "$config" "$temporary"
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
function rewrite_cmdline(line, prefix, value, count, fields, field_index, result, found) {
prefix = line
sub(/cmdline:.*/, "cmdline:", prefix)
value = line
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", value)
count = split(value, fields, /[[:space:]]+/)
result = ""
found = 0
for (field_index = 1; field_index <= count; field_index++) {
if (fields[field_index] == parameter) {
found = 1
continue
}
if (fields[field_index] != "") result = result (result == "" ? "" : " ") fields[field_index]
}
if (expected == "enabled") result = result (result == "" ? "" : " ") parameter
return prefix (result == "" ? "" : " ") result
}
/^\/[^/]/ { target_os = 0 }
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
target_os = index($0, "machine-id=" machine_id) > 0
}
target_os && /^[[:space:]]*cmdline:[[:space:]]*/ {
print rewrite_cmdline($0)
next
}
{ print }
' "$config" >"$temporary"; then
rm -f "$backup" "$temporary"
return 1
fi
mv -f "$temporary" "$config"
if ! usb_authorization_enroll_config "$config" "${config%/*}"; then
mv -f "$backup" "$config"
if ! usb_authorization_secure_boot_enabled; then
limine-enroll-config >/dev/null 2>&1 || true
fi
echo "Restored $config because its bootloader verification data could not be updated." >&2
return 1
fi
rm -f "$backup"
}
usb_authorization_rewrite_snapshot_manifest() {
local expected="$1" manifest="$2" temporary
[[ -e $manifest ]] || return 0
temporary=$(mktemp "${manifest}.omarchy-usb.XXXXXXXXXX")
if ! cp --preserve=mode,ownership "$manifest" "$temporary"; then
rm -f "$temporary"
return 1
fi
# Limine uses the cached lines when present, and structured command lines
# otherwise. Update both, including nested kernels and restore entries.
if ! jq --arg expected "$expected" --arg parameter "$kernel_parameter" '
def cmdline:
if type != "string" then error("Invalid snapshot command line") else
[splits("[[:space:]]+") | select(length > 0 and . != $parameter)]
| if $expected == "enabled" then . + [$parameter] else . end
| join(" ")
end;
def lines:
map(if test("^\\s*(cmdline|kernel_cmdline):"; "i") then
capture("^(?<prefix>\\s*(?:cmdline|kernel_cmdline):\\s*)(?<value>.*)$"; "i")
| .prefix + (.value | cmdline)
else . end);
def kernel:
.cmdlineDetails |= map(.cmdline |= cmdline | .snapshotCmdline |= cmdline)
| .allInConfig |= lines
| .allInSnapshotConfig |= lines
| .subKernels |= map(kernel);
if .jsonFormatVersion != "1.3.0" or (.snapshotEntries | type) != "array" then
error("Unsupported Limine snapshot manifest format")
else
.snapshotEntries |= map(.kernelEntries |= map(kernel))
end
' "$manifest" >"$temporary"; then
rm -f "$temporary"
echo "Cannot update stored USB boot policy in $manifest." >&2
return 1
fi
mv -f "$temporary" "$manifest"
}
usb_authorization_verify_signature() {
# sbctl verify can return zero for unsigned or unreadable files. Check its
# structured result, including the exact path, instead of just the exit code.
if ! sbctl verify --json "$1" | jq -e --arg file "$1" \
'length == 1 and .[0].file_name == $file and .[0].is_signed == 1' >/dev/null; then
echo "Cannot verify the Secure Boot signature of $1 using this machine's signing keys." >&2
return 1
fi
}
# Read the same architecture and enrollment setting as the installed Limine tool.
usb_authorization_limine_settings() (
source /usr/lib/limine/limine-common-functions
load_config || return 1
printf '%s %s\n' "$(limine_efi_arch | tr '[:upper:]' '[:lower:]')" "${ENABLE_ENROLL_LIMINE_CONFIG:-no}"
)
usb_authorization_verify_bootloader() (
local config="$1" esp="$2" architecture enrollment image check digest
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
image="$esp/EFI/limine/limine_$architecture.efi"
usb_authorization_verify_signature "$image" || return 1
check=$(mktemp) || return 1
trap 'rm -f "$check"' EXIT
cp "$image" "$check" || return 1
if [[ $enrollment == "yes" ]]; then
digest=$(b2sum "$config") || return 1
limine enroll-config "$check" "${digest%% *}" --quiet || return 1
else
limine enroll-config "$check" --reset --quiet || return 1
fi
# Enrollment edits fixed bytes in the executable. Reapplying the expected
# value must be a no-op, including when enrollment is explicitly disabled.
if ! cmp -s "$image" "$check"; then
echo "Limine's signed executable does not match the configured menu enrollment." >&2
return 1
fi
)
usb_authorization_enroll_config() {
limine-enroll-config || return 1
if usb_authorization_secure_boot_enabled; then
usb_authorization_verify_bootloader "$1" "$2" || return 1
fi
}
# Kernel rebuild hooks also reset and re-sign Limine. Checkpoint the complete
# boot files, not just the menu: rebuilding can overwrite images it references.
# The outer process holds the real boot lock; the private mount namespace gives
# nested Limine tools their own lock inode so they cannot deadlock against it.
usb_authorization_boot_transaction() (
local action="$1" state architecture enrollment index manifest changed=0 committed=0 restored=1
local machine_id="$(</etc/machine-id)"
local cache="${2:-/var/cache/boot/$machine_id}"
local -a paths=("$boot_path" "$cache" "$limine_defaults" "$drop_in")
exec 8>/run/lock/boot-partition.lock
flock --timeout 30 8 || return 1
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
usb_authorization_verify_signature "$boot_path/EFI/limine/limine_$architecture.efi" || return 1
state=$(mktemp -d /var/tmp/omarchy-usb-boot.XXXXXXXXXX) || return 1
trap '
if (( changed && ! committed )); then
echo "USB boot policy failed; restoring the previous boot files and settings." >&2
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d $state/$index && ! -L $state/$index ]]; then
mkdir -p "${paths[index]}" && cp -a "$state/$index/." "${paths[index]}/" || restored=0
elif [[ -e $state/$index || -L $state/$index ]]; then
cp -a --remove-destination "$state/$index" "${paths[index]}" || restored=0
else
rm -rf -- "${paths[index]}" || restored=0
fi
done
# New unreferenced images are harmless, but a newly created cached backup
# manifest must not reintroduce the failed policy on a later recovery.
for manifest in snapshots.json snapshots.json.old; do
[[ -e $state/0/$machine_id/limine_history/$manifest ]] ||
rm -f "$boot_path/$machine_id/limine_history/$manifest" || restored=0
[[ -e $state/1/lss/$manifest ]] || rm -f "$cache/lss/$manifest" || restored=0
done
sync -f "$boot_path" || restored=0
fi
if (( restored )); then
rm -rf "$state"
else
echo "Boot recovery was incomplete. Preserve and restore the checkpoint at $state before rebooting." >&2
fi
' EXIT
trap 'exit 1' HUP INT TERM
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d ${paths[index]} ]]; then
cp -a "${paths[index]}/." "$state/$index" || return 1
elif [[ -e ${paths[index]} || -L ${paths[index]} ]]; then
cp -a "${paths[index]}" "$state/$index" || return 1
fi
done
: >"$state/lock"
changed=1
usb_authorization_run_transaction "$state" "$action" || return 1
usb_authorization_verify_bootloader "$limine_conf" "$boot_path" || return 1
sync -f "$boot_path" || return 1
committed=1
)
usb_authorization_run_transaction() {
unshare --mount --propagation private /bin/bash -c '
mount --bind "$1/lock" /run/lock/boot-partition.lock || exit 1
source "$2"
usb_authorization_main "$3"
' bash "$1" "${BASH_SOURCE[0]}" "$2"
}
# Called with Limine's boot-partition lock held. Prepare signed copies of
# conflicting historical UKIs before changing the selected boot policy.
usb_authorization_reconcile_archived_images() (
local expected="$1" config="$2" esp="$3" machine_id="$4"
local cache="${5:-/var/cache/boot/$machine_id/lss/snapshots.json}"
local history="$esp/$machine_id/limine_history" staging manifest names name image
local cmdline hash_command old_digest new_digest old_hash new_hash replacement index=0
local published=0 committed=0 section layout image_base image_size alignment address
local -a manifests=() originals=()
usb_authorization_secure_boot_enabled || return 0
[[ -d $history ]] || return 0
staging=$(mktemp -d "$esp/.omarchy-usb-uki.XXXXXXXXXX") || return 1
trap '
if (( published && ! committed )); then
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
done
fi
rm -rf "$staging"
' EXIT
for manifest in "$history/snapshots.json" "$history/snapshots.json.old" "$cache"; do
[[ -f $manifest ]] || continue
jq -e '.jsonFormatVersion == "1.3.0" and (.snapshotEntries | type == "array")' "$manifest" >/dev/null || return 1
manifests+=("$manifest")
done
(( ${#manifests[@]} )) || return 0
names=$(jq -sr '[.[] | .snapshotEntries[] | .. | objects | .imageDetails? // empty | .[] | .fileHashName] | unique[]' "${manifests[@]}") || return 1
printf '[]\n' >"$staging/replacements.json"
while IFS= read -r name; do
[[ -n $name ]] || continue
# Only image names, never arbitrary paths supplied by a manifest.
if [[ $name == */* || $name == "." || $name == ".." ]]; then
echo "Invalid archived kernel image name: $name" >&2
return 1
fi
image="$history/$name"
[[ -f $image && ! -L $image ]] || continue
: >"$staging/cmdline"
objcopy -O binary --only-section=.cmdline "$image" "$staging/cmdline" 2>/dev/null || continue
cmdline=$(tr '\0' '\n' <"$staging/cmdline")
[[ -n $cmdline ]] || continue
if usb_authorization_cmdline_has_parameter "$cmdline" "$kernel_parameter"; then
[[ $expected == "disabled" ]] || continue
else
[[ $expected == "enabled" ]] || continue
fi
# Multi-profile images and signed PCR policies need their original builder.
# Do not silently invalidate their additional boot/unlock guarantees.
for section in .profile .pcrsig; do
objcopy -O binary --only-section="$section" "$image" "$staging/section" || return 1
if [[ -s $staging/section ]]; then
echo "Rebuild archived image $image with its original signing policy before changing USB boot protection ($section)." >&2
return 1
fi
done
if [[ ! $name =~ ^(.+)_(sha1|sha256|b3|xxh)_([0-9a-f]+)$ ]]; then
echo "Cannot verify the content hash of archived image $image." >&2
return 1
fi
case "${BASH_REMATCH[2]}" in
sha1) hash_command=sha1sum ;;
sha256) hash_command=sha256sum ;;
b3) hash_command=b3sum ;;
xxh) hash_command=xxhsum ;;
esac
old_digest=${BASH_REMATCH[3]}
new_digest=$("$hash_command" "$image") || return 1
if [[ ${new_digest%% *} != "$old_digest" ]]; then
echo "Archived image hash mismatch: $image" >&2
return 1
fi
# Verify the old signature before signing a modified copy with this machine's key.
usb_authorization_verify_signature "$image" || return 1
cp "$image" "$staging/unsigned.efi" || return 1
sbattach --remove "$staging/unsigned.efi" || return 1
# Preserve the embedded boot arguments and change only our exact USB
# parameter. Do not turn a signed command line into an external override.
printf '%s' "$cmdline" | jq -Rrs --arg parameter "$kernel_parameter" --arg expected "$expected" '
[scan("[^[:space:]]+|[[:space:]]+") | select(. != $parameter)]
| join("") | sub("[[:space:]]+$"; "")
| if $expected == "enabled" then . + " " + $parameter else . end
' >"$staging/new-cmdline" || return 1
printf '\0' >>"$staging/new-cmdline"
# objcopy --update-section retains PE VirtualSize and can truncate a longer
# command line. Add a fresh section after the existing virtual image instead.
layout=$(objdump -p "$staging/unsigned.efi" | awk '
$1 == "ImageBase" { base = $2 }
$1 == "SizeOfImage" { size = $2 }
$1 == "SectionAlignment" { alignment = $2 }
END {
if (base !~ /^[0-9a-fA-F]+$/ || size !~ /^[0-9a-fA-F]+$/ || alignment !~ /^[0-9a-fA-F]+$/) exit 1
print "0x" base, "0x" size, "0x" alignment
}
') || return 1
read -r image_base image_size alignment <<<"$layout"
(( alignment > 0 )) || return 1
address=$((image_base + (image_size + alignment - 1) / alignment * alignment))
objcopy --remove-section=.cmdline "$staging/unsigned.efi" "$staging/without-cmdline.efi" || return 1
objcopy --add-section .cmdline="$staging/new-cmdline" \
--change-section-vma .cmdline="$address" \
--set-section-flags .cmdline=alloc,load,readonly,data,contents \
"$staging/without-cmdline.efi" "$staging/image.efi" || return 1
sbctl sign "$staging/image.efi" || return 1
usb_authorization_verify_signature "$staging/image.efi" || return 1
objcopy -O binary --only-section=.cmdline "$staging/image.efi" "$staging/cmdline" || return 1
cmp -s "$staging/cmdline" "$staging/new-cmdline" || return 1
new_digest=$("$hash_command" "$staging/image.efi") || return 1
replacement="${name%"$old_digest"}${new_digest%% *}"
old_hash=$(b2sum "$image") || return 1
new_hash=$(b2sum "$staging/image.efi") || return 1
# Publish copies first: every old reference remains usable until its manifest changes.
if [[ -e $history/$replacement || -L $history/$replacement ]]; then
[[ ! -L $history/$replacement ]] && cmp -s "$staging/image.efi" "$history/$replacement" || return 1
else
mv "$staging/image.efi" "$history/$replacement" || return 1
fi
jq --arg old "$name" --arg new "$replacement" \
--arg old_hash "${old_hash%% *}" --arg new_hash "${new_hash%% *}" \
'. + [{old: $old, new: $new, old_hash: $old_hash, new_hash: $new_hash}]' \
"$staging/replacements.json" >"$staging/next.json" || return 1
mv "$staging/next.json" "$staging/replacements.json" || return 1
done <<<"$names"
jq -e 'length > 0' "$staging/replacements.json" >/dev/null || return 0
originals=("$config" "${manifests[@]}")
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "${originals[index]}" "$staging/original.$index" || return 1
cp --preserve=mode,ownership "${originals[index]}" "$staging/updated.$index" || return 1
# Update cached menu lines, structured image metadata, restore paths and
# both hashes. The new file name remains a valid Limine content address.
if (( index == 0 )); then
jq -Rrs --slurpfile replacements "$staging/replacements.json" '
split("\n") | map(reduce $replacements[0][] as $r (. ;
if contains($r.old) then split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash)
else . end)) | join("\n")
' "$staging/original.$index" >"$staging/updated.$index" || return 1
else
jq --slurpfile replacements "$staging/replacements.json" '
walk(if type == "string" then
reduce $replacements[0][] as $r (. ; split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash))
else . end)
' "$staging/original.$index" >"$staging/updated.$index" || return 1
fi
done
published=1
for ((index=0; index<${#originals[@]}; index++)); do
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
done
usb_authorization_enroll_config "$config" "$esp" || return 1
committed=1
)
usb_authorization_prepare_archived_images() (
local expected="$1" config="$2" esp="$3" machine_id="${4:-$(</etc/machine-id)}"
usb_authorization_secure_boot_enabled || return 0
# Package hooks acquire the boot lock too; install this optional tool before
# taking it, never while rewriting archived images.
if omarchy-cmd-missing sbattach; then
omarchy-pkg-add sbsigntools || return 1
fi
exec 9>/run/lock/boot-partition.lock
flock --timeout 30 9 || return 1
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id"
)
usb_authorization_sync_and_verify() (
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}" manifest
# Share Limine's lock so a concurrent watcher cannot restore an old manifest
# while we update it. The sync wrapper must not reacquire our lock.
exec 9>/run/lock/boot-partition.lock
flock --timeout 30 9 || return 1
# Recover/upgrade the manifest first, including backups if necessary.
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id" || return 1
for manifest in \
"$esp/$machine_id/limine_history/snapshots.json" \
"$esp/$machine_id/limine_history/snapshots.json.old" \
"/var/cache/boot/$machine_id/lss/snapshots.json"; do
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest" || return 1
done
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine_id" || return 1
# Verify the regenerated menu, so synchronization cannot mask stale stored
# command lines with a one-time edit to limine.conf.
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_enroll_config "$config" "$esp" || return 1
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
)
# A Mac's boot chain belongs to its boot package, so boot-time denial is not
# managed here. Disabling what is not configured succeeds, so turning USB
# authorization off still works; anything configured by another route is left
# to the owner, since this cannot rebuild a Mac's boot files.
usb_authorization_apple_boot() {
if [[ ${1:-} == "enable" ]]; then
echo "USB authorization from boot is not available on Apple Silicon yet." >&2
return 2
fi
if [[ -e $drop_in ]] || grep -Fqx "$snapshot_begin" "$limine_defaults" 2>/dev/null; then
echo "Boot-time USB authorization is configured in $drop_in or $limine_defaults, which this command cannot remove on Apple Silicon." >&2
echo "Remove it by hand: delete $drop_in and the lines from '$snapshot_begin' to '$snapshot_end' in $limine_defaults, then rebuild the boot files." >&2
return 1
fi
}
usb_authorization_main() {
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
exit 1
fi
case "${1:-}" in
enable)
if usb_authorization_snapshot_block_status "$limine_defaults"; then
snapshot_status=0
else
snapshot_status=$?
fi
if (( snapshot_status == 2 )); then
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
exit 1
fi
# Quote the expected value because the Limine key contains glob brackets.
if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then
echo "Refusing to replace the existing $drop_in." >&2
exit 1
fi
usb_authorization_prepare_archived_images enabled "$limine_conf" "$boot_path" || exit 1
usb_authorization_enable_snapshot_setting "$limine_defaults"
if [[ ! -e $drop_in ]]; then
install -d -m755 "${drop_in%/*}"
temporary=$(mktemp "${drop_in}.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
printf '%s\n' "$setting" >"$temporary"
chmod 644 "$temporary"
mv -f "$temporary" "$drop_in"
trap - EXIT
fi
if ! limine-mkinitcpio ||
! usb_authorization_sync_and_verify enabled "$limine_conf" "$boot_path"; then
echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2
exit 1
fi
;;
disable)
if usb_authorization_snapshot_block_status "$limine_defaults"; then
had_snapshot_setting=1
else
snapshot_status=$?
if (( snapshot_status == 2 )); then
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
exit 1
fi
fi
if [[ -e $drop_in ]]; then
if [[ $(<"$drop_in") != "$setting" ]]; then
echo "Refusing to remove the modified $drop_in." >&2
exit 1
fi
had_drop_in=1
fi
usb_authorization_prepare_archived_images disabled "$limine_conf" "$boot_path" || exit 1
if (( had_drop_in )); then
rm -f "$drop_in"
fi
usb_authorization_disable_snapshot_setting "$limine_defaults"
if ! limine-mkinitcpio ||
! usb_authorization_sync_and_verify disabled "$limine_conf" "$boot_path"; then
if (( had_drop_in )); then
printf '%s\n' "$setting" >"$drop_in"
chmod 644 "$drop_in"
fi
if (( had_snapshot_setting )); then
usb_authorization_enable_snapshot_setting "$limine_defaults"
fi
echo "USBGuard remains required because the boot images could not be verified as default-allow." >&2
exit 1
fi
;;
*)
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
exit 2
;;
esac
}
main() {
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
return 1
fi
case "${1:-}" in
enable|disable)
if /usr/bin/omarchy-hw-aarch64-apple; then
usb_authorization_apple_boot "$1"
elif usb_authorization_secure_boot_enabled; then
usb_authorization_boot_transaction "$1"
else
usb_authorization_main "$1"
fi
;;
*)
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
return 2
;;
esac
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
-137
View File
@@ -1,137 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Create an approval request for a blocked USB device
set -euo pipefail
case "${USBGUARD_IPC_SIGNAL:-}" in
IPC.Connected) ;;
Device.PolicyApplied)
# PresenceChanged precedes policy evaluation, even for trusted devices.
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
;;
Device.PresenceChanged)
[[ ${USBGUARD_DEVICE_EVENT:-} == "Remove" ]] || exit 0
;;
*) exit 0 ;;
esac
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
mkdir -p "$requests"
chmod 700 "${requests%/*}" "$requests"
exec 9>"$requests/.lock"
request_generation() {
local daemon_id
# USBGuard reuses device IDs after restarting. Include both the daemon
# invocation and watcher lifetime so old prompts cannot suppress new ones.
daemon_id=$(systemctl show usbguard.service --property=InvocationID --value) || return 1
[[ $daemon_id =~ ^[0-9a-f]{32}$ ]] || return 1
printf '%s:%s' "${OMARCHY_USB_AUTHORIZATION_WATCH_ID:-standalone}" "$daemon_id"
}
if [[ $USBGUARD_IPC_SIGNAL == "IPC.Connected" ]]; then
flock 9
generation=$(request_generation) || exit 1
for pending in "$requests"/request-*.json; do
[[ -f $pending ]] || continue
if ! jq -e --arg generation "$generation" '.generation == $generation' "$pending" >/dev/null 2>&1; then
rm -f "$pending"
fi
done
flock -u 9
# USBGuard scans present devices before accepting IPC connections. Repeat
# this on every reconnect: temporary approvals disappear on daemon restart.
blocked=$(usbguard list-devices --blocked) || exit 1
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" || true
fi
done <<<"$blocked"
exit 0
fi
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
# A removed device's final target may be allow even though a blocked request
# remains. Its identity attributes are unchanged; normalize the target.
USBGUARD_DEVICE_RULE="block ${USBGUARD_DEVICE_RULE#* }"
# The token is safe to carry through the presentation launcher's command
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
# is never interpolated into a shell command.
token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" | sha256sum | cut -d' ' -f1)"
request="$requests/$token.json"
# watch executes handlers concurrently. A reconnect scan and a policy event
# can refer to the same device; publish only one pending request.
if [[ $USBGUARD_IPC_SIGNAL == "Device.PresenceChanged" ]]; then
flock 9
blocked=$(usbguard list-devices --blocked) || exit 1
# A delayed removal event must not erase a request for a reappeared device.
if ! grep -Fqx -- "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" <<<"$blocked"; then
rm -f "$request"
fi
exit 0
fi
# Keep retrying without needing another USB event. Handlers belong to the
# watcher service and are stopped with the graphical session.
while true; do
flock 9
generation=$(request_generation) || exit 1
if [[ -f $request ]] && jq -e --arg generation "$generation" '.generation == $generation' "$request" >/dev/null 2>&1; then
exit 0
fi
# Discard events overtaken by another policy decision or device replacement.
blocked=$(usbguard list-devices --blocked) || exit 1
current=0
while IFS= read -r line; do
if [[ $line == "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" ]]; then
current=1
break
fi
done <<<"$blocked"
(( current )) || exit 0
# Login and shell restarts can leave D-Bus alive before notifications are ready.
# Bound each wait so disappeared devices are rechecked between attempts.
if ! omarchy-notification-wait 1; then
flock -u 9
sleep 1
continue
fi
temporary=$(mktemp "$requests/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq -n \
--arg id "$USBGUARD_DEVICE_ID" \
--arg rule "$USBGUARD_DEVICE_RULE" \
--arg generation "$generation" \
'{id: $id, rule: $rule, generation: $generation}' >"$temporary"
chmod 600 "$temporary"
mv -f "$temporary" "$request"
if omarchy-notification-send \
--app-name omarchy-action \
--urgency critical \
--glyph 󰖀 \
"USB accessory blocked" \
"Click to review it. Keep it blocked unless you recognize the device." \
--exec omarchy-launch-floating-terminal-with-presentation \
omarchy-usb-authorization-review "$token"; then
exit 0
fi
rm -f "$request"
flock -u 9
sleep 1
done
@@ -1,23 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore the kernel's default USB authorization state
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2
exit 1
fi
# USBGuard intentionally leaves root hubs in default-deny mode when it exits.
# Restore both the hub defaults and devices that are still connected.
shopt -s nullglob
for attribute in /sys/bus/usb/devices/usb*/authorized_default; do
echo 1 >"$attribute"
done
for attribute in /sys/bus/usb/devices/*/authorized; do
echo 1 >"$attribute"
done
shopt -u nullglob
-142
View File
@@ -1,142 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Review a blocked USB device authorization request
set -euo pipefail
token="${1:-}"
if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
echo "This USB authorization request is invalid or expired." >&2
exit 1
fi
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
# Serialize request updates with watcher events, but release the lock while the
# user chooses. A changed request must never inherit an old dialog's approval.
if [[ ! -d ${request%/*} ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
exec 9>"${request%/*}/.lock"
flock 9
if [[ ! -f $request || -L $request ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
snapshot=$(cat "$request")
id=$(jq -er '.id | select(test("^[0-9]+$"))' <<<"$snapshot")
expected_rule=$(jq -er '.rule | select(type == "string" and startswith("block "))' <<<"$snapshot")
choice=$(jq -er '.approval // "" | select(. == "" or . == "Allow once" or . == "Always allow this device")' <<<"$snapshot")
allowed_rule="allow ${expected_rule#block }"
approval_failed() {
echo "Could not confirm USB approval. The request has been kept; try again." >&2
exit 1
}
read_current_rule() {
local devices line
current_rule=""
devices=$(usbguard list-devices) || approval_failed
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then
current_rule="${line#"$id: "}"
return
fi
done <<<"$devices"
}
validate_device() {
read_current_rule
if [[ $current_rule == "$expected_rule" ]] ||
[[ -n $choice && $current_rule == "$allowed_rule" ]]; then
return
fi
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
}
validate_device
if [[ -z $choice ]]; then
flock -u 9
gum style --foreground 212 --bold "Blocked USB accessory"
echo
echo "The following details come from the device and can be forged:"
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo
choice=$(gum choose \
"Allow once" \
"Always allow this device" \
"Keep blocked") || exit 130
flock 9
if [[ ! -f $request || -L $request || $(cat "$request") != "$snapshot" ]]; then
echo "This USB authorization request changed. Open the latest notification to review it." >&2
exit 1
fi
# A fresh choice can authorize only the blocked identity shown in the dialog.
read_current_rule
if [[ $current_rule != "$expected_rule" ]]; then
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
fi
fi
case "$choice" in
"Allow once" | "Always allow this device")
# Save intent before applying policy: a later query can fail after the device
# was authorized. Retry must verify that approval, including persistent trust.
temporary=$(mktemp "${request%/*}/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
mv -f "$temporary" "$request"
if [[ $choice == "Always allow this device" ]]; then
pkexec /usr/bin/omarchy-usb-authorization-approve "$id" "$current_rule" || approval_failed
elif [[ $current_rule == "$expected_rule" ]]; then
# Match the complete identity, since numeric IDs can be reassigned.
usbguard allow-device "$expected_rule" || approval_failed
fi
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
# return success without authorizing anything. Verify the exact device.
allowed=$(usbguard list-devices --allowed) || approval_failed
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
if [[ $choice == "Always allow this device" ]]; then
# Let USBGuard match policy rules: saved rules can omit the device's port.
# --show-devices indents the devices matching each policy rule.
policy=$(usbguard list-rules --label omarchy-usb-authorization-v1 --show-devices) || approval_failed
policy_allows=0
matched=0
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
policy_allows=0
if [[ $line =~ ^[0-9]+:[[:space:]]allow[[:space:]] ]]; then
policy_allows=1
fi
elif (( policy_allows )) && [[ $line == $'\t'"$id: $allowed_rule" ]]; then
matched=1
break
fi
done <<<"$policy"
(( matched )) || approval_failed
echo "USB accessory added to the trusted policy."
else
echo "USB accessory allowed until it is disconnected."
fi
;;
"Keep blocked")
echo "USB accessory remains blocked."
;;
*) exit 1 ;;
esac
rm -f "$request"
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Watch USBGuard for devices awaiting user approval
set -euo pipefail
# Starting a new graphical-session watcher must offer fresh prompts even if
# their old notification actions were dismissed and aged out of history.
OMARCHY_USB_AUTHORIZATION_WATCH_ID=$(cat /proc/sys/kernel/random/uuid)
export OMARCHY_USB_AUTHORIZATION_WATCH_ID
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
# The handler scans blocked devices on IPC.Connected, including reconnects
# after a daemon restart, once the event subscription is established.
exec usbguard watch --wait --exec "$event_handler"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# omarchy:summary=Install and configure Voxtype dictation
# omarchy:requires-sudo=true
set -e
# Install voxtype and configure it for use.
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
omarchy-pkg-add wtype voxtype-bin
# Setup voxtype
mkdir -p ~/.config/voxtype
cp "$OMARCHY_PATH/default/voxtype/config.toml" ~/.config/voxtype/
voxtype setup --download --no-post-install
if omarchy-hw-vulkan; then
voxtype setup gpu --enable || true
fi
voxtype setup systemd
hyprctl reload >/dev/null
omarchy-restart-shell
omarchy-notification-send -g  "Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
fi
@@ -18,10 +18,6 @@ if omarchy-cmd-present voxtype; then
omarchy-pkg-drop voxtype-bin omarchy-pkg-drop voxtype-bin
rm -rf ~/.config/voxtype rm -rf ~/.config/voxtype
rm -rf ~/.local/share/voxtype rm -rf ~/.local/share/voxtype
backend_config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
if [[ -f $backend_config && $(< "$backend_config") == "voxtype" ]]; then
rm "$backend_config"
fi
hyprctl reload >/dev/null hyprctl reload >/dev/null
else else
echo "Voxtype was not installed." echo "Voxtype was not installed."
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# omarchy:summary=Stream voxtype --follow status as bar-friendly JSON
if omarchy-cmd-missing voxtype; then
echo '{"alt": "", "class": "idle", "tooltip": ""}'
exit 0
fi
# Keep the status follower as the direct child of the bar. Quickshell
# clean up their direct child when they restart; a shell pipeline/coproc would
# leave `voxtype status --follow` orphaned under the user systemd instance.
exec setpriv --pdeathsig TERM voxtype status --follow --extended --format json
-6
View File
@@ -4,12 +4,6 @@
# omarchy:args=<install|remove|launch|stop|status> [options] # omarchy:args=<install|remove|launch|stop|status> [options]
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
# The VM is an x86_64 Windows guest under KVM, which only an x86_64 host runs.
if ! omarchy-hw-x86; then
echo "Windows VM needs an x86_64 machine (it runs an x86_64 guest under KVM)." >&2
exit 1
fi
# The Windows VM runs a privileged container (KVM, /dev/net/tun, NET_ADMIN), so # The Windows VM runs a privileged container (KVM, /dev/net/tun, NET_ADMIN), so
# it needs the root-owned Docker daemon. By default the user is NOT in the docker # it needs the root-owned Docker daemon. By default the user is NOT in the docker
# group (that group is root-equivalent), so Docker access is gated behind a # group (that group is root-equivalent), so Docker access is gated behind a
+1 -1
View File
@@ -1,5 +1,5 @@
--ozone-platform=wayland --ozone-platform=wayland
--ozone-platform-hint=wayland --ozone-platform-hint=wayland
--password-store=gnome-libsecret --password-store=gnome-libsecret
--enable-features=TouchpadOverscrollHistoryNavigation,OverlayScrollbar --enable-features=TouchpadOverscrollHistoryNavigation
--load-extension=/usr/share/omarchy/default/chromium/extensions/copy-url,/usr/share/omarchy/default/chromium/extensions/yt-dlp,/usr/share/omarchy/default/chromium/extensions/whatsapp-slim --load-extension=/usr/share/omarchy/default/chromium/extensions/copy-url,/usr/share/omarchy/default/chromium/extensions/yt-dlp,/usr/share/omarchy/default/chromium/extensions/whatsapp-slim
-3
View File
@@ -1,3 +0,0 @@
# Keep Pinyin offline without prompting to enable cloud prediction.
FirstRun=False
CloudPinyinEnabled=False
-2
View File
@@ -1,2 +0,0 @@
[TriggerKey]
0=Super+semicolon
-1
View File
@@ -1 +0,0 @@
Allow Overriding System XKB Settings=False
-19
View File
@@ -1,19 +0,0 @@
# Omarchy binds Super+I. Leave Ctrl+Space to tmux/Herdr and other Super keys to the desktop.
[Hotkey/TriggerKeys]
0=Zenkaku_Hankaku
1=Hangul
[Hotkey/ActivateKeys]
0=Henkan
[Hotkey/DeactivateKeys]
0=Muhenkan
[Hotkey/AltTriggerKeys]
[Hotkey/EnumerateGroupForwardKeys]
[Hotkey/EnumerateGroupBackwardKeys]
[Behavior]
ActiveByDefault=False
-1
View File
@@ -1 +0,0 @@
superwhisper
@@ -11,7 +11,7 @@
# #
# To put it into use, remove .sample from this file name. # To put it into use, remove .sample from this file name.
# Example: add an Include line above the first repository, [omarchy]. # Example: add an Include line above [core].
# Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself. # Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself.
CONF=/etc/pacman.conf CONF=/etc/pacman.conf
@@ -21,4 +21,4 @@ MARKER="Include = $SNIPPET"
[[ -r $SNIPPET ]] || exit 0 [[ -r $SNIPPET ]] || exit 0
grep -qxF "$MARKER" "$CONF" && exit 0 grep -qxF "$MARKER" "$CONF" && exit 0
sudo sed -i "0,/^\[omarchy\]/s||$MARKER\n\n[omarchy]|" "$CONF" sudo sed -i "0,/^\[core\]/s||$MARKER\n\n[core]|" "$CONF"
+4 -4
View File
@@ -21,6 +21,9 @@
{ {
"id": "omarchy.indicators" "id": "omarchy.indicators"
}, },
{
"id": "omarchy.elsewhen"
},
{ {
"id": "omarchy.clock", "id": "omarchy.clock",
"format": "dddd HH:mm", "format": "dddd HH:mm",
@@ -28,7 +31,7 @@
"verticalFormat": "HH\n\u2014\nmm" "verticalFormat": "HH\n\u2014\nmm"
}, },
{ {
"id": "omarchy.elsewhen" "id": "omarchy.keyboard-layout"
}, },
{ {
"id": "omarchy.weather" "id": "omarchy.weather"
@@ -38,9 +41,6 @@
} }
], ],
"right": [ "right": [
{
"id": "omarchy.keyboard-layout"
},
{ {
"id": "omarchy.tray" "id": "omarchy.tray"
}, },
+4 -8
View File
@@ -6,16 +6,12 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
-- Launcher, image selector, emojis, clipboard overlays, the OSD, reminders, the -- Launcher, image selector, emojis, clipboard overlays, the OSD, reminders, the
-- Wi-Fi QR code and keyboard-driven panels should pop without compositor layer -- Wi-Fi QR code and keyboard-driven panels should pop without compositor layer
-- animations. Overlays open as soon as their scale is ready, without a -- animations. The overlays stay mapped between opens and grow from a parked
-- compositor fade or slide. Panels keep their own QML opacity transition -- 1x1 when shown, which Hyprland would otherwise animate as a slide in from
-- for normal open/close, and skip it for panel handoff. -- the corner. Panels keep their own QML opacity transition for normal
-- open/close, and skip it for panel handoff.
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" }) hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" })
-- A boot intro hands the desktop between OWE's video layer and the shell's
-- background layer. The media fades itself, so a compositor fade on either
-- layer only dips the screen toward the empty desktop during the handoff.
hl.layer_rule({ match = { namespace = "^(omarchy-background|owe-background)$" }, no_anim = true, animation = "none" })
-- Dev gallery is the main shell workbench; open it maximized like -- Dev gallery is the main shell workbench; open it maximized like
-- SUPER+ALT+F so component previews have the whole workspace. -- SUPER+ALT+F so component previews have the whole workspace.
o.window({ class = "^org.quickshell$", title = "^Omarchy shell – dev gallery$" }, { maximize = true }) o.window({ class = "^org.quickshell$", title = "^Omarchy shell – dev gallery$" }, { maximize = true })
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff. Show more