Compare commits
111
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -45,3 +45,7 @@ unicode characters. Agent file-editing tools can strip multi-byte codepoints
|
||||
in some positions — do **not** rewrite widget files wholesale through those
|
||||
tools. For glyph fixes, make a targeted edit with the surrounding context, or
|
||||
use a Python script that inserts codepoints via `chr(0xXXXXX)`.
|
||||
|
||||
## Palette references
|
||||
|
||||
Import `qs.Commons as Commons` and use `Commons.Color` for the palette singleton, including `Connections.target`. Qt 6.12 introduces a `Color` type in `QtQuick` that shadows unqualified palette references. This applies to first-party components and third-party plugin QML alike. Keep the unqualified `qs.Commons` import when the file also uses `Style`, `Util`, or `Border`.
|
||||
@@ -46,6 +46,7 @@ GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||
GROUP_DESCRIPTIONS[dev]="Omarchy development tools"
|
||||
GROUP_DESCRIPTIONS[disk]="Disk performance helpers"
|
||||
GROUP_DESCRIPTIONS[dictation]="Dictation recording"
|
||||
GROUP_DESCRIPTIONS[display]="Display and text scaling"
|
||||
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
|
||||
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Get or set the default dictation backend
|
||||
# omarchy:args=[backend]
|
||||
# omarchy:examples=omarchy default dictation | omarchy default dictation superwhisper
|
||||
|
||||
if (($# > 1)); then
|
||||
echo "Usage: omarchy default dictation [backend]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (($# == 0)); then
|
||||
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
|
||||
if [[ ! -s $config ]]; then
|
||||
echo "No dictation backend configured. Select one in Setup > Defaults > Dictation." >&2
|
||||
exit 1
|
||||
fi
|
||||
read -r backend < "$config"
|
||||
else
|
||||
backend=$1
|
||||
fi
|
||||
|
||||
if [[ ! $backend =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
|
||||
echo "Invalid dictation backend: $backend" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (($# == 0)); then
|
||||
echo "$backend"
|
||||
else
|
||||
installer="omarchy-install-dictation-$backend"
|
||||
if omarchy-cmd-missing "$installer"; then
|
||||
echo "No installer available for dictation backend: $backend" >&2
|
||||
exit 1
|
||||
fi
|
||||
omarchy-launch-floating-terminal-with-presentation "$installer"
|
||||
fi
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Record dictation with the configured backend
|
||||
# omarchy:args=<start|stop|toggle>
|
||||
# omarchy:examples=omarchy dictation start | omarchy dictation toggle
|
||||
|
||||
if (($# != 1)) || [[ $1 != "start" && $1 != "stop" && $1 != "toggle" ]]; then
|
||||
echo "Usage: omarchy dictation <start|stop|toggle>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
|
||||
mkdir -p "${config%/*}"
|
||||
exec 9> "$config.lock"
|
||||
if ! flock -w 5 9; then
|
||||
echo "Dictation is busy; try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backend=$(omarchy-default-dictation) || exit 1
|
||||
adapter="omarchy-dictation-$backend"
|
||||
if omarchy-cmd-missing "$adapter"; then
|
||||
echo "The selected dictation backend ($backend) is not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
"$adapter" "$1" 9>&-
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Record dictation using Superwhisper
|
||||
# omarchy:args=<start|stop|toggle>
|
||||
|
||||
case "${1:-}" in
|
||||
start) superwhisper start ;;
|
||||
stop)
|
||||
if omarchy-cmd-missing superwhisper || ! systemctl --user is-active --quiet superwhisper.service; then
|
||||
exit 0
|
||||
else
|
||||
superwhisper stop
|
||||
fi
|
||||
;;
|
||||
toggle) superwhisper record ;;
|
||||
*) echo "Usage: omarchy dictation superwhisper <start|stop|toggle>" >&2; exit 1 ;;
|
||||
esac
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:hidden=true
|
||||
# omarchy:summary=Save a configured dictation backend after stopping the previous backend
|
||||
# omarchy:args=<backend>
|
||||
|
||||
set -e
|
||||
|
||||
if (($# != 1)) || [[ ! $1 =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
|
||||
echo "Usage: omarchy-dictation-use <backend>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
|
||||
mkdir -p "${config%/*}"
|
||||
exec 9> "$config.lock"
|
||||
if ! flock -w 5 9; then
|
||||
echo "Dictation is busy; try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
previous=$(omarchy-default-dictation 2>/dev/null) || previous=""
|
||||
if [[ -n $previous && $previous != "$1" ]] && omarchy-cmd-present "omarchy-dictation-$previous"; then
|
||||
if ! "omarchy-dictation-$previous" stop 9>&-; then
|
||||
echo "Could not stop $previous; keeping the existing dictation backend." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
temporary=$(mktemp "$config.XXXXXX")
|
||||
trap 'rm -f "$temporary"' EXIT
|
||||
printf '%s\n' "$1" > "$temporary"
|
||||
mv -f "$temporary" "$config"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Record dictation using Voxtype
|
||||
# omarchy:args=<start|stop|toggle>
|
||||
|
||||
case "${1:-}" in
|
||||
start|toggle) voxtype record "$1" ;;
|
||||
stop)
|
||||
if omarchy-cmd-missing voxtype || [[ $(voxtype status) == "stopped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
voxtype record stop
|
||||
fi
|
||||
;;
|
||||
*) echo "Usage: omarchy dictation voxtype <start|stop|toggle>" >&2; exit 1 ;;
|
||||
esac
|
||||
+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" ]]
|
||||
@@ -10,6 +10,7 @@ sync_clamshell() {
|
||||
(
|
||||
flock -n 9 || exit 0
|
||||
omarchy-hyprland-monitor-clamshell
|
||||
omarchy-system-lid-inhibit
|
||||
) 9>"$LOCK"
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
;;
|
||||
*)
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install and configure Superwhisper dictation
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
omarchy-pkg-add superwhisper-bin
|
||||
bash /usr/share/superwhisper/setup-user
|
||||
# Existing profiles may still use Right Alt, which conflicts with Omarchy.
|
||||
# Keep a recording shortcut active while moving away from that binding.
|
||||
shortcuts=$(superwhisper shortcuts show)
|
||||
if [[ $(jq -r '.shortcuts.hold' <<< "$shortcuts") == "RightAlt" ]]; then
|
||||
if [[ -z $(jq -r '.shortcuts.toggle' <<< "$shortcuts") ]]; then
|
||||
superwhisper shortcuts set hold "RightSuper" >/dev/null
|
||||
else
|
||||
superwhisper shortcuts set hold none >/dev/null
|
||||
fi
|
||||
fi
|
||||
# Superwhisper requires a native recording shortcut; keep it separate from
|
||||
# Omarchy's push-to-talk keys.
|
||||
superwhisper shortcuts set toggle "Alt+Space" >/dev/null
|
||||
superwhisper shortcuts set hold none >/dev/null
|
||||
omarchy-dictation-use superwhisper
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-notification-send -g "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
superwhisper settings
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install and configure Voxtype dictation
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
voxtype_lib_path="${OMARCHY_VOXTYPE_LIB_PATH:-/usr/lib/voxtype}"
|
||||
|
||||
if omarchy-cmd-missing voxtype; then
|
||||
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
|
||||
omarchy-pkg-add wtype voxtype-bin
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
mkdir -p "$config_home/voxtype"
|
||||
if [[ ! -e $config_home/voxtype/config.toml ]]; then
|
||||
cp "$OMARCHY_PATH/default/voxtype/config.toml" "$config_home/voxtype/"
|
||||
# A Vulkan runtime is not enough: aarch64 voxtype-bin ships no Vulkan backend.
|
||||
if omarchy-hw-vulkan && [[ -x $voxtype_lib_path/voxtype-vulkan ]]; then
|
||||
voxtype setup gpu --enable || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Setup reuses the configured model and downloads it only when missing.
|
||||
# Retry these steps even when an earlier attempt already installed the binary.
|
||||
voxtype setup --download --no-post-install
|
||||
voxtype setup systemd
|
||||
systemctl --user enable --now voxtype.service
|
||||
|
||||
omarchy-dictation-use voxtype
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-notification-send -g "Voxtype Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
@@ -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,7 +15,9 @@
|
||||
# against a half-written tree, and that failed reload leaves a second engine
|
||||
# generation behind that turns the next restart's IPC kill into a crash.
|
||||
run_shell() {
|
||||
QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
|
||||
local startup_background
|
||||
startup_background=$(readlink -f "$HOME/.local/state/omarchy/current/background" 2>/dev/null)
|
||||
OMARCHY_STARTUP_BACKGROUND="$startup_background" QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
|
||||
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
|
||||
shell_pid=$!
|
||||
|
||||
|
||||
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
|
||||
@@ -68,8 +68,6 @@ run_first_run_step() {
|
||||
fi
|
||||
}
|
||||
|
||||
run_first_run_step "install Voxtype post-update hook" \
|
||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
|
||||
run_first_run_step "install fingerprint setup post-update hook" \
|
||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
|
||||
run_first_run_step "install agent setup post-update hook" \
|
||||
@@ -77,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"
|
||||
+264
-89
@@ -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
|
||||
@@ -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[@]}"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove Superwhisper dictation and its desktop integration
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
|
||||
systemctl --user disable --now superwhisper.service 2>/dev/null || true
|
||||
omarchy-shell -q shell setPluginEnabled superwhisper.panel false >/dev/null
|
||||
omarchy-pkg-drop superwhisper-bin
|
||||
systemctl --user daemon-reload
|
||||
|
||||
# Remove the package setup's links, preserving independently installed copies
|
||||
# and the user's preferences, history, and models.
|
||||
unlink_package_link() {
|
||||
if [[ -L $1 && $(readlink "$1") == "$2" ]]; then
|
||||
rm "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
unlink_package_link "$HOME/.local/bin/superwhisper" /usr/bin/superwhisper
|
||||
unlink_package_link "$data_home/superwhisper/app/current" /opt/superwhisper
|
||||
unlink_package_link "$config_home/omarchy/plugins/superwhisper-panel" /opt/superwhisper/assets/omarchy-plugin/superwhisper-panel
|
||||
for agent in .agents .claude .codex .pi/agent; do
|
||||
unlink_package_link "$HOME/$agent/skills/superwhisper" /opt/superwhisper/assets/agent-skill/superwhisper
|
||||
done
|
||||
|
||||
backend_config="$config_home/omarchy/defaults/dictation"
|
||||
if [[ -f $backend_config && $(< "$backend_config") == "superwhisper" ]]; then
|
||||
rm "$backend_config"
|
||||
fi
|
||||
|
||||
omarchy-shell -q shell rescanPlugins
|
||||
hyprctl reload >/dev/null
|
||||
if systemctl --user -q is-active omarchy-fcitx5.service; then
|
||||
systemctl --user restart omarchy-fcitx5.service
|
||||
fi
|
||||
@@ -18,6 +18,10 @@ if omarchy-cmd-present voxtype; then
|
||||
omarchy-pkg-drop voxtype-bin
|
||||
rm -rf ~/.config/voxtype
|
||||
rm -rf ~/.local/share/voxtype
|
||||
backend_config="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
|
||||
if [[ -f $backend_config && $(< "$backend_config") == "voxtype" ]]; then
|
||||
rm "$backend_config"
|
||||
fi
|
||||
hyprctl reload >/dev/null
|
||||
else
|
||||
echo "Voxtype was not installed."
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
omarchy-lifecycle-dispatch pre-remove steam
|
||||
omarchy-pkg-drop steam
|
||||
|
||||
rm -rf \
|
||||
|
||||
+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
|
||||
@@ -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
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Play the current background's matching theme intro
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
state_home="$HOME/.local/state"
|
||||
current_dir="$state_home/omarchy/current"
|
||||
current_background_link="$current_dir/background"
|
||||
marker="$state_home/omarchy/background-intro.session-id"
|
||||
toggle="$state_home/omarchy/toggles/background-intros-off"
|
||||
video_extensions=(mp4 m4v mov webm mkv avi)
|
||||
|
||||
resolve_intro() {
|
||||
local background
|
||||
local stem
|
||||
local extension
|
||||
local intro
|
||||
|
||||
background=$(readlink -f "${1:-$current_background_link}" 2>/dev/null) || return 1
|
||||
[[ -f $background ]] || return 1
|
||||
case ${background,,} in
|
||||
*.jpg | *.jpeg | *.png | *.bmp | *.webp) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
stem=${background##*/}
|
||||
stem=${stem%.*}
|
||||
for extension in "${video_extensions[@]}"; do
|
||||
intro="${background%/*}/intros/$stem.$extension"
|
||||
if [[ -f $intro ]]; then
|
||||
printf '%s\n' "$intro"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Preparation is speculative: the selected staged clip is compared again by OWE.
|
||||
prepare_theme_intro() {
|
||||
local theme="$1" background remembered name source intro
|
||||
local -a names=()
|
||||
[[ -n $theme && $theme != .* && $theme != */* ]] || return 0
|
||||
[[ ! -f $toggle ]] && ! animations_off || return 0
|
||||
owe warm >/dev/null 2>&1 || return 0
|
||||
[[ $(cat "$current_dir/theme.name" 2>/dev/null) != "$theme" ]] || return 0
|
||||
remembered=$(cat "$state_home/omarchy/theme-backgrounds/$theme" 2>/dev/null || true)
|
||||
if [[ ${remembered%/*} == "$current_dir/theme/backgrounds" ]]; then
|
||||
name=${remembered##*/}
|
||||
elif [[ -f $remembered ]]; then
|
||||
background="$remembered"
|
||||
else
|
||||
mapfile -d '' -t names < <(
|
||||
find -L "$HOME/.config/omarchy/backgrounds/$theme" -maxdepth 1 -type f -print0 2>/dev/null | sort -z
|
||||
)
|
||||
if (( ${#names[@]} > 0 )); then
|
||||
background=${names[0]}
|
||||
else
|
||||
mapfile -d '' -t names < <(
|
||||
find -L "$OMARCHY_PATH/themes/$theme/backgrounds" "$HOME/.config/omarchy/themes/$theme/backgrounds" -maxdepth 1 -type f \
|
||||
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.bmp' -o -iname '*.webp' \) -printf '%f\0' 2>/dev/null | sort -zu
|
||||
)
|
||||
(( ${#names[@]} > 0 )) || return 0
|
||||
name=${names[0]}
|
||||
fi
|
||||
fi
|
||||
if [[ -n ${name:-} ]]; then
|
||||
for source in "$HOME/.config/omarchy/themes/$theme" "$OMARCHY_PATH/themes/$theme"; do
|
||||
if [[ -f $source/backgrounds/$name ]]; then
|
||||
background="$source/backgrounds/$name"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ -n ${background:-} ]] || return 0
|
||||
intro=$(resolve_intro "$background" || true)
|
||||
[[ -z $intro ]] || owe intro-prepare "$intro" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
animations_off() {
|
||||
[[ $(hyprctl -j getoption animations:enabled 2>/dev/null | jq -r '.bool' 2>/dev/null) == "false" ]]
|
||||
}
|
||||
|
||||
theme_switch=false
|
||||
intro_prepared=false
|
||||
expected_background=""
|
||||
expected_theme=""
|
||||
case ${1:-} in
|
||||
"") ;;
|
||||
--prepare-theme)
|
||||
prepare_theme_intro "${2:-}"
|
||||
exit 0
|
||||
;;
|
||||
--resolve-only)
|
||||
resolve_intro "${2:-}" || true
|
||||
exit 0
|
||||
;;
|
||||
--resolve-startup)
|
||||
if [[ ! -f $toggle ]] && ! animations_off; then
|
||||
resolve_intro || true
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
--theme-switch)
|
||||
theme_switch=true
|
||||
expected_background=$(readlink -f "${2:-}" 2>/dev/null) || exit 1
|
||||
expected_theme=${3:-}
|
||||
intro_prepared=${4:-false}
|
||||
[[ -n $expected_theme ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-theme-bg-boot-intro [--resolve-only [background]|--resolve-startup|--prepare-theme theme|--theme-switch background theme-id [prepared]]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $theme_switch == false ]]; then
|
||||
mkdir -p "$(dirname "$marker")"
|
||||
exec 9>"$state_home/omarchy/background-intro.lock"
|
||||
flock 9
|
||||
|
||||
session_id=${OMARCHY_SESSION_ID:-${HYPRLAND_INSTANCE_SIGNATURE:-}}
|
||||
[[ -n $session_id ]] || exit 0
|
||||
if [[ -f $marker && $(<"$marker") == "$session_id" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
marker_staged=$(mktemp "$state_home/omarchy/.background-intro.session-id.XXXXXX")
|
||||
trap 'rm -f "$marker_staged"' EXIT
|
||||
printf '%s\n' "$session_id" >"$marker_staged"
|
||||
mv -f "$marker_staged" "$marker"
|
||||
trap - EXIT
|
||||
exec 9>&-
|
||||
fi
|
||||
|
||||
selection_current() {
|
||||
[[ $(readlink -f "$current_background_link") == "$expected_background" &&
|
||||
$(stat -Lc '%d:%i' "$current_dir/theme" 2>/dev/null) == "$expected_theme" ]]
|
||||
}
|
||||
|
||||
if [[ $theme_switch == true ]] && ! selection_current; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -f $toggle ]] || animations_off; then
|
||||
[[ $theme_switch == false ]] || exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
intro=$(resolve_intro || true)
|
||||
if [[ -z $intro ]]; then
|
||||
[[ $theme_switch == false ]] || exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $theme_switch == true && $intro_prepared != "true" ]]; then
|
||||
owe refresh >/dev/null || exit 1
|
||||
elif [[ $theme_switch == false ]]; then
|
||||
deadline=$((SECONDS + 5))
|
||||
while ! timeout 1 owe status >/dev/null 2>&1; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
(( SECONDS < deadline )) || exit 0
|
||||
fi
|
||||
|
||||
# The asynchronous outgoing cover must reach the compositor before playback
|
||||
# can hide the shell background. Older shells have no readiness method.
|
||||
if [[ $theme_switch == true ]]; then
|
||||
deadline=$((SECONDS + 3))
|
||||
while true; do
|
||||
cover_status=$(omarchy-shell shell themeIntroCoverStatus "$expected_theme" 2>/dev/null || true)
|
||||
case $cover_status in
|
||||
loading)
|
||||
(( SECONDS < deadline )) || exit 1
|
||||
sleep 0.01
|
||||
;;
|
||||
error) exit 1 ;;
|
||||
superseded) exit 0 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $theme_switch == true ]] && ! selection_current; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A selection or setting changed while OWE was starting no longer permits this intro.
|
||||
if [[ ! -f $toggle && $intro == "$(resolve_intro || true)" ]] && ! animations_off; then
|
||||
if [[ $theme_switch == true && $intro_prepared == "true" ]]; then
|
||||
owe intro --start first-frame --refresh "$intro"
|
||||
else
|
||||
owe intro --start first-frame "$intro"
|
||||
fi
|
||||
elif [[ $theme_switch == true ]]; then
|
||||
exit 1
|
||||
fi
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Enable or disable background intros
|
||||
# omarchy:args=<enable|disable|toggle|status>
|
||||
# omarchy:examples=omarchy theme bg intro toggle | omarchy theme bg intro disable
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
toggle="$HOME/.local/state/omarchy/toggles/background-intros-off"
|
||||
case ${1:-} in
|
||||
enable)
|
||||
rm -f "$toggle"
|
||||
omarchy-notification-send "Background Intros" "Enabled"
|
||||
;;
|
||||
disable)
|
||||
mkdir -p "$(dirname "$toggle")"
|
||||
touch "$toggle"
|
||||
omarchy-notification-send "Background Intros" "Disabled"
|
||||
;;
|
||||
toggle)
|
||||
if [[ -f $toggle ]]; then
|
||||
omarchy-theme-bg-intro enable
|
||||
else
|
||||
omarchy-theme-bg-intro disable
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
[[ ! -f $toggle ]]
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-theme-bg-intro <enable|disable|toggle|status>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
+84
-6
@@ -50,6 +50,13 @@ shell_ipc() {
|
||||
timeout 2 omarchy-shell "$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
wait_for_theme_intro() {
|
||||
local method="$1" token="$2" pending="$3" deadline=$((SECONDS + 3))
|
||||
while (( SECONDS < deadline )) && [[ $(omarchy-shell shell "$method" "$token" 2>/dev/null) == "$pending" ]]; do
|
||||
sleep 0.02
|
||||
done
|
||||
}
|
||||
|
||||
is_video_path() {
|
||||
[[ ${1,,} =~ \.(mp4|m4v|mov|webm|mkv|avi)$ ]]
|
||||
}
|
||||
@@ -70,10 +77,18 @@ snapshot_background_path() {
|
||||
}
|
||||
|
||||
snapshot_current_background() {
|
||||
local current_background
|
||||
local current_background snapshot
|
||||
|
||||
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
||||
snapshot_background_path "$current_background" "previous"
|
||||
# An interrupted intro is still moving; its final still would flash as a
|
||||
# cover. Capture the frame already playing instead, when OWE supports it.
|
||||
mkdir -p "$BACKGROUND_TRANSITION_CACHE"
|
||||
snapshot="$BACKGROUND_TRANSITION_CACHE/previous-$$.ppm"
|
||||
if owe render "$(jq -nc --arg path "$snapshot" '{cmd: "snapshot", path: $path}')" >/dev/null 2>&1; then
|
||||
echo "$snapshot"
|
||||
else
|
||||
current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
||||
snapshot_background_path "$current_background" "previous"
|
||||
fi
|
||||
}
|
||||
|
||||
theme_background_state_file() {
|
||||
@@ -171,7 +186,7 @@ set_theme_background_link() {
|
||||
}
|
||||
|
||||
set_theme_background() {
|
||||
local new_background new_background_snapshot
|
||||
local new_background new_background_snapshot theme_id intro_prepared=false
|
||||
|
||||
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
||||
if ! choose_theme_background; then
|
||||
@@ -182,6 +197,37 @@ set_theme_background() {
|
||||
fi
|
||||
|
||||
new_background="$CHOSEN_THEME_BACKGROUND"
|
||||
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
|
||||
if [[ -n $INTRO_PREPARE_PID ]] && wait "$INTRO_PREPARE_PID"; then
|
||||
intro_prepared=true
|
||||
fi
|
||||
theme_id=$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH")
|
||||
THEME_INTRO_TOKEN="$theme_id"
|
||||
shell_ipc shell prepareThemeIntro "$OLD_BACKGROUND_SNAPSHOT" "$theme_id" "$colors_payload" "$shell_payload" || \
|
||||
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
|
||||
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
|
||||
(
|
||||
if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then
|
||||
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" &&
|
||||
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
|
||||
shell_ipc background setInstant "$new_background" || true
|
||||
fi
|
||||
fi
|
||||
shell_ipc shell finishThemeIntro "$theme_id" || true
|
||||
wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
|
||||
# Reloading Hyprland interrupts video presentation. Wait for playback,
|
||||
# then serialize with theme selection so an old intro cannot reload
|
||||
# the compositor during the next theme's video.
|
||||
(
|
||||
flock 9
|
||||
if [[ $(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
|
||||
omarchy-restart-hyprctl
|
||||
fi
|
||||
) 9>"$THEME_SET_LOCK"
|
||||
[[ -z $OLD_BACKGROUND_SNAPSHOT ]] || rm -f "$OLD_BACKGROUND_SNAPSHOT"
|
||||
) 9>&- &
|
||||
return
|
||||
fi
|
||||
new_background_snapshot=""
|
||||
if [[ -n $PREPARED_BACKGROUND_SNAPSHOT && $PREPARED_BACKGROUND == "$new_background" ]]; then
|
||||
new_background_snapshot="$PREPARED_BACKGROUND_SNAPSHOT"
|
||||
@@ -337,6 +383,12 @@ fi
|
||||
exec 9>"$THEME_SET_LOCK"
|
||||
flock 9
|
||||
|
||||
# A current video frame only represents the screen after its opening fade.
|
||||
# Finish that short reveal before capturing the next outgoing cover.
|
||||
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
|
||||
wait_for_theme_intro themeIntroStatus "$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null)" "pending"
|
||||
fi
|
||||
|
||||
PREVIOUS_THEME_NAME=$(cat "$CURRENT_THEME_NAME_PATH" 2>/dev/null || true)
|
||||
remember_current_theme_background
|
||||
|
||||
@@ -365,11 +417,24 @@ OLD_BACKGROUND_SNAPSHOT=""
|
||||
PREPARED_BACKGROUND=""
|
||||
PREPARED_BACKGROUND_SNAPSHOT=""
|
||||
BACKGROUND_TRANSITION_SNAPSHOTS=true
|
||||
THEME_BACKGROUND_INTRO=""
|
||||
INTRO_PREPARE_PID=""
|
||||
THEME_INTRO_TOKEN=""
|
||||
if [[ $THEME_HEADLESS != "1" && $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
|
||||
# Resolve the staged choice while the old theme still exists. Video changes
|
||||
# switch directly to the durable path in QML, so neither side needs a copy.
|
||||
if choose_staged_theme_background; then
|
||||
if background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
|
||||
if [[ $PREVIOUS_THEME_NAME != "$THEME_NAME" && ! -f $HOME/.local/state/omarchy/toggles/background-intros-off ]] &&
|
||||
[[ $(hyprctl -j getoption animations:enabled 2>/dev/null | jq -r '.bool') == "true" ]]; then
|
||||
THEME_BACKGROUND_INTRO=$(omarchy-theme-bg-boot-intro --resolve-only "$STAGED_THEME_BACKGROUND")
|
||||
fi
|
||||
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
|
||||
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
|
||||
# OWE retains the opened clip across the staged directory's rename.
|
||||
owe intro-prepare "$THEME_BACKGROUND_INTRO" >/dev/null 2>&1 9>&- &
|
||||
INTRO_PREPARE_PID=$!
|
||||
shell_ipc shell prepareThemeIntro "$OLD_BACKGROUND_SNAPSHOT" "$(stat -Lc '%d:%i' "$NEXT_THEME_PATH")" "" "" || true
|
||||
elif background_transition_uses_snapshots "$CHOSEN_THEME_BACKGROUND"; then
|
||||
OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background)
|
||||
|
||||
# Hand the shell the next background now, so it decodes while the
|
||||
@@ -389,6 +454,12 @@ fi
|
||||
# Generate dynamic configs
|
||||
omarchy-theme-set-templates
|
||||
|
||||
# Replacing the theme's still can cancel an active OWE intro even before the
|
||||
# background link changes. Present its cover before replacing either path.
|
||||
if [[ -n $THEME_BACKGROUND_INTRO ]]; then
|
||||
wait_for_theme_intro themeIntroCoverStatus "$(stat -Lc '%d:%i' "$NEXT_THEME_PATH")" "loading"
|
||||
fi
|
||||
|
||||
# Swap next theme in as current
|
||||
rm -rf "$CURRENT_THEME_PATH"
|
||||
mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"
|
||||
@@ -418,7 +489,6 @@ flock -u 9
|
||||
|
||||
post_theme_commands=(
|
||||
omarchy-restart-terminal
|
||||
omarchy-restart-hyprctl
|
||||
omarchy-restart-btop
|
||||
omarchy-restart-opencode
|
||||
omarchy-restart-helix
|
||||
@@ -437,6 +507,14 @@ post_theme_commands=(
|
||||
)
|
||||
|
||||
if [[ $THEME_HEADLESS != "1" ]]; then
|
||||
# Let the wallpaper and bar finish their reveal before app retints compete
|
||||
# with decoding and the shell's frame rendering. An intro reloads Hyprland
|
||||
# after playback; ordinary theme changes reload it with the app retints.
|
||||
if [[ -n $THEME_INTRO_TOKEN ]]; then
|
||||
wait_for_theme_intro themeIntroStatus "$THEME_INTRO_TOKEN" "pending"
|
||||
else
|
||||
post_theme_commands+=(omarchy-restart-hyprctl)
|
||||
fi
|
||||
run_parallel "${post_theme_commands[@]}"
|
||||
|
||||
# Call hook on theme set
|
||||
|
||||
@@ -29,6 +29,8 @@ if [[ -n $OMARCHY_THEME_SYNC_FROM ]] || ! omarchy-toggle-enabled herdr-theme-syn
|
||||
fi
|
||||
|
||||
theme=$(cat "$HOME/.local/state/omarchy/current/theme.name" 2>/dev/null)
|
||||
control_dir="$XDG_RUNTIME_DIR/omarchy-theme-sync"
|
||||
mkdir -m 700 -p "$control_dir"
|
||||
|
||||
# Runs on the remote machine under a login shell, so OMARCHY_PATH and PATH are set there.
|
||||
# SSH carries no desktop session, so the environment comes from the live Hyprland instance.
|
||||
@@ -66,10 +68,13 @@ read -r -d '' REMOTE_SCRIPT <<'EOF'
|
||||
} </dev/null
|
||||
EOF
|
||||
|
||||
# One machine at a time, so an SSH agent like 1Password asks for approval at most once.
|
||||
# Reuse each authenticated connection for ten idle minutes, so an SSH agent like
|
||||
# 1Password does not need to approve every theme change. Connect one machine at a time.
|
||||
# SSH joins its arguments into a shell command, so the theme travels on stdin as a quoted assignment.
|
||||
for target in $targets; do
|
||||
result=$(printf 'theme=%q from=%q\n%s\n' "$theme" "$(hostname)" "$REMOTE_SCRIPT" |
|
||||
timeout 60 ssh -o BatchMode=yes -o ConnectTimeout=5 "$target" bash -l -s 2>&1 | tail -n 1)
|
||||
timeout 60 ssh -o BatchMode=yes -o ConnectTimeout=5 \
|
||||
-o ControlMaster=auto -o ControlPersist=600 -o "ControlPath=$control_dir/%C" \
|
||||
"$target" bash -l -s 2>&1 | tail -n 1)
|
||||
echo "$(date '+%F %T') $target: ${result:-unreachable}"
|
||||
done
|
||||
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
|
||||
+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"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install and configure Voxtype dictation
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
# Install voxtype and configure it for use.
|
||||
|
||||
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
|
||||
omarchy-pkg-add wtype voxtype-bin
|
||||
|
||||
# Setup voxtype
|
||||
mkdir -p ~/.config/voxtype
|
||||
cp "$OMARCHY_PATH/default/voxtype/config.toml" ~/.config/voxtype/
|
||||
|
||||
voxtype setup --download --no-post-install
|
||||
if omarchy-hw-vulkan; then
|
||||
voxtype setup gpu --enable || true
|
||||
fi
|
||||
voxtype setup systemd
|
||||
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-restart-shell
|
||||
omarchy-notification-send -g "Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
fi
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Stream voxtype --follow status as bar-friendly JSON
|
||||
|
||||
if omarchy-cmd-missing voxtype; then
|
||||
echo '{"alt": "", "class": "idle", "tooltip": ""}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Keep the status follower as the direct child of the bar. Quickshell
|
||||
# clean up their direct child when they restart; a shell pipeline/coproc would
|
||||
# leave `voxtype status --follow` orphaned under the user systemd instance.
|
||||
exec setpriv --pdeathsig TERM voxtype status --follow --extended --format json
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Keep Pinyin offline without prompting to enable cloud prediction.
|
||||
FirstRun=False
|
||||
CloudPinyinEnabled=False
|
||||
@@ -0,0 +1,2 @@
|
||||
[TriggerKey]
|
||||
0=Super+semicolon
|
||||
@@ -0,0 +1 @@
|
||||
Allow Overriding System XKB Settings=False
|
||||
@@ -0,0 +1,19 @@
|
||||
# Omarchy binds Super+I. Leave Ctrl+Space to tmux/Herdr and other Super keys to the desktop.
|
||||
[Hotkey/TriggerKeys]
|
||||
0=Zenkaku_Hankaku
|
||||
1=Hangul
|
||||
|
||||
[Hotkey/ActivateKeys]
|
||||
0=Henkan
|
||||
|
||||
[Hotkey/DeactivateKeys]
|
||||
0=Muhenkan
|
||||
|
||||
[Hotkey/AltTriggerKeys]
|
||||
|
||||
[Hotkey/EnumerateGroupForwardKeys]
|
||||
|
||||
[Hotkey/EnumerateGroupBackwardKeys]
|
||||
|
||||
[Behavior]
|
||||
ActiveByDefault=False
|
||||
@@ -0,0 +1 @@
|
||||
superwhisper
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# To put it into use, remove .sample from this file name.
|
||||
|
||||
# Example: add an Include line above [core].
|
||||
# Example: add an Include line above the first repository, [omarchy].
|
||||
# Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself.
|
||||
|
||||
CONF=/etc/pacman.conf
|
||||
@@ -21,4 +21,4 @@ MARKER="Include = $SNIPPET"
|
||||
[[ -r $SNIPPET ]] || exit 0
|
||||
grep -qxF "$MARKER" "$CONF" && exit 0
|
||||
|
||||
sudo sed -i "0,/^\[core\]/s||$MARKER\n\n[core]|" "$CONF"
|
||||
sudo sed -i "0,/^\[omarchy\]/s||$MARKER\n\n[omarchy]|" "$CONF"
|
||||
@@ -21,9 +21,6 @@
|
||||
{
|
||||
"id": "omarchy.indicators"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.elsewhen"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.clock",
|
||||
"format": "dddd HH:mm",
|
||||
@@ -31,7 +28,7 @@
|
||||
"verticalFormat": "HH\n\u2014\nmm"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.keyboard-layout"
|
||||
"id": "omarchy.elsewhen"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.weather"
|
||||
@@ -41,6 +38,9 @@
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"id": "omarchy.keyboard-layout"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.tray"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,11 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
|
||||
-- for normal open/close, and skip it for panel handoff.
|
||||
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" })
|
||||
|
||||
-- A boot intro hands the desktop between OWE's video layer and the shell's
|
||||
-- background layer. The media fades itself, so a compositor fade on either
|
||||
-- layer only dips the screen toward the empty desktop during the handoff.
|
||||
hl.layer_rule({ match = { namespace = "^(omarchy-background|owe-background)$" }, no_anim = true, animation = "none" })
|
||||
|
||||
-- Dev gallery is the main shell workbench; open it maximized like
|
||||
-- SUPER+ALT+F so component previews have the whole workspace.
|
||||
o.window({ class = "^org.quickshell$", title = "^Omarchy shell – dev gallery$" }, { maximize = true })
|
||||
@@ -1,4 +1,12 @@
|
||||
require("default.hypr.startup-cursor")
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
-- Applications inherit the user's cursor; only the compositor starts blank.
|
||||
if omarchy_startup_cursor_pending then
|
||||
hl.env("XCURSOR_PATH", omarchy_startup_cursor.path)
|
||||
hl.env("XCURSOR_THEME", omarchy_startup_cursor.xcursor)
|
||||
end
|
||||
|
||||
-- Slow app launch fix -- set systemd vars before starting session services.
|
||||
hl.exec_cmd("systemctl --user import-environment $(env | cut -d'=' -f 1)")
|
||||
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Bound only once the chosen backend's own program is installed (Omarchy ships
|
||||
-- every adapter): an aarch64 machine has no Superwhisper build, and
|
||||
-- push-to-talk would hold Right Alt (AltGr) for nothing.
|
||||
local pipe = io.popen("omarchy-default-dictation 2>/dev/null", "r")
|
||||
local backend = pipe and pipe:read("*l")
|
||||
if pipe then
|
||||
pipe:close()
|
||||
end
|
||||
if backend and backend:match("^[%w%-]+$") and not o.cmd_missing(backend) then
|
||||
o.bind("SUPER + CTRL + X", "Toggle dictation", "omarchy-dictation toggle")
|
||||
o.bind_hold("F9", "Start dictation (push-to-talk)", "omarchy-dictation start", "Stop dictation (push-to-talk)", "omarchy-dictation stop")
|
||||
-- A modifier's mask changes between its press and release. Match the keysym
|
||||
-- independently of that mask; AltGr layouts use a different keysym.
|
||||
o.bind_hold("ALT + Alt_R", "Start dictation (push-to-talk)", "omarchy-dictation start", "Stop dictation (push-to-talk)", "omarchy-dictation stop", { ignore_mods = true })
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Same terminal tag test as clipboard.lua.
|
||||
local function active_window_is_terminal()
|
||||
local window = hl.get_active_window()
|
||||
if not window then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, tag in ipairs(window.tags or {}) do
|
||||
if tag:gsub("%*$", "") == "terminal" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Keep the desktop shortcut independent of Fcitx's custom switching keys.
|
||||
-- Read the live Fcitx group so setup works without a compositor reload.
|
||||
o.bind("SUPER + I", "Switch input language", "omarchy-input-method cycle")
|
||||
o.bind("SUPER + SHIFT + I", "Switch input language back", "omarchy-input-method cycle back")
|
||||
|
||||
-- Pass the tmux/Herdr prefix through and return to direct input, so the next
|
||||
-- key reaches the multiplexer instead of the engine's composition.
|
||||
o.bind("CTRL + SPACE", "Direct input for the terminal prefix", function()
|
||||
if active_window_is_terminal() then
|
||||
-- Wait for the local controller reply before dispatching the next key.
|
||||
o.shell_succeeds("timeout 0.3 fcitx5-remote --check -c")
|
||||
end
|
||||
end, { non_consuming = true })
|
||||
@@ -0,0 +1,43 @@
|
||||
-- Defaults for Japanese (JIS) keyboards, which only apply when the installed
|
||||
-- layout is jp. See default/hypr/keyboard.lua.
|
||||
|
||||
local keyboard = require("default.hypr.keyboard")
|
||||
|
||||
if not keyboard.jis() then
|
||||
return
|
||||
end
|
||||
|
||||
-- Same down/up split as the universal clipboard shortcuts in clipboard.lua.
|
||||
local function send_shortcut_once(mods, key)
|
||||
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
|
||||
|
||||
hl.timer(function()
|
||||
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
|
||||
end, { timeout = 50, type = "oneshot" })
|
||||
end
|
||||
|
||||
local function active_window_class()
|
||||
local window = hl.get_active_window()
|
||||
return ((window and window.class) or ""):lower()
|
||||
end
|
||||
|
||||
-- On JIS, + is Shift + ;, so apps see Ctrl + Shift + semicolon and never zoom
|
||||
-- in. Hand them Ctrl + keypad plus instead, which Chromium, Electron apps,
|
||||
-- VS Code, and Firefox read as zoom in. Obsidian only listens for Ctrl + ^,
|
||||
-- which JIS types without Shift.
|
||||
local zoom_in_overrides = {
|
||||
obsidian = { "CTRL", "asciicircum" },
|
||||
}
|
||||
|
||||
o.bind("CTRL + SHIFT + semicolon", "Zoom in (JIS + key)", function()
|
||||
local class = active_window_class()
|
||||
|
||||
for pattern, chord in pairs(zoom_in_overrides) do
|
||||
if class:find(pattern, 1, true) then
|
||||
send_shortcut_once(chord[1], chord[2])
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
send_shortcut_once("CTRL", "KP_Add")
|
||||
end)
|
||||
@@ -34,6 +34,9 @@ o.bind("SUPER + CTRL + Delete", "Toggle laptop display", "omarchy-hyprland-monit
|
||||
o.bind("SUPER + CTRL + ALT + Delete", "Toggle laptop display mirroring", "omarchy-hyprland-monitor-internal-mirror toggle")
|
||||
o.bind("switch:on:Lid Switch", nil, "omarchy-system-lid-close", { locked = true })
|
||||
o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
|
||||
-- Apple Silicon reports its lid through the SMC, under its own switch name.
|
||||
o.bind("switch:on:Apple SMC power/lid events", nil, "omarchy-system-lid-close", { locked = true })
|
||||
o.bind("switch:off:Apple SMC power/lid events", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
|
||||
|
||||
o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot")
|
||||
o.bind("ALT + PRINT", "Screenrecording", "omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
if o.cmd_present("voxtype") then
|
||||
o.bind("SUPER + CTRL + X", "Toggle dictation", "voxtype record toggle")
|
||||
o.bind("F9", "Start dictation (push-to-talk)", "voxtype record start")
|
||||
o.bind("F9", "Stop dictation (push-to-talk)", "voxtype record stop", { release = true })
|
||||
end
|
||||
Loaded 100 of 512 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user