Compare commits
94
Commits
No files matched your search
@@ -63,6 +63,30 @@ 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.
|
||||
- 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
|
||||
|
||||
- Follow the "Privilege Escalation" section of `default/agents/skills/omarchy/SKILL.md`. It draws the
|
||||
@@ -80,7 +104,7 @@ Use these instead of raw shell commands:
|
||||
|
||||
- `omarchy-cmd-missing` / `omarchy-cmd-present` - check for commands
|
||||
- `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages (don't use these if you can just use `omarchy-pkg-add`/`omarchy-pkg-drop`)
|
||||
- `omarchy-pkg-add` - install packages (handles both pacman and AUR)
|
||||
- `omarchy-pkg-add` - install packages from the configured pacman repos, Omarchy's included; use `omarchy-pkg-aur-add` for AUR packages
|
||||
- `omarchy-pkg-drop` - remove packages; use this instead of raw `pacman -R*`
|
||||
- `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly
|
||||
- `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands)
|
||||
@@ -89,6 +113,10 @@ Commands installed by Omarchy's default package set are runtime invariants. Invo
|
||||
|
||||
Exceptions are allowed for migration and package-helper scripts where the helper may not be available yet, where the helper itself is being implemented, or where direct package-manager behavior is required.
|
||||
|
||||
# Background Work
|
||||
|
||||
- Long-running code (the shell, daemons, watchers) reacts to events instead of polling with a timer. Polling is the last resort, for state with no change signal; [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) lists the event sources and the push-refresh pattern.
|
||||
|
||||
# Menu
|
||||
|
||||
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
|
||||
|
||||
@@ -12,6 +12,8 @@ commands and reusable setup leaves:
|
||||
- avoid `exit` in sourced setup scripts unless intentionally aborting setup.
|
||||
- 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`.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -38,6 +38,17 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
||||
bar widgets register one target each — `omarchy.indicators`,
|
||||
`omarchy.system-update`, `omarchy.clock`. There is no `bar` target.
|
||||
|
||||
## Keeping state current without polling
|
||||
|
||||
The shell runs all day on battery, and a widget is built once per monitor, so a `Timer` that starts a `Process` costs that process times the number of monitors, every tick, forever. Don't poll for state that only changes through known events. Pick the first of these that fits:
|
||||
|
||||
- A native Quickshell service that signals on change: `UPower` and `PowerProfiles` (`Quickshell.Services.UPower`), `Hyprland` and each workspace's `lastIpcObject`, `Pipewire`, `Mpris`, `SystemTray`, `Notifications`.
|
||||
- An event stream the shell already runs, such as the Hyprland IPC events or the long-running `pactl subscribe` in `Commons/UntypedInput.qml`. Extend it rather than starting a second one.
|
||||
- A file watch: `FileView` with `watchChanges`, or the directory watch in `plugins/bar/Bar.qml`, when the state lives in a file the user can read.
|
||||
- A push from the command that changes the state. If only Omarchy commands change it, have them call `omarchy-shell -q <target> refresh` after the change, the way `omarchy-reminder` and `omarchy-sudo-passwordless` do. When the change happens later, without a command running (an expiry, say), schedule the refresh with a transient `systemd-run --user --on-active=…` timer. A root-owned timer cannot reach the user's shell.
|
||||
|
||||
Polling is the last resort, kept for state with no change signal at all (a remote API, say). Then gate the timer on the panel being open where you can, pick the longest interval the UI can tolerate, and put the poll in one shared service rather than in a per-monitor widget. Never poll a privileged probe: every `sudo -n` can write to the auth log.
|
||||
|
||||
## Editing widget files with glyphs
|
||||
|
||||
Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Basecamp
|
||||
Exec=omarchy-launch-webapp https://launchpad.37signals.com
|
||||
StartupWMClass=chrome-launchpad.37signals.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=basecamp
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Discord
|
||||
Exec=omarchy-launch-webapp https://discord.com/channels/@me
|
||||
StartupWMClass=chrome-discord.com__channels_@me-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=omarchy-discord
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Google Contacts
|
||||
Exec=omarchy-launch-webapp https://contacts.google.com/
|
||||
StartupWMClass=chrome-contacts.google.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-contacts
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Google Maps
|
||||
Exec=omarchy-launch-webapp https://maps.google.com
|
||||
StartupWMClass=chrome-maps.google.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-maps
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Google Messages
|
||||
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
|
||||
StartupWMClass=chrome-messages.google.com__web_conversations-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-messages
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=Google Photos
|
||||
Exec=omarchy-launch-webapp https://photos.google.com/
|
||||
StartupWMClass=chrome-photos.google.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-photos
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=WhatsApp
|
||||
Exec=omarchy-launch-webapp https://web.whatsapp.com/
|
||||
StartupWMClass=chrome-web.whatsapp.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=whatsapp
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=X
|
||||
Exec=omarchy-launch-webapp https://x.com/
|
||||
StartupWMClass=chrome-x.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=x
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Version=1.0
|
||||
Name=YouTube
|
||||
Exec=omarchy-launch-webapp https://youtube.com/
|
||||
StartupWMClass=chrome-youtube.com__-Default
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=youtube
|
||||
|
||||
@@ -19,6 +19,9 @@ for diagnostics or after hardware/package updates.
|
||||
|
||||
--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.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -66,10 +69,21 @@ else
|
||||
fi
|
||||
|
||||
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_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}"
|
||||
export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}"
|
||||
export PATH="$OMARCHY_PATH/bin:$PATH"
|
||||
|
||||
source "$OMARCHY_INSTALL/helpers/logging.sh"
|
||||
source "$OMARCHY_INSTALL/hardware/all.sh"
|
||||
source "$OMARCHY_INSTALL/helpers/image-target.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
|
||||
@@ -15,6 +15,14 @@ fi
|
||||
wpctl set-default "$node_id" 2>/dev/null || true
|
||||
pactl set-default-source "$source_name" 2>/dev/null || true
|
||||
|
||||
pactl list short source-outputs 2>/dev/null | awk '{ print $1 }' | while read -r output; do
|
||||
# A filter's own capture (the input half of a filter chain, loopback or echo
|
||||
# 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
|
||||
done
|
||||
@@ -1,14 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Print PulseAudio sink availability for the shell
|
||||
# omarchy:summary=Print PulseAudio sink or source availability for the shell
|
||||
# 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
|
||||
# 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.
|
||||
fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
|
||||
fronted=""
|
||||
[[ $kind == sinks ]] && fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
|
||||
|
||||
pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
|
||||
LC_ALL=C pactl list "$kind" 2>/dev/null | awk -v fronted="$fronted" -v header="$header" '
|
||||
function emit_sink() {
|
||||
if (name == "") return
|
||||
if (fronted != "" && name == fronted) {
|
||||
@@ -18,7 +31,7 @@ pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
|
||||
print name "\t" ((port_count == 0 || available) ? 1 : 0)
|
||||
}
|
||||
|
||||
/^Sink #/ {
|
||||
index($0, header) == 1 {
|
||||
emit_sink()
|
||||
name = ""
|
||||
in_ports = 0
|
||||
|
||||
+104
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
# omarchy:summary=Configure the bar and its widget layout
|
||||
# omarchy:group=bar
|
||||
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement]
|
||||
# omarchy:args=use <id> | reset | defaults | position [top|bottom|left|right] | background [opaque|glassy|none|cycle] | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement] | get <id> <key>
|
||||
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar put omarchy.keyboard-layout --after omarchy.clock | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm
|
||||
|
||||
set -euo pipefail
|
||||
@@ -16,13 +16,19 @@ Usage: omarchy bar <command> [args...]
|
||||
use <id> Use a bar option as the active bar
|
||||
reset Return to the built-in Omarchy bar
|
||||
defaults Restore the default bar and service widgets
|
||||
position <top|bottom|left|right> Bar position
|
||||
transparent <true|false|toggle> Bar transparency
|
||||
position [top|bottom|left|right] Bar position; prints the current one
|
||||
when given none
|
||||
background [opaque|glassy|none|cycle]
|
||||
Bar background: glassy shows what is
|
||||
behind the bar blurred, cycle steps
|
||||
none, glassy, opaque; prints the
|
||||
current one when given none
|
||||
put <id> [placement] Put a widget on the bar, leaving one
|
||||
that is already there where it is
|
||||
move <id> [placement] Move a widget within or between sections
|
||||
set <id> <key> <value> [--json] [placement]
|
||||
Set a per-widget option
|
||||
get <id> <key> Print a per-widget option
|
||||
|
||||
Placement:
|
||||
--section <left|center|right> Target section
|
||||
@@ -192,30 +198,92 @@ cmd_defaults() {
|
||||
" \
|
||||
--slurpfile defaults "$DEFAULTS_FILE" \
|
||||
--argjson widgets "$optional_widgets"
|
||||
sync_glass || fail "could not change the blur behind the bar"
|
||||
echo "Restored the default Omarchy bar"
|
||||
}
|
||||
|
||||
cmd_position() {
|
||||
local position="${1:-}"
|
||||
[[ -n $position ]] || fail "position is required"
|
||||
if (( $# == 0 )); then
|
||||
jq -r '.bar.position // "top"' "$(source_file)"
|
||||
return
|
||||
fi
|
||||
|
||||
local position="$1"
|
||||
(( $# == 1 )) || fail "position takes a single value"
|
||||
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
|
||||
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
|
||||
echo "Bar position set to $position"
|
||||
}
|
||||
|
||||
# The bar background is opaque, glassy, or none. A shell.json from before it
|
||||
# had one says transparent instead, which is none, and setting a background
|
||||
# drops it.
|
||||
current_background() {
|
||||
jq -r '.bar.background // (if .bar.transparent == true then "none" else "opaque" end)' "$(source_file)"
|
||||
}
|
||||
|
||||
cmd_background() {
|
||||
if (( $# == 0 )); then
|
||||
current_background
|
||||
return
|
||||
fi
|
||||
|
||||
local background="$1"
|
||||
(( $# == 1 )) || fail "background takes a single value"
|
||||
[[ $background =~ ^(opaque|glassy|none|cycle)$ ]] || fail "background must be opaque, glassy, none, or cycle"
|
||||
|
||||
if [[ $background == "cycle" ]]; then
|
||||
# Each step changes the bar by one degree: clear, then glassy, then opaque.
|
||||
case $(current_background) in
|
||||
none) background="glassy" ;;
|
||||
glassy) background="opaque" ;;
|
||||
*) background="none" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
local previous
|
||||
previous=$(current_background)
|
||||
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$background"
|
||||
if ! sync_glass; then
|
||||
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$previous"
|
||||
fail "could not change the blur behind the bar"
|
||||
fi
|
||||
echo "Bar background set to $background"
|
||||
}
|
||||
|
||||
# The older on/off form of the background, kept for scripts and dotfiles:
|
||||
# transparent is none, and not transparent is opaque.
|
||||
cmd_transparent() {
|
||||
local transparent="${1:-}"
|
||||
[[ -n $transparent ]] || fail "transparent is required"
|
||||
(( $# == 1 )) || fail "transparent takes a single value"
|
||||
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
|
||||
if [[ $transparent == "toggle" ]]; then
|
||||
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)"
|
||||
echo "Bar transparency toggled"
|
||||
else
|
||||
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
|
||||
echo "Bar transparency set to $transparent"
|
||||
[[ $(current_background) == "none" ]] && transparent="false" || transparent="true"
|
||||
fi
|
||||
if [[ $transparent == "true" ]]; then
|
||||
cmd_background none
|
||||
else
|
||||
cmd_background opaque
|
||||
fi
|
||||
}
|
||||
|
||||
# Hyprland blurs behind the bar only while the bar-glassy toggle is on, so it
|
||||
# follows the bar background, reloading Hyprland only when it changes. The
|
||||
# toggle sits where Hyprland reads it, under XDG_STATE_HOME.
|
||||
sync_glass() {
|
||||
local dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
|
||||
local flag="$dir/bar-glassy.lua"
|
||||
|
||||
if [[ $(current_background) == "glassy" ]]; then
|
||||
[[ ! -f $flag ]] || return 0
|
||||
mkdir -p "$dir" && cp "$OMARCHY_PATH/default/hypr/toggles/bar-glassy.lua" "$flag" || return 1
|
||||
else
|
||||
[[ -f $flag ]] || return 0
|
||||
rm -f "$flag" || return 1
|
||||
fi
|
||||
|
||||
hyprctl reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
bar_widget_default_section() {
|
||||
@@ -363,6 +431,21 @@ cmd_set() {
|
||||
echo "Set $key on $id"
|
||||
}
|
||||
|
||||
# Prints the option from the first instance of the widget on the bar, raw for a
|
||||
# string and as JSON otherwise, and fails when no instance sets it.
|
||||
cmd_get() {
|
||||
local id="${1:-}"
|
||||
local key="${2:-}"
|
||||
[[ -n $id ]] || fail "get requires a widget id"
|
||||
[[ -n $key ]] || fail "get requires a setting key"
|
||||
(( $# == 2 )) || fail "get takes a widget id and a setting key"
|
||||
|
||||
jq -e -r --arg id "$id" --arg key "$key" '
|
||||
first(.bar.layout[]?[]? | select(.id == $id) | .[$key] | select(. != null))
|
||||
| if type == "string" then . else tojson end
|
||||
' "$(source_file)" 2>/dev/null || exit 1
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------- dispatch
|
||||
|
||||
command="${1:-}"
|
||||
@@ -385,6 +468,14 @@ case "$command" in
|
||||
transparent)
|
||||
cmd_transparent "$@"
|
||||
;;
|
||||
background)
|
||||
cmd_background "$@"
|
||||
;;
|
||||
# Internal: the shell runs this when it loads a bar background, so one set
|
||||
# by editing shell.json gets its blur too.
|
||||
sync-glass)
|
||||
sync_glass || fail "could not change the blur behind the bar"
|
||||
;;
|
||||
put)
|
||||
cmd_put "$@"
|
||||
;;
|
||||
@@ -394,6 +485,9 @@ case "$command" in
|
||||
set)
|
||||
cmd_set "$@"
|
||||
;;
|
||||
get)
|
||||
cmd_get "$@"
|
||||
;;
|
||||
-h | --help | help | "")
|
||||
usage
|
||||
;;
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
# omarchy:summary=Returns true if a battery is present on the system.
|
||||
|
||||
for bat in /sys/class/power_supply/BAT*; do
|
||||
[[ -r $bat/present ]] &&
|
||||
[[ $(cat $bat/present) == "1" ]] &&
|
||||
[[ $(cat $bat/type) == "Battery" ]] &&
|
||||
# Not every battery is called BAT*: Apple Silicon reports macsmc-battery and
|
||||
# some Framework and Chromebook firmware CMB0, so decide by type, not by name.
|
||||
power_supply_path="${OMARCHY_POWER_SUPPLY_PATH:-/sys/class/power_supply}"
|
||||
|
||||
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
|
||||
done
|
||||
|
||||
|
||||
@@ -18,11 +18,23 @@ case "${1:-}" in
|
||||
;;
|
||||
esac
|
||||
|
||||
battery=$(upower -e 2>/dev/null | grep BAT | head -n 1)
|
||||
# UPower names the device after the sysfs entry, and only some kernels call
|
||||
# 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
|
||||
|
||||
battery_info=$(upower -i "$battery")
|
||||
|
||||
percentage=$(awk '/percentage/ { print int($2); exit }' <<<"$battery_info")
|
||||
capacity=$(awk '/energy-full:/ { printf "%d", $2; exit }' <<<"$battery_info")
|
||||
time_remaining=$(awk '/time to (empty|full)/ {
|
||||
@@ -56,7 +68,10 @@ elif [[ -r $battery_path/current_now && -r $battery_path/voltage_now ]]; then
|
||||
'BEGIN { print microamps * microvolts / 1000000000000 }')
|
||||
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 {
|
||||
if (rate < 0) rate = -rate
|
||||
rounded = sprintf("%.1f", rate)
|
||||
sub(/\.0$/, "", rounded)
|
||||
print rounded
|
||||
@@ -65,8 +80,12 @@ state=$(awk '/state/ { print $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")
|
||||
|
||||
[[ -z $threshold_end ]] && threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
|
||||
[[ -z $threshold_start ]] && threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
|
||||
# Read the thresholds from the battery UPower resolved rather than a BAT* glob,
|
||||
# which misses every battery with another name.
|
||||
[[ -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
|
||||
for supply in "$power_supply_path"/*; do
|
||||
@@ -107,7 +126,9 @@ if [[ $shell_output == "true" ]]; then
|
||||
printf 'size\t%s\n' "${capacity}Wh"
|
||||
printf 'time\t%s\n' "$time_remaining"
|
||||
|
||||
cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
|
||||
cycles=""
|
||||
[[ ! -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"
|
||||
|
||||
|
||||
@@ -40,7 +40,30 @@ use_apple_display() {
|
||||
}
|
||||
|
||||
use_ddc_display() {
|
||||
[[ -n $monitor ]] && ! monitor_is_internal
|
||||
[[ -n $monitor ]] && ! monitor_is_internal && monitor_may_have_ddc
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -50,6 +73,8 @@ if (( $# == 0 )); then
|
||||
elif use_ddc_display; then
|
||||
omarchy-brightness-display-ddc "$monitor"
|
||||
exit
|
||||
elif ! use_backlight_display; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
device="$(omarchy-hw-display)" || exit 1
|
||||
@@ -85,6 +110,8 @@ if use_apple_display; then
|
||||
elif use_ddc_display; then
|
||||
brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1
|
||||
(( no_osd )) || omarchy-osd -i brightness -p "$brightness"
|
||||
elif ! use_backlight_display; then
|
||||
exit 1
|
||||
else
|
||||
# Current device highlighted
|
||||
device="$(omarchy-hw-display)" || exit 1
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# omarchy:summary=Start or stop screen recording
|
||||
# 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]
|
||||
# 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:examples=omarchy screenrecord | omarchy capture screenrecording --with-desktop-audio
|
||||
# omarchy:aliases=omarchy screenrecord
|
||||
#
|
||||
@@ -14,19 +14,20 @@
|
||||
# DMA-BUF modifier import on some configurations, leaving recording unable to
|
||||
# start.
|
||||
#
|
||||
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends gpu-screen-recorder's stderr (and
|
||||
# the picker target it was launched with) to
|
||||
# $XDG_RUNTIME_DIR/omarchy-screenrecord.log so users can attach a log when
|
||||
# reporting capture failures.
|
||||
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends the recorder's stderr (and the
|
||||
# picker target it was launched with) to $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
|
||||
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"
|
||||
MICROPHONE_AUDIO="false"
|
||||
WEBCAM="false"
|
||||
@@ -35,6 +36,7 @@ WEBCAM_SIZE="medium"
|
||||
RESOLUTION=""
|
||||
FULLSCREEN="false"
|
||||
STOP_RECORDING="false"
|
||||
STATUS="false"
|
||||
# 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
|
||||
# beside it, `mv` replaces it, and `rm -f` deletes its preview -- so a name in
|
||||
@@ -46,6 +48,17 @@ STOP_RECORDING="false"
|
||||
RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
|
||||
RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename"
|
||||
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")
|
||||
|
||||
for arg in "$@"; do
|
||||
@@ -58,6 +71,7 @@ for arg in "$@"; do
|
||||
--resolution=*) RESOLUTION="${arg#*=}" ;;
|
||||
--fullscreen) FULLSCREEN="true" ;;
|
||||
--stop-recording) STOP_RECORDING="true" ;;
|
||||
--status) STATUS="true" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -69,6 +83,13 @@ small | medium | large) ;;
|
||||
;;
|
||||
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() {
|
||||
cleanup_webcam
|
||||
|
||||
@@ -99,7 +120,7 @@ start_webcam_overlay() {
|
||||
'--vf=lavfi=[crop=ih*8/9:ih]' \
|
||||
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
|
||||
--no-border --no-audio --no-osc --osd-level=0 \
|
||||
--really-quiet &>/dev/null &
|
||||
--really-quiet 9>&- &>/dev/null &
|
||||
|
||||
# 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
|
||||
@@ -153,12 +174,132 @@ select_capture_target() {
|
||||
echo "region:${BASH_REMATCH[3]}x${BASH_REMATCH[4]}+${BASH_REMATCH[1]}+${BASH_REMATCH[2]}"
|
||||
}
|
||||
|
||||
start_screenrecording() {
|
||||
mkdir -p "$RUNTIME_DIR" || return 1
|
||||
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
|
||||
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
|
||||
chmod 700 "$RUNTIME_DIR" || return 1
|
||||
# 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() {
|
||||
# A pid left by a recording that ended without a stop would otherwise hide
|
||||
# this one from the toggle until it has started and saved its own.
|
||||
rm -f "$REC_PID_FILE"
|
||||
local capture_args=()
|
||||
local target
|
||||
|
||||
@@ -174,6 +315,7 @@ start_screenrecording() {
|
||||
capture_args=(-w portal -s "${RESOLUTION:-$(default_resolution)}")
|
||||
else
|
||||
target=$(select_capture_target) || return 1
|
||||
start_cancelled && return 1
|
||||
|
||||
case $target in
|
||||
monitor:*)
|
||||
@@ -187,50 +329,101 @@ start_screenrecording() {
|
||||
fi
|
||||
|
||||
[[ $WEBCAM == "true" ]] && start_webcam_overlay "$target"
|
||||
start_cancelled && return 1
|
||||
|
||||
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
|
||||
local audio_devices=""
|
||||
local audio_args=()
|
||||
local backend pid=""
|
||||
|
||||
[[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output"
|
||||
for backend in $(recorder_backends); do
|
||||
[[ $backend == "wf-recorder" && $target == "portal" ]] && continue
|
||||
echo "===== $(date '+%F %T') args: $* backend: $backend target: $target =====" >>"$LOG_FILE"
|
||||
|
||||
if [[ $MICROPHONE_AUDIO == "true" ]]; then
|
||||
# Merge audio tracks into one - separate tracks only play one at a time in most players
|
||||
[[ -n $audio_devices ]] && audio_devices+="|"
|
||||
audio_devices+="default_input"
|
||||
fi
|
||||
if [[ $backend == "wf-recorder" ]]; then
|
||||
launch_wf_recorder "$target" "$filename" || continue
|
||||
pid=$REC_PID
|
||||
else
|
||||
local audio_devices=""
|
||||
local audio_args=()
|
||||
|
||||
[[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac)
|
||||
[[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output"
|
||||
|
||||
echo "===== $(date '+%F %T') args: $* target: $target =====" >>"$LOG_FILE"
|
||||
gpu-screen-recorder "${capture_args[@]}" -k auto -f 60 -fm cfr -fallback-cpu-encoding yes -o "$filename" "${audio_args[@]}" 2>>"$LOG_FILE" &
|
||||
local pid=$!
|
||||
if [[ $MICROPHONE_AUDIO == "true" ]]; then
|
||||
# Merge audio tracks into one - separate tracks only play one at a time in most players
|
||||
[[ -n $audio_devices ]] && audio_devices+="|"
|
||||
audio_devices+="default_input"
|
||||
fi
|
||||
|
||||
while kill -0 $pid 2>/dev/null && [[ ! -f $filename ]]; do
|
||||
sleep 0.2
|
||||
[[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac)
|
||||
|
||||
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
|
||||
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
echo "$filename" >"$RECORDING_FILE"
|
||||
toggle_screenrecording_indicator
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# 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() {
|
||||
pkill -SIGINT -f "^gpu-screen-recorder" # SIGINT required to save video properly
|
||||
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal INT # SIGINT required to save video properly
|
||||
|
||||
# Wait a maximum of 5 seconds to finish before hard killing
|
||||
local count=0
|
||||
while pgrep -f "^gpu-screen-recorder" >/dev/null && ((count < 50)); do
|
||||
while omarchy-capture-screenrecording-process "${RECORDER[@]}" && ((count < 50)); do
|
||||
sleep 0.1
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
toggle_screenrecording_indicator
|
||||
cleanup_webcam
|
||||
wf_audio_cleanup
|
||||
|
||||
if pgrep -f "^gpu-screen-recorder" >/dev/null; then
|
||||
pkill -9 -f "^gpu-screen-recorder"
|
||||
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
|
||||
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL
|
||||
omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted."
|
||||
else
|
||||
finalize_recording
|
||||
@@ -251,10 +444,10 @@ stop_screenrecording() {
|
||||
(
|
||||
sleep 2
|
||||
rm -f "$preview"
|
||||
) &
|
||||
) 9>&- &
|
||||
fi
|
||||
|
||||
rm -f "$RECORDING_FILE"
|
||||
rm -f "$RECORDING_FILE" "$REC_PID_FILE"
|
||||
}
|
||||
|
||||
toggle_screenrecording_indicator() {
|
||||
@@ -262,7 +455,14 @@ toggle_screenrecording_indicator() {
|
||||
}
|
||||
|
||||
screenrecording_active() {
|
||||
pgrep -f "^gpu-screen-recorder" >/dev/null
|
||||
if select_recorder && omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
|
||||
return 0
|
||||
else
|
||||
if [[ -f $REC_PID_FILE ]]; then
|
||||
wf_audio_cleanup
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
finalize_recording() {
|
||||
@@ -283,7 +483,10 @@ finalize_recording() {
|
||||
# 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
|
||||
# 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
|
||||
|
||||
local processed="${latest%.mp4}-processed.mp4"
|
||||
@@ -294,10 +497,66 @@ finalize_recording() {
|
||||
fi
|
||||
}
|
||||
|
||||
if screenrecording_active; then
|
||||
stop_screenrecording
|
||||
elif [[ $STOP_RECORDING == "true" ]]; then
|
||||
exit 1
|
||||
else
|
||||
start_screenrecording || cleanup_webcam
|
||||
mkdir -p "$RUNTIME_DIR" || exit 1
|
||||
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
|
||||
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
|
||||
chmod 700 "$RUNTIME_DIR" || exit 1
|
||||
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"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/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
|
||||
+31
-3
@@ -60,6 +60,23 @@ link_dev_checkout() {
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
@@ -102,9 +119,6 @@ case "$channel" in
|
||||
packages=(omarchy-dev omarchy-settings-dev)
|
||||
;;
|
||||
dev)
|
||||
confirm_dev || { echo "Cancelled."; exit 0; }
|
||||
dev_checkout="$HOME/omarchy"
|
||||
validate_dev_checkout "$dev_checkout"
|
||||
pacman_channel=edge
|
||||
packages=(omarchy-dev omarchy-settings-dev)
|
||||
;;
|
||||
@@ -115,6 +129,20 @@ case "$channel" in
|
||||
;;
|
||||
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
|
||||
# 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
|
||||
|
||||
+419
-19
@@ -1,32 +1,432 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set a new encryption password for a drive selected.
|
||||
# omarchy:summary=Set a new password for an encrypted drive
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
encrypted_drives=$(blkid -t TYPE=crypto_LUKS -o device)
|
||||
# Changes a drive's LUKS key and nothing else. For the disk holding /, the new
|
||||
# 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
|
||||
drive_to_change="$encrypted_drives"
|
||||
drive="$encrypted_drives"
|
||||
else
|
||||
drive_to_change="$(omarchy-drive-select "$encrypted_drives")"
|
||||
drive="$(omarchy-drive-select "$encrypted_drives")"
|
||||
fi
|
||||
|
||||
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
|
||||
if [[ -z $drive ]]; then
|
||||
echo "No drive selected."
|
||||
return 0
|
||||
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
|
||||
echo "No encrypted drives available."
|
||||
exit 1
|
||||
change_selected_drive
|
||||
fi
|
||||
@@ -13,6 +13,12 @@ for arg in "$@"; do
|
||||
esac
|
||||
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
|
||||
echo -e "Hibernation is not supported on your system" >&2
|
||||
exit 0
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Detect whether the CPU is aarch64 (every ARM machine, Apple Silicon included).
|
||||
|
||||
[[ $(uname -m) == "aarch64" ]]
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/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" ]]
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/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"
|
||||
+40
-6
@@ -5,14 +5,48 @@
|
||||
|
||||
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.
|
||||
# Glob the candidates in the loop list: [[ ]] does not do pathname expansion.
|
||||
# The Touch Bar on T2 Macs registers a backlight that never drives the display panel.
|
||||
device="$(ls -1 "$backlight_path" 2>/dev/null | grep -vx appletb_backlight | head -n1)"
|
||||
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.
|
||||
for candidate in "$backlight_path"/gmux_backlight "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do
|
||||
if [[ -e $candidate ]]; then
|
||||
# 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.
|
||||
for candidate in "$backlight_path"/gmux_backlight "${prefers[@]/#/$backlight_path/}" "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do
|
||||
if [[ -e $candidate ]] && usable "${candidate##*/}"; then
|
||||
device="${candidate##*/}"
|
||||
break
|
||||
fi
|
||||
|
||||
+26
-2
@@ -2,15 +2,39 @@
|
||||
|
||||
# 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.
|
||||
for state in /proc/acpi/button/lid/*/state; do
|
||||
for state in "${acpi_states[@]}"; do
|
||||
[[ -e $state ]] && exit 0
|
||||
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
|
||||
# button. 8=Portable 9=Laptop 10=Notebook 14=Sub Notebook 30=Tablet
|
||||
# 31=Convertible 32=Detachable.
|
||||
case $(< /sys/class/dmi/id/chassis_type 2>/dev/null) in
|
||||
chassis_type=""
|
||||
read -r chassis_type <"$dmi_chassis" 2>/dev/null || true
|
||||
case $chassis_type in
|
||||
8 | 9 | 10 | 14 | 30 | 31 | 32) exit 0 ;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -3,9 +3,45 @@
|
||||
# omarchy:summary=Returns true when the laptop lid is closed
|
||||
# omarchy:hidden=true
|
||||
|
||||
for state in /proc/acpi/button/lid/*/state; do
|
||||
[[ -r $state ]] || continue
|
||||
[[ $(< "$state") == *"closed"* ]] && exit 0
|
||||
done
|
||||
acpi_lid="${OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid}"
|
||||
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
|
||||
|
||||
shopt -s nullglob
|
||||
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
|
||||
Executable
+202
@@ -0,0 +1,202 @@
|
||||
#!/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
|
||||
+26
-1
@@ -2,5 +2,30 @@
|
||||
|
||||
# omarchy:summary=Print the detected Hyprland touchpad or trackpad device name
|
||||
|
||||
device=$(hyprctl devices -j | jq -r '[.mice[] | .name | select(test("touchpad|trackpad"; "i"))] | first // empty')
|
||||
mice=$(hyprctl devices -j | jq -r '.mice[].name')
|
||||
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"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Detect whether the CPU is x86_64.
|
||||
|
||||
[[ $(uname -m) == "x86_64" ]]
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or set window corners: theme, sharp, or rounded
|
||||
# omarchy:args=[theme|sharp|rounded]
|
||||
# omarchy:examples=omarchy hyprland corners | omarchy hyprland corners rounded
|
||||
|
||||
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
|
||||
|
||||
# Sharp and rounded are toggle files loaded after every theme, so they override
|
||||
# the corners of whichever theme is current. Theme removes both.
|
||||
if (( $# == 0 )); then
|
||||
if [[ -f $TOGGLES_DIR/corners-sharp.lua ]]; then
|
||||
echo "sharp"
|
||||
elif [[ -f $TOGGLES_DIR/corners-rounded.lua ]]; then
|
||||
echo "rounded"
|
||||
else
|
||||
echo "theme"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
theme) LABEL="Corners follow the theme" ;;
|
||||
sharp) LABEL="Sharp corners for all themes" ;;
|
||||
rounded) LABEL="Rounded corners for all themes" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Save the new choice before dropping the old one, so a failed save keeps it.
|
||||
if [[ $1 != "theme" ]]; then
|
||||
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/corners-$1.lua" "$TOGGLES_DIR/corners-$1.lua" || exit 1
|
||||
fi
|
||||
for mode in sharp rounded; do
|
||||
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/corners-$mode.lua" || exit 1
|
||||
done
|
||||
|
||||
hyprctl reload >/dev/null || exit 1
|
||||
omarchy-notification-send -g "$LABEL"
|
||||
@@ -10,6 +10,7 @@ sync_clamshell() {
|
||||
(
|
||||
flock -n 9 || exit 0
|
||||
omarchy-hyprland-monitor-clamshell
|
||||
omarchy-system-lid-inhibit
|
||||
) 9>"$LOCK"
|
||||
}
|
||||
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Returns true when the compositor holds a session lock no other locker is alive to own
|
||||
# omarchy:args=<own-pid>
|
||||
# omarchy:hidden=true
|
||||
|
||||
# Hyprland lets any client take over a held lock (misc:allow_session_lock_restore),
|
||||
# so a shell that recovers a lock it merely sees can take the live lock screen of
|
||||
# another shell, and strand it when it exits. A lock is stranded only while no
|
||||
# other Quickshell or hyprlock is running; until then the answer is undetermined,
|
||||
# so the caller keeps asking until that locker exits or it gives up.
|
||||
#
|
||||
# Exits 0 stranded, 1 unlocked, 2 undetermined. The caller passes its own pid,
|
||||
# as a crash re-execs the shell in place.
|
||||
omarchy-hyprland-session-locked || exit
|
||||
|
||||
if pgrep -u "$UID" -x 'quickshell|qs|hyprlock' | grep -qvxF "$1"; then
|
||||
exit 2
|
||||
fi
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or set how see-through windows are: opaque, transparent, or glassy
|
||||
# omarchy:args=[opaque|transparent|glassy]
|
||||
# omarchy:examples=omarchy hyprland window transparency | omarchy hyprland window transparency glassy
|
||||
|
||||
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
|
||||
|
||||
# Opaque and glassy are toggle files loaded after every theme: opaque makes the
|
||||
# windows Omarchy makes transparent solid, and glassy blurs what is behind any
|
||||
# see-through window and gives terminals a see-through background. Transparent,
|
||||
# the default, removes both.
|
||||
current_mode() {
|
||||
if [[ -f $TOGGLES_DIR/window-opaque.lua ]]; then
|
||||
echo "opaque"
|
||||
elif [[ -f $TOGGLES_DIR/window-glassy.lua ]]; then
|
||||
echo "glassy"
|
||||
else
|
||||
echo "transparent"
|
||||
fi
|
||||
}
|
||||
|
||||
# Save the new choice before dropping the old one, so a failed save keeps it.
|
||||
save_mode() {
|
||||
if [[ $1 != "transparent" ]]; then
|
||||
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/window-$1.lua" "$TOGGLES_DIR/window-$1.lua" || return 1
|
||||
fi
|
||||
local mode
|
||||
for mode in opaque glassy; do
|
||||
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/window-$mode.lua" || return 1
|
||||
done
|
||||
}
|
||||
|
||||
if (( $# == 0 )); then
|
||||
current_mode
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
opaque) LABEL="Opaque windows" ;;
|
||||
transparent) LABEL="Transparent windows" ;;
|
||||
glassy) LABEL="Glassy windows" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# A theme switch renders the next theme's terminal files from the saved choice
|
||||
# and then replaces the current theme, so the choice and the glass written into
|
||||
# the current theme are changed under its lock, or the switch could drop them.
|
||||
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
|
||||
flock 9
|
||||
|
||||
# A choice whose glass cannot be written is taken back, so the saved choice
|
||||
# is always the one on screen.
|
||||
previous=$(current_mode)
|
||||
save_mode "$1" || exit 1
|
||||
if ! omarchy-theme-set-glass; then
|
||||
save_mode "$previous"
|
||||
omarchy-theme-set-glass
|
||||
exit 1
|
||||
fi
|
||||
flock -u 9
|
||||
|
||||
hyprctl reload >/dev/null || exit 1
|
||||
omarchy-restart-terminal
|
||||
omarchy-theme-set-foot
|
||||
omarchy-restart-btop
|
||||
omarchy-notification-send -g "$LABEL"
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Print the layout of a workspace: dwindle, scrolling, or floating
|
||||
# omarchy:args=[workspace-id]
|
||||
|
||||
if [[ -n $1 ]]; then
|
||||
WORKSPACE_ID=$1
|
||||
TILED_LAYOUT=$(hyprctl workspaces -j | jq -r --arg id "$WORKSPACE_ID" 'first(.[] | select((.id | tostring) == $id) | .tiledLayout) // empty')
|
||||
else
|
||||
read -r WORKSPACE_ID TILED_LAYOUT < <(hyprctl activeworkspace -j | jq -r '"\(.id) \(.tiledLayout // "")"')
|
||||
fi
|
||||
|
||||
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
|
||||
|
||||
# Hyprland only reports the tiled layout, and floating is not one, so the mode is
|
||||
# read back from the file omarchy-hyprland-workspace-layout-toggle wrote. Files
|
||||
# saved before floating existed name no mode, and a workspace without a mode of
|
||||
# its own follows the default layout.
|
||||
if [[ $WORKSPACE_ID =~ ^[0-9]+$ ]] && (( WORKSPACE_ID >= 1 )); then
|
||||
MODE=$(sed -n 's/.*mode = "\([a-z]*\)".*/\1/p' "$MODES_DIR/$WORKSPACE_ID.lua" 2>/dev/null)
|
||||
|
||||
if [[ -z $MODE && -f $MODES_DIR/default.lua ]]; then
|
||||
MODE=$(omarchy-hyprland-workspace-layout-default)
|
||||
fi
|
||||
fi
|
||||
|
||||
MODE=${MODE:-$TILED_LAYOUT}
|
||||
echo "${MODE:-dwindle}"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or set the layout of workspaces without one of their own: dwindle, scrolling, or floating
|
||||
# omarchy:args=[dwindle|scrolling|floating]
|
||||
# omarchy:examples=omarchy hyprland workspace layout default | omarchy hyprland workspace layout default scrolling
|
||||
|
||||
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
|
||||
DEFAULT_FILE="$MODES_DIR/default.lua"
|
||||
|
||||
# Without a saved default, Hyprland's own layout is the default, which the
|
||||
# user's looknfeel may have set.
|
||||
if (( $# == 0 )); then
|
||||
MODE=$(sed -n 's/.*default = "\([a-z]*\)".*/\1/p' "$DEFAULT_FILE" 2>/dev/null)
|
||||
[[ -n $MODE ]] || MODE=$(hyprctl getoption general.layout -j 2>/dev/null | jq -r '.str // empty')
|
||||
echo "${MODE:-dwindle}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
dwindle) LABEL=tiling ;;
|
||||
scrolling | floating) LABEL=$1 ;;
|
||||
*)
|
||||
echo "Usage: omarchy-hyprland-workspace-layout-default [dwindle|scrolling|floating]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Saved even for dwindle, so the choice also wins over a layout set in the
|
||||
# user's own looknfeel. Workspaces given a layout with Super + L keep theirs.
|
||||
mkdir -p "$MODES_DIR"
|
||||
printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
|
||||
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-shell -q omarchy.workspaces refresh
|
||||
omarchy-notification-send -g "New workspaces use the $LABEL layout"
|
||||
@@ -1,21 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Toggle the layout on the current active workspace between dwindle and scrolling
|
||||
# omarchy:summary=Cycle the current active workspace through the dwindle, scrolling, and floating layouts
|
||||
# omarchy:args=[back]
|
||||
|
||||
DIRECTION=${1:-next}
|
||||
if [[ $DIRECTION != "next" && $DIRECTION != "back" ]]; then
|
||||
echo "Usage: omarchy-hyprland-workspace-layout-toggle [back]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTIVE_WORKSPACE=$(hyprctl activeworkspace -j | jq -r '.id')
|
||||
[[ $ACTIVE_WORKSPACE =~ ^-?[0-9]+$ ]] || exit 1
|
||||
CURRENT_LAYOUT=$(hyprctl activeworkspace -j | jq -r '.tiledLayout')
|
||||
LAYOUTS_DIR="$HOME/.local/state/omarchy/workspace-layouts"
|
||||
LAYOUT_FILE="$LAYOUTS_DIR/$ACTIVE_WORKSPACE.lua"
|
||||
# Named workspaces report a negative id, and Hyprland reads a leading "-" as a
|
||||
# selector relative to the current workspace: targeting one lands on workspace 1
|
||||
# and reconfigures that instead.
|
||||
[[ $ACTIVE_WORKSPACE =~ ^[0-9]+$ ]] || exit 1
|
||||
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
|
||||
MODE_FILE="$MODES_DIR/$ACTIVE_WORKSPACE.lua"
|
||||
|
||||
case "$CURRENT_LAYOUT" in
|
||||
dwindle) NEW_LAYOUT=scrolling ;;
|
||||
*) NEW_LAYOUT=dwindle ;;
|
||||
esac
|
||||
CURRENT_MODE=$(omarchy-hyprland-workspace-layout-current)
|
||||
|
||||
mkdir -p "$LAYOUTS_DIR"
|
||||
printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKSPACE" "$NEW_LAYOUT" >"$LAYOUT_FILE"
|
||||
if [[ $DIRECTION == "back" ]]; then
|
||||
case "$CURRENT_MODE" in
|
||||
dwindle) NEW_MODE=floating ;;
|
||||
floating) NEW_MODE=scrolling ;;
|
||||
*) NEW_MODE=dwindle ;;
|
||||
esac
|
||||
else
|
||||
case "$CURRENT_MODE" in
|
||||
dwindle) NEW_MODE=scrolling ;;
|
||||
scrolling) NEW_MODE=floating ;;
|
||||
*) NEW_MODE=dwindle ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
|
||||
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_LAYOUT"
|
||||
APPLY="o.workspace_mode({ workspace = \"$ACTIVE_WORKSPACE\", mode = \"$NEW_MODE\" })"
|
||||
|
||||
# Saving a mode that did not apply would leave the file naming one layout while
|
||||
# the screen shows another, and restore it at the next login.
|
||||
APPLIED=0
|
||||
if hyprctl eval "$APPLY" >/dev/null 2>&1; then
|
||||
APPLIED=1
|
||||
fi
|
||||
|
||||
if (( ! APPLIED )); then
|
||||
omarchy-notification-send -g "Workspace layout needs a Hyprland reload"
|
||||
elif [[ $NEW_MODE == "$(omarchy-hyprland-workspace-layout-default)" ]] && rm -f "$MODE_FILE"; then
|
||||
# Cycled onto the default, a workspace goes back to following it, so a later
|
||||
# change of the default reaches it too.
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
||||
elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
||||
else
|
||||
# The mode is on screen but nothing will bring it back, and the next press
|
||||
# reads the mode from this file, so say so rather than counting it as set.
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE but could not be saved"
|
||||
fi
|
||||
|
||||
# The bar shows the focused workspace's layout.
|
||||
if (( APPLIED )); then
|
||||
omarchy-shell -q omarchy.workspaces refresh
|
||||
fi
|
||||
|
||||
# Titlebars and shadows are set up while the config loads, and only
|
||||
# when something floats, so the first floating workspace needs a reload to get
|
||||
# them. Once the titlebar plugin is in, later ones have them already.
|
||||
if (( APPLIED )) && [[ $NEW_MODE == "floating" ]] && ! hyprctl plugin list -j 2>/dev/null | jq -e 'any(.[]?; .name == "hyprbars")' >/dev/null 2>&1; then
|
||||
hyprctl reload >/dev/null
|
||||
fi
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/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
|
||||
@@ -8,6 +8,8 @@ set -e
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
opt_path="${OMARCHY_OPT_PATH:-/opt}"
|
||||
|
||||
setup_chromium_policy_directory() {
|
||||
browser_policy_setup_dir "$1"
|
||||
}
|
||||
@@ -17,6 +19,17 @@ announce_browser_installed() {
|
||||
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() {
|
||||
mkdir -p ~/.config
|
||||
cp -f "$OMARCHY_PATH/config/chromium-flags.conf" "$1"
|
||||
@@ -37,6 +50,7 @@ chromium)
|
||||
setup_chromium_policy_directory /etc/chromium/policies/managed
|
||||
copy_chromium_flags ~/.config/chromium-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
omarchy-lifecycle-dispatch post-install chromium
|
||||
announce_browser_installed "Chromium"
|
||||
;;
|
||||
chrome)
|
||||
@@ -46,6 +60,7 @@ chrome)
|
||||
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
|
||||
copy_chromium_flags ~/.config/chrome-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
omarchy-lifecycle-dispatch post-install chrome
|
||||
announce_browser_installed "Chrome"
|
||||
;;
|
||||
edge)
|
||||
@@ -55,6 +70,7 @@ edge)
|
||||
setup_chromium_policy_directory /etc/opt/edge/policies/managed
|
||||
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
omarchy-lifecycle-dispatch post-install edge
|
||||
announce_browser_installed "Edge"
|
||||
;;
|
||||
brave)
|
||||
@@ -63,7 +79,9 @@ brave)
|
||||
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-flags.conf
|
||||
link_widevine_cdm brave-bin
|
||||
omarchy-theme-set-browser
|
||||
omarchy-lifecycle-dispatch post-install brave
|
||||
announce_browser_installed "Brave"
|
||||
;;
|
||||
brave-origin)
|
||||
@@ -72,7 +90,9 @@ brave-origin)
|
||||
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-origin-flags.conf
|
||||
link_widevine_cdm brave-origin-bin
|
||||
omarchy-theme-set-browser
|
||||
omarchy-lifecycle-dispatch post-install brave-origin
|
||||
announce_browser_installed "Brave Origin"
|
||||
;;
|
||||
firefox)
|
||||
@@ -81,6 +101,7 @@ firefox)
|
||||
|
||||
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
|
||||
setup_firefox_wayland
|
||||
omarchy-lifecycle-dispatch post-install firefox
|
||||
announce_browser_installed "Firefox"
|
||||
;;
|
||||
zen)
|
||||
@@ -89,6 +110,7 @@ zen)
|
||||
|
||||
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
|
||||
setup_firefox_wayland
|
||||
omarchy-lifecycle-dispatch post-install zen
|
||||
announce_browser_installed "Zen"
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -92,6 +92,9 @@ phoenix)
|
||||
;;
|
||||
rust)
|
||||
echo -e "Installing Rust...\n"
|
||||
# Crates that generate C bindings with bindgen, including Ruby gems built
|
||||
# with rb-sys, load libclang from clang.
|
||||
omarchy-pkg-add clang
|
||||
bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y
|
||||
;;
|
||||
java)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
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
|
||||
@@ -17,7 +19,8 @@ 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/"
|
||||
if omarchy-hw-vulkan; then
|
||||
# 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
|
||||
|
||||
@@ -15,6 +15,7 @@ echo "Installing Steam..."
|
||||
# GPU.
|
||||
omarchy-install-gaming-gpu-lib32
|
||||
omarchy-pkg-add steam
|
||||
omarchy-lifecycle-dispatch post-install steam
|
||||
|
||||
echo ""
|
||||
echo "Steam will start automatically now. This might take a while..."
|
||||
|
||||
@@ -15,17 +15,15 @@ if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI
|
||||
aether \
|
||||
cliamp \
|
||||
libreoffice-fresh \
|
||||
xournalpp \
|
||||
pinta \
|
||||
obsidian \
|
||||
obs-studio \
|
||||
kdenlive \
|
||||
moonlight-qt \
|
||||
omacut \
|
||||
monologue \
|
||||
omacalc \
|
||||
omawrite \
|
||||
hype; then
|
||||
hype \
|
||||
signed; then
|
||||
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Executable
+200
@@ -0,0 +1,200 @@
|
||||
#!/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" "$@"
|
||||
@@ -9,12 +9,25 @@ declare -A LUA_BIND_KEY_MAP
|
||||
declare -A LUA_BIND_DISPATCHER_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
|
||||
# via the compiled keymap, with a small fallback for common keys so the menu
|
||||
# remains readable if xkbcli cannot resolve a symbol.
|
||||
parse_keycodes() {
|
||||
awk '
|
||||
awk -v key_names="$key_names_file" '
|
||||
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, " ")
|
||||
for (i in fallbacks) {
|
||||
separator = index(fallbacks[i], "=")
|
||||
@@ -59,6 +72,13 @@ parse_keycodes() {
|
||||
symbol = mouse_symbol[code]
|
||||
if (symbol == "") symbol = "mouse:" code
|
||||
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
|
||||
@@ -81,7 +101,9 @@ build_lua_bind_cache() {
|
||||
LUA_BIND_DISPATCHER_MAP["$cache_key"]="$dispatcher"
|
||||
LUA_BIND_ARG_MAP["$cache_key"]="$arg"
|
||||
done < <(
|
||||
lua <<'LUA'
|
||||
# -E keeps LUA_INIT and LUA_PATH from the environment out of the scan, which
|
||||
# loads the user's config.
|
||||
lua -E - <<'LUA'
|
||||
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }
|
||||
|
||||
local function split_keys(keys)
|
||||
@@ -533,8 +555,9 @@ output_binding_records_uncached() {
|
||||
|
||||
keybindings_cache_key() {
|
||||
{
|
||||
printf 'v14\n'
|
||||
printf 'v15\n'
|
||||
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
|
||||
cat "$key_names_file" 2>/dev/null
|
||||
hyprctl binds 2>/dev/null
|
||||
} | sha256sum | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
+46
-4
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Pick one option from a menu
|
||||
# omarchy:summary=Pick one or multiple options from a menu
|
||||
# omarchy:group=menu
|
||||
# omarchy:name=select
|
||||
# omarchy:args=prompt [option...] [-- menu args...]
|
||||
# omarchy:args=prompt [option...] [-- --multiple --selected <value> --on-change <command-json> --change-key <key> --width <pixels> --maxheight <pixels>]
|
||||
# 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
|
||||
@@ -12,6 +12,12 @@
|
||||
# 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 --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
|
||||
|
||||
if (( $# < 1 )); then
|
||||
@@ -25,12 +31,43 @@ shift
|
||||
options=()
|
||||
menu_width=""
|
||||
menu_maxheight=""
|
||||
multiple=0
|
||||
on_change="[]"
|
||||
change_key=""
|
||||
selected=()
|
||||
|
||||
while (( $# > 0 )); do
|
||||
if [[ $1 == "--" ]]; then
|
||||
shift
|
||||
while (( $# > 0 )); do
|
||||
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)
|
||||
shift
|
||||
if (( $# == 0 )); then
|
||||
@@ -73,20 +110,25 @@ rm -f "$done_file"
|
||||
trap 'rm -f "$selection_file" "$done_file"' EXIT
|
||||
|
||||
options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}")
|
||||
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 '
|
||||
my $payload = {
|
||||
mode => "select",
|
||||
prompt => decode("UTF-8", $ARGV[0]),
|
||||
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]),
|
||||
doneFile => decode("UTF-8", $ARGV[3])
|
||||
};
|
||||
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
|
||||
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
|
||||
print encode_json($payload)
|
||||
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight")
|
||||
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json" "$on_change" "$change_key")
|
||||
|
||||
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null
|
||||
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null || exit 2
|
||||
|
||||
while [[ ! -e $done_file ]]; do
|
||||
sleep 0.05
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/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
|
||||
@@ -56,6 +56,7 @@ if [[ -z ${profile:-} ]] || ! profile_available "$profile"; then
|
||||
fi
|
||||
|
||||
powerprofilesctl set "$profile" || exit
|
||||
omarchy-shell -q battery refreshPowerProfile >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -n $requested_profile ]]; then
|
||||
mkdir -p "$state_dir"
|
||||
|
||||
@@ -75,6 +75,10 @@ run_first_run_step "install agent setup post-update hook" \
|
||||
|
||||
run_first_run_step "enable user systemd units" \
|
||||
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" \
|
||||
bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh"
|
||||
run_first_run_step "set GTK primary paste" \
|
||||
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/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"
|
||||
+266
-91
@@ -9,7 +9,9 @@
|
||||
# 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
|
||||
# tarball, re-keys LUKS from the throwaway install passphrase to the user's
|
||||
# password, and hands off to SDDM.
|
||||
# password, and hands off to SDDM. A platform whose boot package owns its boot
|
||||
# chain prepares it, drops the install key's unlock from it and records the
|
||||
# kept key slot through omarchy-lifecycle-dispatch.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -33,6 +35,9 @@ fi
|
||||
|
||||
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
|
||||
# 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
|
||||
@@ -182,11 +187,11 @@ tips=(
|
||||
"Super + Space opens the Omarchy menu for apps, settings, and more"
|
||||
"Super + K shows all the key bindings"
|
||||
"Super is the Windows or command key on your keyboard"
|
||||
"Use Xournal++ to sign PDFs"
|
||||
"Sign PDFs and fill them in with Signed"
|
||||
"Share files with phones and laptops using LocalSend"
|
||||
"Turn any website into an app with Install > Web App in the menu"
|
||||
"Super + Return opens a terminal, Super + Shift + Return the browser"
|
||||
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
|
||||
"Edit images with Pinta, trim videos with Omacut, docs with LibreOffice"
|
||||
"Super + Ctrl + Print grabs text off the screen with OCR"
|
||||
"Print takes a screenshot, Alt + Print records the screen"
|
||||
"Set a reminder with Super + Ctrl + R"
|
||||
@@ -579,14 +584,27 @@ greeter_screen() {
|
||||
# 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
|
||||
# (live VT + persisted) so the password typed next, and the LUKS re-key below,
|
||||
# use the chosen layout.
|
||||
# use the chosen layout. A layout that did not take is asked for again: the
|
||||
# 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() {
|
||||
local status
|
||||
local status failures=0
|
||||
|
||||
while true; do
|
||||
step "Let's setup your keyboard..."
|
||||
omarchy_prompt_keyboard && status=0 || status=$?
|
||||
((status == 0)) && break
|
||||
if ((status == 0)); then
|
||||
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.
|
||||
((status == OMARCHY_FORM_BACK)) && continue
|
||||
@@ -598,7 +616,7 @@ keyboard_form() {
|
||||
confirm_reboot && exec systemctl reboot
|
||||
done
|
||||
|
||||
apply_keyboard "$keyboard"
|
||||
omarchy_persist_input_selection
|
||||
}
|
||||
|
||||
confirm_reboot() {
|
||||
@@ -609,21 +627,73 @@ confirm_reboot() {
|
||||
gum confirm --affirmative "Yes, reboot" --negative "No, keep setting up" "Reboot this machine?"
|
||||
}
|
||||
|
||||
# Load the layout on the live VT and persist it for the installed system.
|
||||
# systemd-firstboot writes both the console KEYMAP and the XKB layout Hyprland
|
||||
# reads, matching what the ISO's configure_keyboard does at install time. A
|
||||
# keymap localectl doesn't know keeps the default rather than failing (defensive;
|
||||
# the picker no longer offers any such layout).
|
||||
apply_keyboard() {
|
||||
local keymap="$1"
|
||||
[[ $(tty 2>/dev/null) == /dev/tty* ]] && loadkeys "$keymap" 2>/dev/null || true
|
||||
# XKB settings for the form's keymaps that kbd-model-map lacks, as its rows for
|
||||
# the nearest keymap give them, so the desktop types what the console does:
|
||||
# kbd's cz is QWERTY and bg-cp1251 phonetic. Non-Latin layouts keep a US layout
|
||||
# to switch to, so the shared password stays typeable.
|
||||
keyboard_xkb_settings() {
|
||||
local layout variant="" options=terminate:ctrl_alt_bksp
|
||||
local switch=terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
|
||||
|
||||
if localectl --no-pager list-keymaps 2>/dev/null | grep -qix "$keymap"; then
|
||||
systemd-firstboot --keymap="$keymap" --force >>"$LOG_FILE" 2>&1 || \
|
||||
localectl set-keymap "$keymap" >>"$LOG_FILE" 2>&1 || \
|
||||
log_step "could not persist keymap $keymap"
|
||||
else
|
||||
log_step "keymap $keymap unknown to localectl; keeping the default"
|
||||
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() {
|
||||
local keymap=$1 keymaps saved
|
||||
|
||||
if [[ $(tty 2>/dev/null) == /dev/tty* ]] && ! loadkeys "$keymap" >>"$LOG_FILE" 2>&1; then
|
||||
log_step "loadkeys could not load keymap $keymap"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! keymaps=$(localectl --no-pager list-keymaps 2>>"$LOG_FILE"); then
|
||||
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
|
||||
}
|
||||
|
||||
@@ -714,7 +784,7 @@ luks_device() {
|
||||
local src part
|
||||
src=$(findmnt -no SOURCE / | sed 's/\[.*//')
|
||||
[[ -n $src ]] || return 1
|
||||
part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
|
||||
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
|
||||
[[ -n $part ]] && { echo "$part"; return 0; }
|
||||
return 1
|
||||
}
|
||||
@@ -822,13 +892,24 @@ configure_hostname() {
|
||||
# step, overwriting the placeholder (UTC) the deferred install seeded.
|
||||
configure_timezone() {
|
||||
[[ -n ${timezone:-} ]] || return 0
|
||||
if timedatectl set-timezone "$timezone" 2>&1; then
|
||||
return 0
|
||||
if ! timedatectl set-timezone "$timezone" 2>&1; then
|
||||
# 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
|
||||
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
|
||||
# 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() {
|
||||
@@ -851,16 +932,99 @@ finalize_user() {
|
||||
"$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
|
||||
# password: add the user's key, kill every other slot (throwaway + any seller
|
||||
# keys a reset left behind), then rebuild the UKI without the embedded
|
||||
# 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.
|
||||
# password and retire every other slot (throwaway + any seller keys a reset
|
||||
# left behind). luks_rekey journals each step, so an interrupted attempt
|
||||
# resumes where it stopped.
|
||||
rekey_luks() {
|
||||
[[ -f $PROVISIONING_DIR/luks-key ]] || return 0
|
||||
luks_rekey_pending || return 0
|
||||
|
||||
local device
|
||||
if ! device=$(luks_device) || [[ ! -e $device ]]; then
|
||||
@@ -869,61 +1033,32 @@ rekey_luks() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
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."
|
||||
return 1
|
||||
fi
|
||||
luks_rekey "$device"
|
||||
}
|
||||
|
||||
# Add the user's key (a retry with a different password just adds another
|
||||
# slot; all but the current one are killed once the rebuild succeeds).
|
||||
cryptsetup luksAddKey --key-file "$PROVISIONING_DIR/luks-key" "$device" <(printf '%s' "$password")
|
||||
|
||||
# Rebuild the no-auto-unlock UKI FIRST, keeping the throwaway key and slot as
|
||||
# a fallback. Only once that succeeds do we kill the other slots and destroy
|
||||
# the staged key — so a limine-update failure leaves a recoverable,
|
||||
# still-auto-unlocking state to retry, never a disk locked to a password the
|
||||
# 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
|
||||
# The re-key's fingerprint of the console keyboard layout the boot files ask for
|
||||
# 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
|
||||
awk '/^[ \t]*(KEYMAP|KEYMAP_TOGGLE|XKB[A-Z_]*)=/ { sub(/^[ \t]+/, ""); print }' /etc/vconsole.conf |
|
||||
sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
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
|
||||
# A re-key interrupted after the staged key was retired can only finish with
|
||||
# the password the disk already holds.
|
||||
rekey_accepts_password() {
|
||||
local -
|
||||
set +x
|
||||
local device
|
||||
[[ -f $REKEY_STATE ]] || return 0
|
||||
if ! device=$(luks_device) || [[ ! -e $device ]]; then
|
||||
return 0
|
||||
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"
|
||||
luks_rekey_accepts_password "$device"
|
||||
}
|
||||
|
||||
# Start the ESP's limine.conf over from the shipped template and drop foreign
|
||||
@@ -990,7 +1125,7 @@ reset_limine_config() {
|
||||
cleanup_oem_state() {
|
||||
# Keep groups + packages: omarchy-system-factory-reset stages the bundled Node
|
||||
# tarball from these live copies when the factory snapshot predates it.
|
||||
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user"
|
||||
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" "$REKEY_STATE"
|
||||
|
||||
rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
@@ -1017,7 +1152,7 @@ run_provisioning() {
|
||||
touch "$FINALIZE_WARNING_FLAG"
|
||||
fi
|
||||
|
||||
if [[ -f $PROVISIONING_DIR/luks-key ]]; then
|
||||
if luks_rekey_pending; then
|
||||
log_step "re-keying LUKS to the user's password"
|
||||
echo rekey >"$STATE_FILE"
|
||||
rekey_luks
|
||||
@@ -1034,16 +1169,51 @@ run_provisioning() {
|
||||
limine-update
|
||||
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"
|
||||
cleanup_oem_state
|
||||
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() {
|
||||
local status
|
||||
|
||||
platform_ready || return 1
|
||||
|
||||
while true; do
|
||||
keyboard_form
|
||||
keyboard_form || return 1
|
||||
user_form && status=0 || status=$?
|
||||
if ((status != 0)); then
|
||||
# Esc unwinds to the keyboard step. Ctrl+C is the only other way out of a
|
||||
@@ -1051,7 +1221,12 @@ run_setup() {
|
||||
((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot
|
||||
continue
|
||||
fi
|
||||
confirm_form && break
|
||||
confirm_form || continue
|
||||
if ! rekey_accepts_password; then
|
||||
notice "Setup already set the disk password. Enter that same password." 4
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
touch "$LOG_FILE"
|
||||
|
||||
@@ -24,17 +24,29 @@ omarchy_security_install_sudo_cleanup_traps
|
||||
omarchy_security_revoke_sudo_timestamp || exit 1
|
||||
omarchy_security_enable_no_update_sudo
|
||||
|
||||
channel="${1:-stable}"
|
||||
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||
channel="${1:-}"
|
||||
if [[ -n $channel && $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||
echo "Invalid channel: $channel" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/pacman.sh"
|
||||
if ! platform=$(omarchy-hw-platform); then
|
||||
echo "Cannot tell this machine's platform. No repository configuration changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
templates=$(omarchy_pacman_templates "$platform") || exit 1
|
||||
[[ -n $channel ]] || channel=$(omarchy_pacman_default_channel "$platform") || exit 1
|
||||
if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channel ]]; then
|
||||
echo "Omarchy has no $channel channel for $platform. No repository configuration changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
||||
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||
echo "Setting channel to $channel"
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
|
||||
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||
|
||||
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
|
||||
# custom repositories and IgnorePkg entries shape the downgrade-capable
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Reinstall all default Omarchy packages from the stable channel
|
||||
# omarchy:summary=Reinstall all default Omarchy packages from the stable channel, or on aarch64 the edge channel
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
# Reinstall all default Omarchy packages from the stable channel and downgrade any packages that are too new.
|
||||
# Reinstall all default Omarchy packages from the platform's default channel
|
||||
# (stable, or edge on aarch64) and downgrade any packages that are too new.
|
||||
|
||||
# Set the package repository to the stable mirrors
|
||||
# Set the package repository to the platform's default channel
|
||||
omarchy-refresh-pacman
|
||||
|
||||
# Downgrade any packages to the stable setup
|
||||
# Downgrade any packages to that channel's setup
|
||||
omarchy-update-pacman -Suu --noconfirm
|
||||
|
||||
# Ensure all packages are installed
|
||||
mapfile -t packages < <(grep -v '^#' "$OMARCHY_PATH/install/omarchy-base.packages" | grep -v '^$')
|
||||
# Ensure this platform's default packages are installed
|
||||
default_packages=$(omarchy-pkg-defaults)
|
||||
mapfile -t packages <<<"$default_packages"
|
||||
omarchy-update-pacman -Syu --noconfirm --needed "${packages[@]}"
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
omarchy-lifecycle-dispatch pre-remove steam
|
||||
omarchy-pkg-drop steam
|
||||
|
||||
rm -rf \
|
||||
|
||||
@@ -34,15 +34,13 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
||||
aether \
|
||||
cliamp \
|
||||
libreoffice-fresh \
|
||||
xournalpp \
|
||||
pinta \
|
||||
obsidian \
|
||||
obs-studio \
|
||||
kdenlive \
|
||||
moonlight-qt \
|
||||
omacut \
|
||||
monologue \
|
||||
omacalc \
|
||||
omawrite \
|
||||
hype
|
||||
hype \
|
||||
signed
|
||||
fi
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/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."
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/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."
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#!/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."
|
||||
@@ -2,9 +2,30 @@
|
||||
|
||||
# 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
|
||||
# instance would exit on arrival and the stale process would keep serving the
|
||||
# old ~/.XCompose -- a restart that reports success and changes nothing.
|
||||
systemctl --user stop omarchy-fcitx5.service
|
||||
pkill -x fcitx5 >/dev/null 2>&1 || true
|
||||
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
|
||||
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
|
||||
@@ -10,13 +10,18 @@ fi
|
||||
|
||||
omarchy_security_require_privileged_bash_startup() {
|
||||
[[ $- == *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 '
|
||||
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
|
||||
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
|
||||
stat=$(</proc/self/stat) || exit 1
|
||||
read -r _ caller _ <<<"${stat##*) }"
|
||||
mapfile -d "" -t argv <"/proc/$caller/cmdline" || exit 1
|
||||
executable=$(/usr/bin/readlink -e -- "/proc/$caller/exe") || exit 1
|
||||
[[ $executable == "/usr/bin/bash" &&
|
||||
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
|
||||
${argv[1]:-} == "-p" ]]
|
||||
' omarchy-bash-startup "$$"
|
||||
' omarchy-bash-startup
|
||||
}
|
||||
|
||||
omarchy_security_sanitize_bash_environment() {
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
# omarchy:summary=Add or remove an EFI boot entry for the Omarchy UKI, allowing the system to boot directly
|
||||
# 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
|
||||
echo "Error: System is not booted in UEFI mode" >&2
|
||||
exit 1
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/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."
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
#!/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
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/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
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/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
|
||||
@@ -462,7 +462,34 @@ root_dispatch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The bar indicator only probes on demand, so every grant change pushes a
|
||||
# refresh. Root's expiry timer can't reach the user's shell, so a user timer
|
||||
# refreshes once the deadline has passed. Both are best effort: a stale red
|
||||
# indicator still revokes cleanly when clicked.
|
||||
refresh_indicator() {
|
||||
/usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# A failed enable can still change the grant (a renewal that rolls back removes
|
||||
# the previous policy), so refresh whatever the privileged action returned.
|
||||
change_grant() {
|
||||
local status=0
|
||||
/usr/bin/sudo "$@" || status=$?
|
||||
refresh_indicator
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# A wall-clock deadline, like the sudoers rule and root's timer, so expiry
|
||||
# during suspend still refreshes on resume.
|
||||
schedule_expiry_refresh() {
|
||||
[[ $minutes == "permanent" ]] && return 0
|
||||
/usr/bin/systemd-run --user --quiet --collect --on-calendar="@$((EPOCHSECONDS + 10#$minutes * 60 + 2))" \
|
||||
--timer-property=AccuracySec=1s \
|
||||
-- /usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
show_enabled() {
|
||||
schedule_expiry_refresh
|
||||
if [[ $minutes == "permanent" ]]; then
|
||||
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
|
||||
else
|
||||
@@ -542,12 +569,12 @@ else
|
||||
fi
|
||||
fi
|
||||
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
|
||||
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
|
||||
change_grant -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"
|
||||
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
show_enabled
|
||||
else
|
||||
if (( status != 0 && status != STATUS_INACTIVE )); then
|
||||
@@ -587,7 +614,7 @@ else
|
||||
echo ""
|
||||
|
||||
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
|
||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||
echo ""
|
||||
show_enabled
|
||||
else
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# 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
|
||||
# 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.
|
||||
# re-keys it to the new owner's password and removes the keyfile. A platform
|
||||
# whose boot chain the Limine UKI path can't drive rebuilds, verifies and
|
||||
# unlocks it through omarchy-lifecycle-dispatch.
|
||||
#
|
||||
# Honest limitations:
|
||||
# - On unencrypted disks this is deletion, not forensic erasure. fstrim helps
|
||||
@@ -29,17 +31,25 @@
|
||||
set -euo pipefail
|
||||
|
||||
PROVISIONING_DIR=/var/lib/omarchy/provisioning
|
||||
PACKAGED_PATH=/usr/bin/omarchy-system-factory-reset
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
TOP_MNT=/run/omarchy-system-factory-reset/top
|
||||
NEXT_NAME=@omarchy-reset-next
|
||||
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
|
||||
# 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.
|
||||
# Always elevate the packaged copy, including when invoked from a dev checkout.
|
||||
if (( EUID != 0 )); then
|
||||
mapfile -t gum_env < <(env | grep '^GUM_' || true)
|
||||
exec sudo env "${gum_env[@]}" "$0" "$@"
|
||||
exec sudo env "${gum_env[@]}" "$PACKAGED_PATH" "$@"
|
||||
fi
|
||||
|
||||
export PATH="$OMARCHY_PATH/bin:$PATH"
|
||||
@@ -62,6 +72,51 @@ root_is_btrfs_at() {
|
||||
[[ $(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
|
||||
# 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
|
||||
@@ -81,7 +136,7 @@ luks_device() {
|
||||
local src part
|
||||
src=$(findmnt -no SOURCE / | sed 's/\[.*//')
|
||||
[[ -n $src ]] || return 1
|
||||
part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
|
||||
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
|
||||
[[ -n $part ]] && { echo "$part"; return 0; }
|
||||
return 1
|
||||
}
|
||||
@@ -99,6 +154,11 @@ generate_passphrase() {
|
||||
}
|
||||
|
||||
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 [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then
|
||||
btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true
|
||||
@@ -106,6 +166,9 @@ cleanup() {
|
||||
umount -R "$TOP_MNT" 2>/dev/null || true
|
||||
fi
|
||||
rmdir "$TOP_MNT" 2>/dev/null || true
|
||||
if [[ ${reset_committed:-0} == 0 ]]; then
|
||||
revoke_reset_luks
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
@@ -127,8 +190,54 @@ confirm_reset() {
|
||||
[[ $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
|
||||
# provisioning window. $1 is the root of the staged factory system.
|
||||
# provisioning window. $1 is the root of the staged factory system. Where the
|
||||
# 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() {
|
||||
local next="$1" device passphrase
|
||||
|
||||
@@ -140,19 +249,29 @@ stage_luks_rekey() {
|
||||
local current
|
||||
while true; do
|
||||
current=$(gum input --password --placeholder "Current disk encryption passphrase" --prompt "Passphrase> ") || exit 1
|
||||
if cryptsetup open --test-passphrase --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
|
||||
if cryptsetup open --test-passphrase --token-type passphrase-only --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
gum style --foreground 1 "That passphrase does not unlock $device. Try again."
|
||||
done
|
||||
|
||||
passphrase=$(generate_passphrase)
|
||||
cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")
|
||||
reset_boot_owner || fail "could not tell how this machine's boot chain is reset"
|
||||
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"
|
||||
printf '%s' "$passphrase" >"$next$PROVISIONING_DIR/luks-key"
|
||||
chmod 600 "$next$PROVISIONING_DIR/luks-key"
|
||||
|
||||
[[ $RESET_BOOT == "generic" ]] || return 0
|
||||
|
||||
install -d -m 755 "$next/etc/omarchy"
|
||||
printf '%s' "$passphrase" >"$next/etc/omarchy/provisioning.key"
|
||||
chmod 600 "$next/etc/omarchy/provisioning.key"
|
||||
@@ -235,6 +354,12 @@ verify_limine_hashes() {
|
||||
rebuild_next_boot() {
|
||||
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
|
||||
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")
|
||||
@@ -297,6 +422,23 @@ rebuild_next_boot() {
|
||||
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.
|
||||
scrub_factory_accounts() {
|
||||
local root="$1" user users
|
||||
@@ -320,6 +462,12 @@ scrub_factory_accounts() {
|
||||
sanitize_factory_baseline() {
|
||||
local factory="$1"
|
||||
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
|
||||
btrfs property set -ts "$factory" ro true
|
||||
@@ -345,6 +493,14 @@ stage_full_reset() {
|
||||
[[ -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"
|
||||
|
||||
# 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"
|
||||
systemd-id128 new >"$next/etc/machine-id"
|
||||
rm -f "$next"/etc/ssh/ssh_host_*
|
||||
@@ -360,6 +516,13 @@ stage_full_reset() {
|
||||
scrub_factory_accounts "$next" ||
|
||||
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
|
||||
# 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
|
||||
@@ -388,9 +551,30 @@ stage_full_reset() {
|
||||
log "Activating the factory system"
|
||||
local old="@omarchy-old-$(date +%s)"
|
||||
mv "$top/@" "$top/$old"
|
||||
mv "$next" "$top/@"
|
||||
# Until the factory root is at @ the reset is not committed: cleanup has the
|
||||
# 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
|
||||
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
|
||||
@@ -428,6 +612,10 @@ main() {
|
||||
|
||||
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
|
||||
|
||||
# The running system's limine-snapper-sync must not rewrite the ESP's
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
# omarchy-system-sleep-lock then usually finds the session already secure.
|
||||
#
|
||||
# A docked lid close does not suspend (HandleLidSwitchDocked defaults to
|
||||
# ignore), so it must not lock either: that is clamshell mode, still in use on
|
||||
# the external display.
|
||||
# ignore, and omarchy-system-lid-inhibit covers displays logind does not count),
|
||||
# so it must not lock either: that is clamshell mode, still in use on the
|
||||
# external display.
|
||||
if omarchy-hw-laptop-closed && ! omarchy-hw-external-monitors; then
|
||||
omarchy-system-lock >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/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
|
||||
@@ -20,6 +20,6 @@ fi
|
||||
# Create symlink to the new background media
|
||||
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
|
||||
|
||||
# Update the live shell background immediately when it is running. The
|
||||
# background plugin also polls this symlink, but IPC avoids the visible delay.
|
||||
# Update the live shell background when it is running. Nothing watches the
|
||||
# symlink, so this is how the shell learns of the new background.
|
||||
omarchy-shell -q background set "$BACKGROUND"
|
||||
@@ -207,11 +207,14 @@ set_theme_background() {
|
||||
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
|
||||
# Played or not, the shell gets the new still: it still holds the old
|
||||
# theme's path, which the theme swap above deleted, and loads it again
|
||||
# when OWE hands the desktop back after the intro. While the intro plays
|
||||
# the shell's background is suspended under it, so the swap is unseen.
|
||||
omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared" || true
|
||||
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
|
||||
shell_ipc shell finishThemeIntro "$theme_id" || true
|
||||
wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Apply current Omarchy theme colors to running Foot terminals
|
||||
# omarchy:summary=Apply current Omarchy theme colors and background alpha to running Foot terminals
|
||||
# omarchy:hidden=true
|
||||
|
||||
colors_toml=$HOME/.local/state/omarchy/current/theme/colors.toml
|
||||
foot_ini=$HOME/.local/state/omarchy/current/theme/foot.ini
|
||||
|
||||
if [[ ! -f $colors_toml ]] || ! pgrep -x foot >/dev/null; then
|
||||
exit 0
|
||||
@@ -12,6 +13,16 @@ fi
|
||||
foot_osc=$(omarchy-theme-osc "$colors_toml")
|
||||
[[ -n $foot_osc ]] || exit 0
|
||||
|
||||
# foot takes a background alpha as an urxvt-style [percent] prefix, so a theme
|
||||
# going see-through or back, as with Glassy windows, reaches running terminals.
|
||||
# The last alpha in the theme's foot.ini wins, as it does when foot reads it.
|
||||
alpha=$(sed -n 's/^[[:space:]]*alpha[[:space:]]*=[[:space:]]*\([0-9.]*\).*/\1/p' "$foot_ini" 2>/dev/null | tail -n 1)
|
||||
background=$(omarchy-theme-color --file "$colors_toml" background 2>/dev/null)
|
||||
if [[ -n $background ]]; then
|
||||
percent=$(awk -v alpha="${alpha:-1}" 'BEGIN { printf "%d", alpha * 100 + 0.5 }')
|
||||
foot_osc+=$(printf '\033]11;[%d]%s\007' "$percent" "$background")
|
||||
fi
|
||||
|
||||
for foot_pid in $(pgrep -x foot); do
|
||||
for child_pid in $(pgrep -P "$foot_pid"); do
|
||||
tty=$(readlink "/proc/$child_pid/fd/1" 2>/dev/null)
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Give terminals, btop, and GTK apps a see-through background while windows are glassy
|
||||
# omarchy:args=[theme-dir]
|
||||
# omarchy:hidden=true
|
||||
|
||||
# A Hyprland opacity rule fades a terminal's text along with its background,
|
||||
# and foot turns compositor blur down unless its own config asks for it, so the
|
||||
# glass belongs in each terminal's theme file, as in the glassy Starship
|
||||
# (#14726). The lines sit between markers so leaving glassy takes them out
|
||||
# again, and a theme that sets its own opacity keeps it.
|
||||
|
||||
THEME_DIR="${1:-$HOME/.local/state/omarchy/current/theme}"
|
||||
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
|
||||
GLASSY=false
|
||||
[[ -f $TOGGLES_DIR/window-glassy.lua ]] && GLASSY=true
|
||||
status=0
|
||||
|
||||
# A literal for a sed address, whose delimiter is /.
|
||||
regex() {
|
||||
sed 's/[][\\.*^$/]/\\&/g' <<<"$1"
|
||||
}
|
||||
|
||||
# A block added to a file that did not end in a newline has to supply one, so
|
||||
# its begin marker says so, and taking the block out takes that newline back.
|
||||
# Leaving glassy then gives the file back exactly as it was. Markers come as
|
||||
# an opening and a closing part, so CSS can wrap them in a comment.
|
||||
remove_block() {
|
||||
local file="$1" open="$2" close="$3"
|
||||
local begin end
|
||||
begin=$(regex "$open omarchy-glass begin")
|
||||
end=$(regex "$open omarchy-glass end$close")
|
||||
[[ -f $file ]] && grep -qF "$open omarchy-glass begin" "$file" || return 0
|
||||
|
||||
local added_newline=false
|
||||
grep -qxF "$open omarchy-glass begin after a line without a newline$close" "$file" && added_newline=true
|
||||
sed -i "/^$begin/,/^$end\$/d" "$file" || return 1
|
||||
if [[ $added_newline == "true" && -s $file && -z $(tail -c 1 "$file") ]]; then
|
||||
truncate -s -1 "$file" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
append_block() {
|
||||
local file="$1" open="$2" close="$3" lines="$4"
|
||||
if [[ -s $file && -n $(tail -c 1 "$file") ]]; then
|
||||
printf '\n%s\n%s\n%s\n' "$open omarchy-glass begin after a line without a newline$close" "$lines" "$open omarchy-glass end$close" >>"$file"
|
||||
else
|
||||
printf '%s\n%s\n%s\n' "$open omarchy-glass begin$close" "$lines" "$open omarchy-glass end$close" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Each setting comes with a pattern for the theme setting it already, which
|
||||
# keeps the theme's own; an empty pattern always adds it. Lines that belong in
|
||||
# a section go in under its header. A file that already has the section gets
|
||||
# them inside it, since TOML refuses a table declared twice; otherwise the
|
||||
# header comes along with them.
|
||||
glass() {
|
||||
local file="$THEME_DIR/$1" section="$2"
|
||||
shift 2
|
||||
[[ -f $file ]] || return 0
|
||||
|
||||
remove_block "$file" "#" "" || return 1
|
||||
[[ $GLASSY == "true" ]] || return 0
|
||||
|
||||
local lines=""
|
||||
while (( $# >= 2 )); do
|
||||
if [[ -z $1 ]] || ! grep -qE "$1" "$file"; then
|
||||
lines+="${lines:+$'\n'}$2"
|
||||
fi
|
||||
shift 2
|
||||
done
|
||||
[[ -n $lines ]] || return 0
|
||||
|
||||
# A header may carry trailing space or a comment.
|
||||
local header="^$(regex "$section")[[:space:]]*(#.*)?\$"
|
||||
if [[ -n $section ]] && grep -qE "$header" "$file"; then
|
||||
local block
|
||||
block=$(mktemp) || return 1
|
||||
printf '# omarchy-glass begin\n%s\n# omarchy-glass end\n' "$lines" >"$block"
|
||||
sed -i -E "0,/$header/{/$header/r $block
|
||||
}" "$file"
|
||||
local result=$?
|
||||
rm -f "$block"
|
||||
return $result
|
||||
fi
|
||||
|
||||
append_block "$file" "#" "" "${section:+$section$'\n'}$lines"
|
||||
}
|
||||
|
||||
# Alacritty's own blur works only on macOS and KDE; Hyprland blurs it anyway.
|
||||
glass alacritty.toml '[window]' \
|
||||
'^[[:space:]]*opacity[[:space:]]*=' 'opacity = 0.65' || status=1
|
||||
glass foot.ini '[colors-dark]' \
|
||||
'^[[:space:]]*alpha[[:space:]]*=' 'alpha=0.65' \
|
||||
'^[[:space:]]*blur[[:space:]]*=' 'blur=yes' || status=1
|
||||
glass kitty.conf '' \
|
||||
'^[[:space:]]*background_opacity[[:space:]]' 'background_opacity 0.65' || status=1
|
||||
# btop paints its own background over the terminal's glass. An empty main_bg
|
||||
# uses the terminal background; the last theme value wins, so removing the
|
||||
# block restores the theme's colour without changing btop's user config.
|
||||
glass btop.theme '' \
|
||||
'^[[:space:]]*theme\[main_bg\][[:space:]]*=[[:space:]]*""' 'theme[main_bg]=""' || status=1
|
||||
# GTK only asks Hyprland for blur under a CSS backdrop-filter, which Ghostty
|
||||
# takes as custom CSS and reloads live; its background-blur speaks only KDE's
|
||||
# blur protocol.
|
||||
glass ghostty.conf '' \
|
||||
'^[[:space:]]*background-opacity[[:space:]]*=' 'background-opacity = 0.65' \
|
||||
'' "gtk-custom-css = $OMARCHY_PATH/default/gtk/glass.css" || status=1
|
||||
|
||||
# Other GTK apps, such as Nautilus, read the user's gtk.css at launch, so the
|
||||
# same backdrop blur is imported there. It follows the mode alone, so a theme
|
||||
# switch, which names the theme it is rendering, leaves it be.
|
||||
if (( $# == 0 )); then
|
||||
gtk_css="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-4.0/gtk.css"
|
||||
remove_block "$gtk_css" "/*" " */" || status=1
|
||||
if [[ $GLASSY == "true" ]]; then
|
||||
{ mkdir -p "${gtk_css%/*}" &&
|
||||
append_block "$gtk_css" "/*" " */" "@import url(\"file://$OMARCHY_PATH/default/gtk/glass.css\");"; } || status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $status
|
||||
@@ -451,3 +451,4 @@ if [[ -f $COLORS_FILE ]]; then
|
||||
fi
|
||||
|
||||
apply_shell_section_overrides
|
||||
omarchy-theme-set-glass "$NEXT_THEME_DIR"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/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 "$@"
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/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"
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
#!/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
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/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
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/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"
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/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
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Toggle animations, transparency and other effects that are slow without a GPU
|
||||
# omarchy:summary=Toggle animations, blur and shadows, which are slow without a GPU
|
||||
# omarchy:args=[toggle|on|off]
|
||||
# omarchy:examples=omarchy toggle animations | omarchy toggle animations off | omarchy toggle animations on
|
||||
|
||||
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
|
||||
esac
|
||||
|
||||
if omarchy-hyprland-toggle-enabled no-animations; then
|
||||
omarchy-notification-send -g "Animations disabled"
|
||||
omarchy-notification-send -g "Animations disabled"
|
||||
else
|
||||
omarchy-notification-send -g "Animations enabled"
|
||||
omarchy-notification-send -g "Animations enabled"
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Toggle bar visibility without killing the Omarchy shell
|
||||
# omarchy:summary=Toggle bar visibility without killing the Omarchy shell (on hides the bar, off shows it)
|
||||
# omarchy:args=[toggle|on|off]
|
||||
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
|
||||
|
||||
|
||||
+15
-1
@@ -148,6 +148,15 @@ 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
|
||||
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
|
||||
# needs no privilege because the held command already dropped to this user.
|
||||
# Release it before offering a reboot: a confirmed reboot can terminate this
|
||||
@@ -156,5 +165,10 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
||||
omarchy-update-stay-awake stop
|
||||
update_stay_awake_stopped=1
|
||||
|
||||
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
|
||||
if (( boot_verified )); then
|
||||
"$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
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/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
|
||||
@@ -22,8 +22,32 @@ fi
|
||||
|
||||
# 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.
|
||||
# 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"
|
||||
sudo pacman -Sy --noconfirm archlinux-keyring >/dev/null 2> >(grep -vE '^warning: archlinux-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
|
||||
sudo pacman -Sy --noconfirm "${keyrings[@]}" >/dev/null 2> >(grep -vE '^warning: [a-z0-9@._+-]+-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
|
||||
|
||||
# Say "correct" only once the key verifiably is: before the failure checks
|
||||
# above, a failed recv or reinstall still ended here with exit 0.
|
||||
|
||||
@@ -98,6 +98,17 @@ mapfile -t leftovers < <(
|
||||
# the moved files inactive for nothing.
|
||||
((${#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"
|
||||
for path in "${leftovers[@]}"; do
|
||||
sudo mkdir -p "$replaced${path%/*}"
|
||||
|
||||
@@ -807,6 +807,11 @@ install_omarchy_quattro_packages() {
|
||||
if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then
|
||||
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)
|
||||
# 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
|
||||
as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}"
|
||||
mark_packages_explicit "${base_packages[@]}"
|
||||
@@ -1802,6 +1807,8 @@ copy_missing_config_defaults() {
|
||||
while IFS= read -r -d '' source_path; do
|
||||
rel="${source_path#$source_dir/}"
|
||||
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"
|
||||
[[ -e $target_path || -L $target_path ]] && continue
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/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"
|
||||
Executable
+827
@@ -0,0 +1,827 @@
|
||||
#!/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
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/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
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/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
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/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"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/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"
|
||||
@@ -234,6 +234,11 @@ if [[ -n $MIME_TYPES ]]; then
|
||||
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
# Floating titlebars name the window after this entry rather than its page title.
|
||||
if [[ -z $CUSTOM_EXEC ]]; then
|
||||
printf 'StartupWMClass=%s\n' "$(desktop_string_escape "$(omarchy-webapp-window-class "$APP_URL")")" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
chmod +x "$DESKTOP_FILE"
|
||||
|
||||
if [[ $INTERACTIVE_MODE == "true" ]]; then
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Print the window class Chromium gives a web app opened at a URL
|
||||
# omarchy:args=<url>
|
||||
# omarchy:hidden=true
|
||||
|
||||
# Chromium names an --app window after the URL's host and path, so a web app's
|
||||
# launcher can name its window in StartupWMClass, which titlebars use to show
|
||||
# the app's name rather than the page title.
|
||||
|
||||
url=${1:-}
|
||||
if [[ -z $url ]]; then
|
||||
echo "Usage: omarchy-webapp-window-class <url>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
address=${url#*://}
|
||||
address=${address%%[?#]*}
|
||||
host=${address%%/*}
|
||||
path=${address:${#host}}
|
||||
host=${host##*@}
|
||||
host=${host%%:*}
|
||||
|
||||
printf 'chrome-%s_%s-Default\n' "${host,,}" "${path:-/}" | sed 's|/|_|g'
|
||||
@@ -4,6 +4,12 @@
|
||||
# omarchy:args=<install|remove|launch|stop|status> [options]
|
||||
# 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
|
||||
# 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
|
||||
|
||||
Loaded 100 of 460 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user