Compare commits
86
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
|
||||
@@ -96,16 +120,6 @@ Exceptions are allowed for migration and package-helper scripts where the helper
|
||||
- Do not add `aliases` to new menu entries. Aliases are reserved for
|
||||
established alternate names users already type, kept for compatibility.
|
||||
|
||||
# Region
|
||||
|
||||
- `/etc/omarchy/region` holds `cn`; no file means Global. `omarchy-region` prints the region and switches it (`omarchy region cn|global`); the profile lives in `default/regions/cn/`, its contract in `docs/regions.md`, user-facing docs in `manual/52-china.md`.
|
||||
- `install/config/region.sh` is the one system leaf for both the ISO install (the `/etc/omarchy/region` marker the installer derives from the timezone) and the runtime switch (`OMARCHY_REGION`): the region file and the profile drop-ins it symlinks. Input methods are not part of a region: the installer's keyboard choice and `omarchy-setup-input` own them (`docs/input-methods.md`).
|
||||
- `omarchy-apply-pacman` renders pacman for every caller: the channel templates, the profile's `pacman/mirrorlist-<channel>.prepend` ahead of the mirrorlist, and its `pacman.conf.append`. Nothing in `omarchy update` or migrations may change the region or `/etc/locale.conf`.
|
||||
- Language is separate from region: `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`, the installer sets it from its first screen, and nothing keyed by region touches it.
|
||||
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard.
|
||||
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
|
||||
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
|
||||
|
||||
# Config Structure
|
||||
|
||||
- `config/` - default configs copied to `~/.config/`
|
||||
|
||||
@@ -71,7 +71,6 @@ The manual lives in [`manual/`](manual/), which is its authoritative source.
|
||||
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||
- [China](manual/52-china.md)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,10 +24,6 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||
|
||||
## Text
|
||||
|
||||
Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it.
|
||||
|
||||
## IPC
|
||||
|
||||
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
||||
|
||||
@@ -59,7 +59,6 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
|
||||
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
||||
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
||||
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
||||
GROUP_DESCRIPTIONS[language]="System language"
|
||||
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
||||
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
||||
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
||||
@@ -75,7 +74,6 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
|
||||
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
||||
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
||||
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
||||
GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup"
|
||||
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
|
||||
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
||||
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
||||
|
||||
@@ -85,7 +85,7 @@ finish() {
|
||||
fail() {
|
||||
if [[ $events == "true" ]]; then
|
||||
printf '@@omarchy error %s\n' "$1"
|
||||
omarchy-notification-send -g -u critical "$(omarchy-text "Couldn't add the %1 account" "$name")" "$1"
|
||||
omarchy-notification-send -g -u critical "Couldn't add the $name account" "$1"
|
||||
else
|
||||
echo "$1" >&2
|
||||
fi
|
||||
@@ -113,9 +113,9 @@ interruptible() {
|
||||
}
|
||||
|
||||
if omarchy-cmd-missing "$provider"; then
|
||||
say "$(omarchy-text "Installing the %1 CLI…" "$name")"
|
||||
say "Installing the $name CLI…"
|
||||
if [[ $events == "true" ]]; then
|
||||
mise use -g "$package" >/dev/null 2>&1 || fail "$(omarchy-text "Installing the %1 CLI didn't finish." "$name")"
|
||||
mise use -g "$package" >/dev/null 2>&1 || fail "Installing the $name CLI didn't finish."
|
||||
else
|
||||
mise use -g "$package" || exit 1
|
||||
fi
|
||||
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
|
||||
else
|
||||
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
||||
fi
|
||||
[[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")"
|
||||
[[ -n $account_home ]] || fail "No $name account named $reauth."
|
||||
|
||||
if [[ $account_home == "$HOME/.$provider" ]]; then
|
||||
say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
|
||||
login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||
say "Sign in to $name again in the browser window that opens."
|
||||
login || fail "The $name sign-in didn't finish."
|
||||
else
|
||||
say "$(omarchy-text "Sign in again as this account in the private window that opens.")"
|
||||
say "Sign in again as this account in the private window that opens."
|
||||
private_browser
|
||||
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish."
|
||||
fi
|
||||
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
finish "$(omarchy-text "Signed in to %1 again." "$name")"
|
||||
finish "Signed in to $name again."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The first subscription signs in where the CLI always looks, through your
|
||||
# normal browser: nothing is signed in there yet that it could be mistaken for.
|
||||
if ! primary_signed_in "$provider"; then
|
||||
say "$(omarchy-text "Sign in to %1 in the browser window that opens." "$name")"
|
||||
say "Sign in to $name in the browser window that opens."
|
||||
if login && primary_signed_in "$provider"; then
|
||||
# The first agent signed in on a machine with none chosen becomes the
|
||||
# default, so `omarchy agent` and the panel's starter prompts have one to
|
||||
@@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then
|
||||
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
|
||||
fi
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
finish "$(omarchy-text "Signed in to %1." "$name")"
|
||||
finish "Signed in to $name."
|
||||
else
|
||||
fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||
fail "The $name sign-in didn't finish."
|
||||
fi
|
||||
else
|
||||
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
||||
@@ -189,7 +189,7 @@ else
|
||||
fi
|
||||
|
||||
if [[ $events == "true" ]]; then
|
||||
say "$(omarchy-text "Sign in as the account you're adding in the private window that opens.")"
|
||||
say "Sign in as the account you're adding in the private window that opens."
|
||||
else
|
||||
cat <<EOF
|
||||
|
||||
@@ -201,7 +201,7 @@ Don't log out of the $name CLI to do this: logging out revokes the saved sign-in
|
||||
EOF
|
||||
fi
|
||||
|
||||
home=$(omarchy-agent-account-state begin "$provider") || fail "$(omarchy-text "Couldn't set up a home for the new account.")"
|
||||
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account."
|
||||
|
||||
private_browser
|
||||
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
||||
@@ -211,9 +211,9 @@ EOF
|
||||
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
if [[ $events == "true" ]]; then
|
||||
finish "$(omarchy-text "Added %1." "$added")"
|
||||
finish "Added $added."
|
||||
else
|
||||
finish "$(omarchy-text "Added %1." "$added")"
|
||||
finish "Added $added."
|
||||
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -73,11 +73,6 @@ class AccountError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def text(english: str, *args: object) -> str:
|
||||
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- paths
|
||||
|
||||
|
||||
@@ -340,12 +335,12 @@ def begin(provider: str) -> Path:
|
||||
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
||||
pending = pending.resolve()
|
||||
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
||||
raise AccountError(text("%1 is not a pending %2 login", pending, PROVIDERS[provider]['name']))
|
||||
raise AccountError(f"{pending} is not a pending {PROVIDERS[provider]['name']} login")
|
||||
|
||||
found = identity(provider, pending)
|
||||
if not found["accountId"]:
|
||||
shutil.rmtree(pending, ignore_errors=True)
|
||||
raise AccountError(text("The login didn't finish, so no account was added."))
|
||||
raise AccountError("The login didn't finish, so no account was added.")
|
||||
|
||||
with Locked(provider) as registry:
|
||||
# Each home is asked who it is now, not trusted from when it was added: a
|
||||
@@ -355,10 +350,10 @@ def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
||||
for account in registry["accounts"]:
|
||||
if account.get("accountId") == found["accountId"]:
|
||||
shutil.rmtree(pending, ignore_errors=True)
|
||||
raise AccountError(text(
|
||||
"That's %1 (%2). Try again, and sign in as the other account in the private window.",
|
||||
account["label"], found["email"] or text("already added"),
|
||||
))
|
||||
raise AccountError(
|
||||
f"That's {account['label']} ({found['email'] or 'already added'}). "
|
||||
"Try again, and sign in as the other account in the private window."
|
||||
)
|
||||
|
||||
label = label.strip() or found["email"].split("@")[0] or "Account"
|
||||
account_id = unique_id(provider, registry, label)
|
||||
@@ -400,11 +395,11 @@ def unique_id(provider: str, registry: dict[str, Any], label: str, keep: dict[st
|
||||
def rename(provider: str, target: str, label: str) -> dict[str, Any]:
|
||||
label = label.strip()
|
||||
if not label:
|
||||
raise AccountError(text("Give the account a name"))
|
||||
raise AccountError("Give the account a name")
|
||||
with Locked(provider) as registry:
|
||||
account = find(registry, target)
|
||||
if not account:
|
||||
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
||||
new_id = unique_id(provider, registry, label, keep=account)
|
||||
if not account.get("primary") and not account.get("home"):
|
||||
account["home"] = str(account_home(provider, account))
|
||||
@@ -433,7 +428,7 @@ def use(provider: str, target: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
|
||||
account = find(registry, target)
|
||||
if not account:
|
||||
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
||||
registry["active"] = account["id"]
|
||||
registry["alert"] = ""
|
||||
if not account.get("primary"):
|
||||
@@ -461,12 +456,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
||||
with Locked(provider) as registry:
|
||||
account = find(registry, target)
|
||||
if not account:
|
||||
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
||||
if account.get("primary"):
|
||||
raise AccountError(text("%1 is the login in %2; it can't be removed here.", account["label"], primary_home(provider)))
|
||||
raise AccountError(f"{account['label']} is the login in {primary_home(provider)}; it can't be removed here.")
|
||||
home = account_home(provider, account)
|
||||
if home_in_use(provider, home):
|
||||
raise AccountError(text("A running %1 session uses %2. Quit it first, then remove the account.", PROVIDERS[provider]['name'], account["label"]))
|
||||
raise AccountError(f"A running {PROVIDERS[provider]['name']} session uses {account['label']}. Quit it first, then remove the account.")
|
||||
# Only the account's own directory goes. Its links point into the primary
|
||||
# home, and rmtree removes a link without following it.
|
||||
if home.resolve().parent == (accounts_root() / provider).resolve():
|
||||
@@ -479,14 +474,14 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
||||
|
||||
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
|
||||
if mode not in SWITCH_MODES:
|
||||
raise AccountError(text("Switch mode is manual or auto"))
|
||||
raise AccountError("Switch mode is manual or auto")
|
||||
with Locked(provider) as registry:
|
||||
registry["switch"] = mode
|
||||
if threshold is not None:
|
||||
try:
|
||||
registry["threshold"] = max(50, min(100, int(threshold)))
|
||||
except ValueError:
|
||||
raise AccountError(text("The threshold is a percentage between 50 and 100"))
|
||||
raise AccountError("The threshold is a percentage between 50 and 100")
|
||||
registry["alert"] = ""
|
||||
return dict(registry)
|
||||
|
||||
@@ -561,10 +556,10 @@ def duration(delta: dt.timedelta) -> str:
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
if days:
|
||||
return text("%1d %2h", days, hours)
|
||||
return f"{days}d {hours}h"
|
||||
if hours:
|
||||
return text("%1h %2m", hours, minutes)
|
||||
return text("%1m", minutes)
|
||||
return f"{hours}h {minutes}m"
|
||||
return f"{minutes}m"
|
||||
|
||||
|
||||
# The whole switching policy, with no I/O: given the registry and the per
|
||||
@@ -664,23 +659,21 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool:
|
||||
threshold = registry["threshold"]
|
||||
if decision["action"] == "exhausted":
|
||||
soonest = decision["soonest"]
|
||||
staying = text("Staying on %1.", source["label"])
|
||||
if soonest:
|
||||
staying = text("Staying on %1. %2 resets in %3.", source["label"], soonest[1]["label"], duration(soonest[0] - now))
|
||||
notify(text("All %1 accounts are over %2%", name, threshold), staying)
|
||||
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else ""
|
||||
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}")
|
||||
return False
|
||||
|
||||
target = decision["to"]
|
||||
headline = text("%1 is at %2% of a %3 limit", source["label"], round(decision["fromPeak"]), name)
|
||||
headline = f"{source['label']} is at {round(decision['fromPeak'])}% of a {name} limit"
|
||||
if decision["action"] == "switch":
|
||||
registry["active"] = target["id"]
|
||||
if not target.get("primary"):
|
||||
link_shared(provider, account_home(provider, target))
|
||||
notify(headline, text("New %1 sessions now use %2 (%3%). Running sessions stay on %4.", name, target["label"], round(decision["toPeak"]), source["label"]))
|
||||
notify(headline, f"New {name} sessions now use {target['label']} ({round(decision['toPeak'])}%). Running sessions stay on {source['label']}.")
|
||||
return True
|
||||
notify(
|
||||
headline,
|
||||
text("%1 is at %2%. Click to switch new sessions to it.", target["label"], round(decision["toPeak"])),
|
||||
f"{target['label']} is at {round(decision['toPeak'])}%. Click to switch new sessions to it.",
|
||||
["omarchy-agent-account-use", provider, target["id"]],
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -31,9 +31,9 @@ active=$(jq -r '.active.label + (if (.active.plan // "") != "" then " (" + .acti
|
||||
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
|
||||
echo "New $name sessions already use $active."
|
||||
else
|
||||
message=$(omarchy-text "Running sessions stay on %1." "$previous")
|
||||
message="Running sessions stay on $previous."
|
||||
echo "New $name sessions now use $active. $message"
|
||||
omarchy-notification-send -g "$(omarchy-text "New %1 sessions now use %2" "$name" "$active")" "$message"
|
||||
omarchy-notification-send -g "New $name sessions now use $active" "$message"
|
||||
# The usage record's own limits describe the active account.
|
||||
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
||||
fi
|
||||
@@ -22,7 +22,6 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@@ -33,11 +32,6 @@ from typing import Any
|
||||
|
||||
AGENT_ID = "claude"
|
||||
AGENT_NAME = "Claude Code"
|
||||
def text(english, *args):
|
||||
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||
|
||||
|
||||
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
|
||||
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
||||
PROBE_MIN_INTERVAL_SECONDS = 15
|
||||
@@ -995,12 +989,11 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
||||
except urllib.error.HTTPError as error:
|
||||
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
||||
if error.code == 429:
|
||||
if retry_after:
|
||||
help_text = text("Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", retry_after)
|
||||
else:
|
||||
help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.")
|
||||
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
|
||||
f" (retry after {retry_after}s)" if retry_after else ""
|
||||
) + ". Local Claude Code stats are still shown."
|
||||
else:
|
||||
help_text = text("Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", error.code)
|
||||
help_text = f"Anthropic's usage endpoint returned status {error.code}. Local Claude Code stats are still shown."
|
||||
return {"ok": False, "helpText": help_text}
|
||||
except Exception:
|
||||
# A transport failure reached no server at all — no route, no DNS. Any
|
||||
|
||||
@@ -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
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
|
||||
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
|
||||
# omarchy:requires-sudo=true
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
|
||||
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --render only builds the two files, so an unprivileged caller can hand root
|
||||
# nothing but plain copies of the finished result.
|
||||
render_dir=""
|
||||
if [[ ${1:-} == "--render" ]]; then
|
||||
(( $# == 3 || $# == 4 )) || usage
|
||||
channel="$2"
|
||||
render_dir="$3"
|
||||
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
|
||||
etc="${4:-/}/etc"
|
||||
else
|
||||
(( $# >= 1 && $# <= 2 )) || usage
|
||||
channel="$1"
|
||||
etc="${2:-/}/etc"
|
||||
fi
|
||||
|
||||
case "$channel" in
|
||||
stable|rc|edge) ;;
|
||||
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# The ISO writes this plain-text marker before target finalization. Missing
|
||||
# markers keep existing installations global; never source it as shell code.
|
||||
region=global
|
||||
if [[ -e $etc/omarchy/region ]]; then
|
||||
region=$(<"$etc/omarchy/region")
|
||||
fi
|
||||
case "$region" in
|
||||
global|cn) ;;
|
||||
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Build both files before replacing either. Explicit refreshes retain backups;
|
||||
# ordinary updates do not call this command or enforce regional defaults.
|
||||
if [[ -n $render_dir ]]; then
|
||||
staging="$render_dir"
|
||||
else
|
||||
staging=$(mktemp -d)
|
||||
trap 'rm -rf "$staging"' EXIT
|
||||
fi
|
||||
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
|
||||
if [[ $region == "global" ]]; then
|
||||
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
|
||||
else
|
||||
profile="$OMARCHY_PATH/default/regions/$region/pacman"
|
||||
printf '\n' >> "$staging/pacman.conf"
|
||||
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
|
||||
# pacman tries servers in order, so the regional mirror leads and the
|
||||
# channel's own mirror stays as the fallback.
|
||||
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
|
||||
fi
|
||||
|
||||
if [[ -z $render_dir ]]; then
|
||||
mkdir -p "$etc/pacman.d"
|
||||
for file in pacman.conf pacman.d/mirrorlist; do
|
||||
if [[ -e $etc/$file ]]; then
|
||||
cp -f "$etc/$file" "$etc/$file.bak"
|
||||
fi
|
||||
done
|
||||
# Copying over an existing file keeps its owner and mode, as refresh always has.
|
||||
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
|
||||
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
|
||||
fi
|
||||
@@ -6,8 +6,8 @@ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
|
||||
|
||||
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
||||
omarchy-brightness-keyboard-mute on
|
||||
omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
|
||||
omarchy-osd -i microphone-muted -m "Microphone muted"
|
||||
else
|
||||
omarchy-brightness-keyboard-mute off
|
||||
omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
|
||||
omarchy-osd -i microphone -m "Microphone on"
|
||||
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
|
||||
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
|
||||
sinks_count=$(jq 'length' <<<"$sinks")
|
||||
|
||||
if (( sinks_count == 0 )); then
|
||||
omarchy-osd -m "$(omarchy-text "No audio devices found")"
|
||||
omarchy-osd -m "No audio devices found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,5 +13,5 @@ fi
|
||||
|
||||
level=$1
|
||||
|
||||
omarchy-notification-send -g -u critical "$(omarchy-text "Time to recharge!")" "$(omarchy-text "Battery is down to %1%" "$level")" -i battery-caution -t 30000
|
||||
omarchy-notification-send -g -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000
|
||||
omarchy-hook battery-low "$level"
|
||||
@@ -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"
|
||||
|
||||
@@ -129,9 +150,9 @@ if [[ $charge_holding == "true" ]]; then
|
||||
threshold_label="${threshold_end}%"
|
||||
fi
|
||||
|
||||
omarchy-text "Battery %1% · Holding at %2 · %3W / %4Wh" "$percentage" "$threshold_label" "$power_rate" "$capacity"
|
||||
echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh"
|
||||
elif [[ $state == "charging" ]]; then
|
||||
omarchy-text "Battery %1% · %2 to full · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||
echo "Battery ${percentage}% · ${time_remaining} to full · ${power_rate}W / ${capacity}Wh"
|
||||
else
|
||||
omarchy-text "Battery %1% · %2 left · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||
echo "Battery ${percentage}% · ${time_remaining} left · ${power_rate}W / ${capacity}Wh"
|
||||
fi
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ SELECTION=$(slurp 2>/dev/null)
|
||||
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
||||
|
||||
if [[ -z $RESULT ]]; then
|
||||
omarchy-notification-send -g -u critical "$(omarchy-text "No QR code found")" "$(omarchy-text "Select a region containing a QR code")"
|
||||
omarchy-notification-send -g -u critical "No QR code found" "Select a region containing a QR code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -33,4 +33,4 @@ fi
|
||||
# the notification history, and an unmarked copy would be retained by clipboard
|
||||
# history. Pasting still works; only the recorded copy is given up.
|
||||
printf '%s' "$RESULT" | wl-copy --sensitive
|
||||
omarchy-notification-send -g "$(omarchy-text "QR code copied to clipboard")"
|
||||
omarchy-notification-send -g "QR code copied to clipboard"
|
||||
@@ -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 "$(omarchy-text "Screen recording directory does not exist: %1" "$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
|
||||
|
||||
@@ -76,7 +97,7 @@ start_webcam_overlay() {
|
||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
|
||||
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
@@ -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,51 +329,102 @@ 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"
|
||||
omarchy-notification-send -u critical -t 5000 "$(omarchy-text "Screen recording error")" "$(omarchy-text "Recording process had to be force-killed. Video may be corrupted.")"
|
||||
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
|
||||
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
||||
@@ -241,7 +434,7 @@ stop_screenrecording() {
|
||||
# Generate a preview thumbnail from the first frame
|
||||
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
|
||||
|
||||
omarchy-notification-send "$(omarchy-text "Screen recording saved")" "$(omarchy-text "Open with Super + Alt + , (or click this)")" \
|
||||
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
||||
-t 10000 --image "${preview:-$filename}" \
|
||||
--exec mpv -- "$filename"
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
mapfile -t devices < <(omarchy-capture-webcam-list)
|
||||
if (( ${#devices[@]} == 0 )); then
|
||||
omarchy-notification-send "$(omarchy-text "No webcam devices found")" -u critical -t 3000
|
||||
omarchy-notification-send "No webcam devices found" -u critical -t 3000
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ TEXT=$(grim -g "$SELECTION" - | tesseract stdin stdout --oem 1 --psm 6 -l "${OMA
|
||||
[[ -z $TEXT ]] && exit 1
|
||||
|
||||
printf "%s" "$TEXT" | wl-copy
|
||||
omarchy-notification-send -g "$(omarchy-text "Copied text from selection to clipboard")"
|
||||
omarchy-notification-send -g "Copied text from selection to clipboard"
|
||||
+31
-25
@@ -47,31 +47,9 @@ validate_dev_checkout() {
|
||||
fi
|
||||
}
|
||||
|
||||
# The dev channel refreshes pacman with the checkout's own tools. A checkout
|
||||
# without this machine's region profile would silently drop its repositories.
|
||||
validate_dev_region() {
|
||||
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
|
||||
|
||||
[[ -f $region_file ]] && region=$(<"$region_file")
|
||||
[[ $region == "global" ]] && return 0
|
||||
|
||||
# The refresh after linking needs a runnable helper and every fragment, not
|
||||
# just a directory with the region's name: dev refreshes edge, and leaving
|
||||
# dev later refreshes stable or rc with this checkout's tools.
|
||||
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
|
||||
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
|
||||
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
|
||||
[[ -r $profile/$fragment ]] || supported=false
|
||||
done
|
||||
if ! $supported; then
|
||||
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
|
||||
fi
|
||||
}
|
||||
|
||||
link_dev_checkout() {
|
||||
local checkout="$1" required
|
||||
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
|
||||
validate_dev_region "$checkout"
|
||||
|
||||
# Check the destination before changing /etc/omarchy.conf or sudo's path.
|
||||
# An existing checkout is not pulled automatically and may predate this policy.
|
||||
@@ -82,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
|
||||
}
|
||||
|
||||
@@ -124,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)
|
||||
;;
|
||||
@@ -137,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
|
||||
|
||||
@@ -18,7 +18,7 @@ copy_url() {
|
||||
|
||||
[[ -n $url ]] || return 1
|
||||
printf '%s' "$url" | wl-copy --type text/plain
|
||||
omarchy-notification-send -g "$(omarchy-text "URL copied to clipboard")"
|
||||
omarchy-notification-send -g "URL copied to clipboard"
|
||||
}
|
||||
|
||||
reply_copied() {
|
||||
|
||||
@@ -87,7 +87,7 @@ download_url() {
|
||||
|
||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
||||
omarchy-notification-send -u critical -g "$(omarchy-text "No video found for download")" "$url"
|
||||
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -148,7 +148,7 @@ download_url() {
|
||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||
# is one discrete argument, so it never reaches a shell.
|
||||
omarchy-notification-send -g "$(omarchy-text "Download complete")" "$title" \
|
||||
omarchy-notification-send -g "Download complete" "$title" \
|
||||
-t 10000 --image "${preview:-$filepath}" \
|
||||
--exec mpv -- "$filepath" || true
|
||||
|
||||
@@ -160,7 +160,7 @@ download_url() {
|
||||
rm -f "$preview"
|
||||
) &
|
||||
else
|
||||
omarchy-notification-send -u critical -g "$(omarchy-text "Download failed")" "$url"
|
||||
omarchy-notification-send -u critical -g "Download failed" "$url"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -40,8 +40,8 @@ announce() {
|
||||
omarchy-notification-send \
|
||||
--urgency critical \
|
||||
--glyph "$CRASH_GLYPH" \
|
||||
"$(omarchy-text "Process crashed: %1" "$comm")" \
|
||||
"$(omarchy-text "Click to diagnose with AI")" \
|
||||
"Process crashed: $comm" \
|
||||
"Click to diagnose with AI" \
|
||||
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
||||
}
|
||||
|
||||
|
||||
@@ -48,4 +48,4 @@ fi
|
||||
|
||||
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
|
||||
|
||||
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default browser" "$name")"
|
||||
omarchy-notification-send -g $glyph "$name is now the default browser"
|
||||
@@ -59,4 +59,4 @@ fi
|
||||
mkdir -p "$(dirname "$editor_file")"
|
||||
printf '%s\n' "$editor" >"$editor_file"
|
||||
|
||||
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default editor" "$name")"
|
||||
omarchy-notification-send -g $glyph "$name is now the default editor"
|
||||
@@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <<EOF
|
||||
$desktop_id
|
||||
EOF
|
||||
|
||||
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default terminal" "$name")"
|
||||
omarchy-notification-send -g $glyph "$name is now the default terminal"
|
||||
@@ -169,7 +169,7 @@ set_terminal_size() {
|
||||
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
|
||||
local new_id
|
||||
new_id="$(omarchy-notification-send \
|
||||
"$(omarchy-text "Restart Foot to apply the new terminal font size")" \
|
||||
"Restart Foot to apply the new terminal font size" \
|
||||
"${replace[@]}" -p 2>/dev/null)" || true
|
||||
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
||||
fi
|
||||
|
||||
+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
|
||||
@@ -74,11 +74,11 @@ XML
|
||||
omarchy-restart-shell
|
||||
|
||||
if pgrep -x ghostty; then
|
||||
omarchy-notification-send -g "$(omarchy-text "You must restart Ghostty to see font change")"
|
||||
omarchy-notification-send -g "You must restart Ghostty to see font change"
|
||||
fi
|
||||
|
||||
if pgrep -x foot; then
|
||||
omarchy-notification-send -g "$(omarchy-text "You must restart Foot to see font change")"
|
||||
omarchy-notification-send -g "You must restart Foot to see font change"
|
||||
fi
|
||||
|
||||
omarchy-hook font-set "$font_name"
|
||||
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
|
||||
mapfile -t cores < <(omarchy-games-retro-cores)
|
||||
|
||||
if (( ${#cores[@]} == 0 )); then
|
||||
omarchy-notification-send -g "$(omarchy-text "No RetroArch cores found")" "/usr/lib/libretro"
|
||||
omarchy-notification-send -g "No RetroArch cores found" "/usr/lib/libretro"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -69,4 +69,4 @@ EOF
|
||||
chmod +x "$desktop_file"
|
||||
update-desktop-database "$desktop_dir" &>/dev/null || true
|
||||
|
||||
omarchy-notification-send -g "$(omarchy-text "%1 installed" "$game_name")" "$(omarchy-text "Start it with Super + Space")"
|
||||
omarchy-notification-send -g "$game_name installed" "Start it with Super + Space"
|
||||
@@ -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" ]]
|
||||
@@ -16,7 +16,7 @@ wake() {
|
||||
on() {
|
||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||
omarchy-hyprland-toggle $TOGGLE off
|
||||
omarchy-notification-send -g "$(omarchy-text "Laptop display enabled")"
|
||||
omarchy-notification-send -g "Laptop display enabled"
|
||||
fi
|
||||
|
||||
wake
|
||||
@@ -24,25 +24,25 @@ on() {
|
||||
|
||||
off() {
|
||||
if [[ -z $INTERNAL ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "No laptop display found")"
|
||||
omarchy-notification-send -g "No laptop display found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The name is written into generated Lua below, so only a plain connector
|
||||
# name may pass; anything else could execute on the next reload.
|
||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! omarchy-hyprland-monitor-external-active; then
|
||||
omarchy-notification-send -g "$(omarchy-text "Can't disable the only active display")"
|
||||
omarchy-notification-send -g "Can't disable the only active display"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
||||
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
||||
omarchy-notification-send -g "$(omarchy-text "Laptop display disabled")"
|
||||
omarchy-notification-send -g "Laptop display disabled"
|
||||
hyprctl reload
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
|
||||
|
||||
on() {
|
||||
if [[ -z $EXTERNAL ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "No external monitors found for mirror")"
|
||||
omarchy-notification-send -g "No external monitors found for mirror"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $INTERNAL ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "No laptop monitor found to mirror")"
|
||||
omarchy-notification-send -g "No laptop monitor found to mirror"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -26,7 +26,7 @@ on() {
|
||||
# names may pass; a user-created headless output can carry any name.
|
||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -35,7 +35,7 @@ on() {
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
|
||||
omarchy-notification-send -g "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
|
||||
omarchy-notification-send -g "Mirroring enabled ($EXTERNAL)"
|
||||
hyprctl reload
|
||||
fi
|
||||
}
|
||||
@@ -43,7 +43,7 @@ on() {
|
||||
off() {
|
||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||
omarchy-hyprland-toggle $TOGGLE off
|
||||
omarchy-notification-send -g "$(omarchy-text "Extended mode restored")"
|
||||
omarchy-notification-send -g "Extended mode restored"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ sync_clamshell() {
|
||||
(
|
||||
flock -n 9 || exit 0
|
||||
omarchy-hyprland-monitor-clamshell
|
||||
omarchy-system-lid-inhibit
|
||||
) 9>"$LOCK"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
# omarchy:summary=Toggle single-window square aspect ratio.
|
||||
|
||||
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
||||
on) omarchy-notification-send -g "$(omarchy-text "Enable single-window square aspect ratio")" ;;
|
||||
off) omarchy-notification-send -g "$(omarchy-text "Disable single-window square aspect ratio")" ;;
|
||||
on) omarchy-notification-send -g "Enable single-window square aspect ratio" ;;
|
||||
off) omarchy-notification-send -g "Disable single-window square aspect ratio" ;;
|
||||
esac
|
||||
@@ -40,14 +40,14 @@ notify_missing_width() {
|
||||
local key="$1"
|
||||
local workspace="$2"
|
||||
|
||||
omarchy-notification-send -g "$(omarchy-text "No saved width found for %1 on workspace %2" "$key" "$workspace")" "Use Super + Alt + Home to save one for this workspace."
|
||||
omarchy-notification-send -g "No saved width found for $key on workspace $workspace" "Use Super + Alt + Home to save one for this workspace."
|
||||
}
|
||||
|
||||
notify_saved_width() {
|
||||
local key="$1"
|
||||
local workspace="$2"
|
||||
|
||||
omarchy-notification-send -g "$(omarchy-text "Saved width for %1 on workspace %2" "$key" "$workspace")" "Restore using Super + Home on this workspace."
|
||||
omarchy-notification-send -g "Saved width for $key on workspace $workspace" "Restore using Super + Home on this workspace."
|
||||
}
|
||||
|
||||
hypr_dispatch() {
|
||||
|
||||
@@ -18,4 +18,4 @@ printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKS
|
||||
|
||||
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
|
||||
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
|
||||
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1" "$NEW_LAYOUT")"
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_LAYOUT"
|
||||
@@ -1,9 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Configure or inspect the input method selected at installation
|
||||
# omarchy:args=<status|configure [--seed|--defaults] [none|mozc|hangul|pinyin|chewing]>
|
||||
# omarchy:summary=Configure, inspect, or switch input methods
|
||||
# omarchy:args=<status|cycle [back]|configure --seed|--defaults>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
python "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
|
||||
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"
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -23,5 +23,5 @@ 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 "$(omarchy-text "Superwhisper Dictation Ready")" "$(omarchy-text "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X).")" -t 10000
|
||||
omarchy-notification-send -g "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
superwhisper settings
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
voxtype_lib_path="${OMARCHY_VOXTYPE_LIB_PATH:-/usr/lib/voxtype}"
|
||||
|
||||
if omarchy-cmd-missing voxtype; then
|
||||
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
|
||||
omarchy-pkg-add wtype voxtype-bin
|
||||
@@ -17,7 +19,8 @@ config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
mkdir -p "$config_home/voxtype"
|
||||
if [[ ! -e $config_home/voxtype/config.toml ]]; then
|
||||
cp "$OMARCHY_PATH/default/voxtype/config.toml" "$config_home/voxtype/"
|
||||
if omarchy-hw-vulkan; then
|
||||
# A Vulkan runtime is not enough: aarch64 voxtype-bin ships no Vulkan backend.
|
||||
if omarchy-hw-vulkan && [[ -x $voxtype_lib_path/voxtype-vulkan ]]; then
|
||||
voxtype setup gpu --enable || true
|
||||
fi
|
||||
fi
|
||||
@@ -30,4 +33,4 @@ systemctl --user enable --now voxtype.service
|
||||
|
||||
omarchy-dictation-use voxtype
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-notification-send -g "$(omarchy-text "Voxtype Dictation Ready")" "$(omarchy-text "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X).")" -t 10000
|
||||
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..."
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or switch the system language between English and Simplified Chinese
|
||||
# omarchy:args=[en|zh]
|
||||
# omarchy:examples=omarchy language | omarchy language zh
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
locale_conf=/etc/locale.conf
|
||||
|
||||
if (($# == 0)); then
|
||||
lang=$(sed -n 's/^LANG=//p' "$locale_conf")
|
||||
# locale.conf may quote the value.
|
||||
lang=${lang#[\"\']}
|
||||
printf '%s\n' "${lang%[\"\']}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case ${1:-} in
|
||||
en) lang=en_US.UTF-8 ;;
|
||||
zh) lang=zh_CN.UTF-8 ;;
|
||||
*)
|
||||
echo "Usage: omarchy-language [en|zh]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sudo sed -i "s/^#\(${lang/./\\.} UTF-8\)/\1/" /etc/locale.gen
|
||||
sudo locale-gen
|
||||
if grep -q '^LANG=' "$locale_conf"; then
|
||||
sudo sed -i "s/^LANG=.*/LANG=$lang/" "$locale_conf"
|
||||
else
|
||||
sudo sed -i '$a\' "$locale_conf"
|
||||
echo "LANG=$lang" | sudo tee -a "$locale_conf" >/dev/null
|
||||
fi
|
||||
|
||||
# The language reaches only a new session, so offer one in the new language.
|
||||
prompt=$(LANG=$lang omarchy-text "Log out now to switch the language?")
|
||||
log_out=$(LANG=$lang omarchy-text "Log out now")
|
||||
later=$(LANG=$lang omarchy-text "Later")
|
||||
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$log_out" --negative "$later" "$prompt" 2>&1; then
|
||||
omarchy-system-logout
|
||||
else
|
||||
LANG=$lang omarchy-text "Log back in to switch the language."
|
||||
fi
|
||||
@@ -11,5 +11,5 @@ if [[ -z $path ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
omarchy-notification-send -u low "$(omarchy-text "Editing config file")" "$path"
|
||||
omarchy-notification-send -u low "Editing config file" "$path"
|
||||
exec omarchy-launch-editor "$path"
|
||||
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
|
||||
case $terminal in
|
||||
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
|
||||
*)
|
||||
omarchy-notification-send -g ✋ "$(omarchy-text "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty")"
|
||||
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
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)
|
||||
@@ -332,7 +354,6 @@ dynamic_bindings() {
|
||||
static_bindings() {
|
||||
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
|
||||
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
|
||||
echo "CTRL SHIFT,SPACE,Switch input language,exec,fcitx5-remote -t"
|
||||
}
|
||||
|
||||
# Actions Omarchy binds to a second chord meant as an alternative, named one at
|
||||
@@ -536,6 +557,7 @@ keybindings_cache_key() {
|
||||
{
|
||||
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 }'
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
|
||||
". as \$plugins
|
||||
| .[] | select($filter)
|
||||
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
||||
[[ -n $rows ]] || { omarchy-notification-send "$(omarchy-text "No plugin to $1")"; exit 0; }
|
||||
[[ -n $rows ]] || { omarchy-notification-send "No plugin to ${1}"; exit 0; }
|
||||
|
||||
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
|
||||
[[ -n $selection ]] || exit 0
|
||||
|
||||
+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
|
||||
|
||||
@@ -33,7 +33,7 @@ else
|
||||
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
||||
|
||||
if ((${status:-0} > 1)); then
|
||||
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not share")" "$(omarchy-text "The file chooser did not open")"
|
||||
omarchy-notification-send -g "" -u critical "Could not share" "The file chooser did not open"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@ set -e
|
||||
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
||||
sudo timedatectl set-timezone "$timezone"
|
||||
omarchy-shell -q omarchy.clock refresh
|
||||
omarchy-notification-send "$(omarchy-text "Timezone is now set to %1" "$timezone")"
|
||||
omarchy-notification-send "Timezone is now set to $timezone"
|
||||
@@ -21,9 +21,9 @@ pending_migrations=$(omarchy-migrate --pending 2>/dev/null) || exit 0
|
||||
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
|
||||
|
||||
if (( pending_count == 1 )); then
|
||||
message=$(omarchy-text "Click to run 1 pending migration.")
|
||||
message="Click to run 1 pending migration."
|
||||
else
|
||||
message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
|
||||
message="Click to run $pending_count pending migrations."
|
||||
fi
|
||||
|
||||
# This runs from omarchy-migrate-notify.service after graphical-session.target,
|
||||
@@ -40,7 +40,7 @@ fi
|
||||
|
||||
# The shell keeps the click command with the toast, so this oneshot can hand the
|
||||
# invitation over and exit instead of staying activated until it is answered.
|
||||
omarchy-notification-send -u critical -g "$(omarchy-text "Pending Omarchy Migrations")" "$message" \
|
||||
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
||||
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
||||
|
||||
# Reached when the notification could not be handed off, so fall back to telling
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
# omarchy:summary=Show the current time and date notification
|
||||
|
||||
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y") · $(omarchy-text "Week %1" "$(date +%V)")"
|
||||
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y · Week %V")"
|
||||
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
|
||||
@@ -159,8 +159,8 @@ done
|
||||
omarchy-plugin-enable "$new_id" >/dev/null
|
||||
clone_complete=1
|
||||
omarchy-notification-send -g \
|
||||
"$(omarchy-text "Editing Cloned Plugin")" \
|
||||
"$(omarchy-text "Original plugin has been replace by clone.")"
|
||||
"Editing Cloned Plugin" \
|
||||
"Original plugin has been replace by clone."
|
||||
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
||||
if (( edit_clone )); then
|
||||
# Word-splitting is deliberate: EDITOR may carry flags.
|
||||
|
||||
@@ -75,6 +75,10 @@ run_first_run_step "install agent setup post-update hook" \
|
||||
|
||||
run_first_run_step "enable user systemd units" \
|
||||
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
|
||||
# Finalization ran it too, maybe without a session; the platform finishes what
|
||||
# needs one here.
|
||||
run_first_run_step "run the platform's user setup" \
|
||||
omarchy-lifecycle-dispatch setup-user
|
||||
run_first_run_step "set GNOME theme" \
|
||||
bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh"
|
||||
run_first_run_step "set GTK primary paste" \
|
||||
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Finish the hardware setup an image build deferred to this machine
|
||||
# omarchy:requires-sudo=true
|
||||
# omarchy:hidden=true
|
||||
|
||||
# Started by omarchy-provision-hardware.service on every boot while an image's
|
||||
# deferred steps remain (see install/helpers/image-target.sh), and safe to call
|
||||
# from a platform's own first boot as well: runs are serialised. The first run
|
||||
# retires the image's build manifest, so from then on this machine is itself.
|
||||
# Steps run in order and each one that succeeds leaves the queue, so a failure
|
||||
# stops the run and the next run resumes at that step. Once the queue is empty
|
||||
# the initramfs is rebuilt if any step changed its configuration or asked for
|
||||
# the rebuild, and the service is disabled. With nothing deferred this does
|
||||
# nothing. A failed step, or a failure to make the pacman keyring the build
|
||||
# asked for, exits 75 (it stays queued for the next run); anything else that
|
||||
# stops the run, such as an untrusted manifest or queue, exits 1.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
export OMARCHY_PATH=/usr/share/omarchy
|
||||
export PATH=/usr/share/omarchy/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
|
||||
elif [[ -z ${OMARCHY_IMAGE_ROOT:-} || -z ${OMARCHY_PATH:-} ]]; then
|
||||
fail "omarchy-provision-hardware must run as root"
|
||||
fi
|
||||
export OMARCHY_INSTALL=$OMARCHY_PATH/install
|
||||
export OMARCHY_INSTALL_USER=""
|
||||
|
||||
source "$OMARCHY_INSTALL/helpers/logging.sh"
|
||||
source "$OMARCHY_INSTALL/helpers/image-target.sh"
|
||||
omarchy_image_init
|
||||
export OMARCHY_INSTALL_LOG_FILE=$omarchy_image_root/var/log/omarchy-install.log
|
||||
|
||||
queue_present() {
|
||||
[[ -e $omarchy_image_queue || -L $omarchy_image_queue ]]
|
||||
}
|
||||
|
||||
# Anything left for the first boot: the manifest, a queue, or the keyring an
|
||||
# image build asked for, which is owed even when no step was queued.
|
||||
work_present() {
|
||||
omarchy_image_manifest_present || queue_present || [[ -e $omarchy_image_keyring_request ]]
|
||||
}
|
||||
|
||||
work_present || exit 0
|
||||
omarchy_image_lock || exit 1
|
||||
# Another run may have finished while this one waited for the lock.
|
||||
work_present || exit 0
|
||||
|
||||
if omarchy_image_manifest_present; then
|
||||
omarchy_image_read_manifest || exit 1
|
||||
mv -f "$omarchy_image_manifest" "$omarchy_image_manifest.booted" || fail "could not retire $omarchy_image_manifest"
|
||||
live_platform=$(omarchy-hw-platform) || live_platform=unknown
|
||||
echo "First boot of an image built for $omarchy_image_platform, on $live_platform hardware"
|
||||
fi
|
||||
|
||||
remaining=()
|
||||
if queue_present; then
|
||||
[[ -f $omarchy_image_queue ]] && omarchy_image_trusted "$omarchy_image_queue" ||
|
||||
fail "$omarchy_image_queue is not a root-owned regular file"
|
||||
mapfile -t queued <"$omarchy_image_queue"
|
||||
for step in "${queued[@]}"; do
|
||||
[[ -z $step ]] || remaining+=("$step")
|
||||
done
|
||||
fi
|
||||
|
||||
# What the boot image is built from: mkinitcpio's configuration, the module
|
||||
# options its modconf hook copies in (leaves such as nvidia.sh write them), and
|
||||
# the kernel command line Limine's entry tool adds (leaves such as intel/fred.sh
|
||||
# add to it).
|
||||
initramfs_inputs() {
|
||||
local file
|
||||
|
||||
for file in "$omarchy_image_root/etc/mkinitcpio.conf" "$omarchy_image_root/etc/mkinitcpio.conf.d/"* \
|
||||
"$omarchy_image_root/etc/modprobe.d/"* "$omarchy_image_root/usr/lib/modprobe.d/"* \
|
||||
"$omarchy_image_root/etc/default/limine" "$omarchy_image_root/etc/limine-entry-tool.d/"*; do
|
||||
[[ -e $file ]] || continue
|
||||
printf '%s\0' "$file"
|
||||
cat -- "$file"
|
||||
done | cksum
|
||||
}
|
||||
|
||||
# Recorded before the first step and kept until a rebuild lands, so a step that
|
||||
# changed the initramfs and then failed or lost power still gets one.
|
||||
if [[ ! -f $omarchy_image_initramfs_baseline ]]; then
|
||||
baseline=$(mktemp "$omarchy_image_dir/.initramfs-inputs.XXXXXX") &&
|
||||
initramfs_inputs >"$baseline" && chmod 0644 "$baseline" && mv -f "$baseline" "$omarchy_image_initramfs_baseline" ||
|
||||
fail "could not record the initramfs inputs"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OMARCHY_INSTALL_LOG_FILE")"
|
||||
|
||||
# A step that would build the boot image itself (a platform's boot setup) asks
|
||||
# for this run's rebuild instead, so the image is built once, after the last
|
||||
# step. The request stays on disk until that rebuild lands.
|
||||
export OMARCHY_IMAGE_BOOT_REBUILD=$omarchy_image_boot_rebuild
|
||||
# Tells a step it runs on the booted machine, after boot already brought up the
|
||||
# targets its units hang off (see install/hardware/bluetooth.sh).
|
||||
export OMARCHY_IMAGE_DEFERRED_HARDWARE=1
|
||||
|
||||
# An image build asks for this machine's own pacman keyring (see
|
||||
# install/post-install/pacman.sh), made before any step installs a package. Any
|
||||
# keyring the build or an interrupted run left is replaced; the request stays
|
||||
# until a keyring is complete.
|
||||
if [[ -e $omarchy_image_keyring_request ]]; then
|
||||
echo "Creating this machine's pacman keyring"
|
||||
keyring=$omarchy_image_root/etc/pacman.d/gnupg
|
||||
if ! { rm -rf "$keyring" && pacman-key --gpgdir "$keyring" --init &&
|
||||
pacman-key --gpgdir "$keyring" --populate; } </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1; then
|
||||
echo "Error: could not create the pacman keyring (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it" >&2
|
||||
exit 75
|
||||
fi
|
||||
rm -f "$omarchy_image_keyring_request"
|
||||
fi
|
||||
|
||||
while (( ${#remaining[@]} )); do
|
||||
step=${remaining[0]}
|
||||
[[ $step == install/hardware/*.sh && $step != *..* ]] || fail "Deferred step is not a hardware setup step: $step"
|
||||
script=$OMARCHY_PATH/$step
|
||||
|
||||
if [[ -L $script ]]; then
|
||||
fail "Deferred step is a symlink: $step"
|
||||
elif [[ ! -e $script ]]; then
|
||||
echo "Skipping deferred hardware step $step: this Omarchy no longer ships it"
|
||||
else
|
||||
echo "Running deferred hardware step $step"
|
||||
if ! run_logged "$script"; then
|
||||
echo "Error: Deferred hardware step failed: $step (see $OMARCHY_INSTALL_LOG_FILE); the next run resumes here" >&2
|
||||
exit 75
|
||||
fi
|
||||
fi
|
||||
|
||||
remaining=("${remaining[@]:1}")
|
||||
omarchy_image_write_queue "${remaining[@]}" || fail "could not update $omarchy_image_queue"
|
||||
done
|
||||
|
||||
if [[ -e $omarchy_image_boot_rebuild || $(initramfs_inputs) != "$(<"$omarchy_image_initramfs_baseline")" ]]; then
|
||||
echo "Rebuilding the initramfs for the deferred hardware setup"
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
limine-mkinitcpio </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
|
||||
else
|
||||
mkinitcpio -P </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
|
||||
fi || fail "Initramfs rebuild failed (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it"
|
||||
# The new image must be on disk before the queue that owes it is cleared.
|
||||
sync || fail "could not flush the rebuilt initramfs to disk"
|
||||
fi
|
||||
rm -f "$omarchy_image_initramfs_baseline" "$omarchy_image_boot_rebuild"
|
||||
|
||||
# Disabled, not deleted: a platform first boot that runs this while the
|
||||
# service's start job waits would otherwise fail that job at the next
|
||||
# daemon-reload. The emptied queue keeps the unit from running.
|
||||
rm -f "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit"
|
||||
rm -f "$omarchy_image_queue"
|
||||
if (( EUID == 0 )); then
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
fi
|
||||
echo "Deferred hardware setup is complete"
|
||||
+263
-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,6 @@ keyboard_form() {
|
||||
confirm_reboot && exec systemctl reboot
|
||||
done
|
||||
|
||||
apply_keyboard "$keyboard"
|
||||
omarchy_persist_input_selection
|
||||
}
|
||||
|
||||
@@ -610,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
|
||||
}
|
||||
|
||||
@@ -715,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
|
||||
}
|
||||
@@ -823,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() {
|
||||
@@ -852,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
|
||||
@@ -870,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
|
||||
@@ -991,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
|
||||
@@ -1018,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
|
||||
@@ -1035,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
|
||||
@@ -1052,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"
|
||||
|
||||
+15
-17
@@ -24,31 +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
|
||||
|
||||
# Render the channel templates plus this machine's regional additions as the
|
||||
# caller, so root only copies finished files. The scratch directory goes on
|
||||
# every exit, including a failed render, a refused sudo prompt, or a signal;
|
||||
# revoking the sudo timestamp stays the last thing the exit path does.
|
||||
rendered=$(mktemp -d)
|
||||
cleanup_refresh() {
|
||||
local status=$?
|
||||
rm -rf "$rendered"
|
||||
omarchy_security_exit_with_revoked_sudo "$status"
|
||||
}
|
||||
trap cleanup_refresh EXIT
|
||||
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
|
||||
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 "$rendered/pacman.conf" /etc/pacman.conf
|
||||
sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
|
||||
rm -rf "$rendered"
|
||||
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,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Show or switch the region this machine is set up for
|
||||
# omarchy:args=[global|cn]
|
||||
# omarchy:examples=omarchy region | omarchy region cn
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
region_file=/etc/omarchy/region
|
||||
|
||||
if (($# == 0)); then
|
||||
if [[ ! -e $region_file ]]; then
|
||||
echo "Global"
|
||||
elif [[ $(<"$region_file") == "cn" ]]; then
|
||||
echo "China"
|
||||
else
|
||||
echo "Unknown region in $region_file: $(<"$region_file")" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $1 != "global" && $1 != "cn" ]]; then
|
||||
echo "Usage: omarchy-region [global|cn]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
channel=$(omarchy-version-channel)
|
||||
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $1 == "cn" ]]; then
|
||||
# Upgrade first: the keyring install below syncs new databases.
|
||||
omarchy-update-pacman -Syu --noconfirm
|
||||
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
|
||||
# Trust ArchLinuxCN before the refresh upgrades through it.
|
||||
rendered=$(mktemp -d)
|
||||
trap 'rm -rf "$rendered"' EXIT
|
||||
omarchy-apply-pacman --render "$channel" "$rendered"
|
||||
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
|
||||
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
|
||||
sudo pacman-key --populate archlinuxcn
|
||||
omarchy-refresh-pacman "$channel"
|
||||
else
|
||||
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
|
||||
omarchy-refresh-pacman "$channel"
|
||||
fi
|
||||
|
||||
# The developer mirrors reach only a new session, so offer one.
|
||||
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")" 2>&1; then
|
||||
omarchy-system-logout
|
||||
else
|
||||
omarchy-text "Log back in to switch the developer mirrors."
|
||||
fi
|
||||
@@ -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[@]}"
|
||||
+14
-15
@@ -57,16 +57,16 @@ show_reminders() {
|
||||
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
||||
|
||||
if [[ -n $reminder_message ]]; then
|
||||
body+="$(omarchy-text "%1 in %2 (%3)" "$reminder_message" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||
body+="$reminder_message in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
||||
else
|
||||
body+="$(omarchy-text "%1-min reminder in %2 (%3)" "$reminder_minutes" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||
body+="${reminder_minutes}-min reminder in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
||||
fi
|
||||
done < <(active_reminder_timers "$now")
|
||||
|
||||
if [[ -z $body ]]; then
|
||||
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "$(omarchy-text "No outstanding reminders")"
|
||||
omarchy-notification-send -g "Upcoming reminders" "No outstanding reminders"
|
||||
else
|
||||
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
|
||||
omarchy-notification-send -g "Upcoming reminders" "${body%$'\n'}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -75,8 +75,7 @@ show_json() {
|
||||
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||
local now=$(date +%s)
|
||||
local count=0
|
||||
local tooltip
|
||||
tooltip=$(omarchy-text "Set Reminder")
|
||||
local tooltip="Set Reminder"
|
||||
|
||||
while IFS=$'\t' read -r timer next; do
|
||||
count=$((count + 1))
|
||||
@@ -92,7 +91,7 @@ show_json() {
|
||||
if [[ -n $reminder_message ]]; then
|
||||
label=$reminder_message
|
||||
else
|
||||
label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
|
||||
label="${reminder_minutes}-min reminder"
|
||||
fi
|
||||
|
||||
item_json=$(jq -cn \
|
||||
@@ -110,9 +109,9 @@ show_json() {
|
||||
done < <(active_reminder_timers "$now")
|
||||
|
||||
if ((count == 1)); then
|
||||
tooltip=$(omarchy-text "1 reminder")
|
||||
tooltip="1 reminder"
|
||||
elif ((count > 1)); then
|
||||
tooltip=$(omarchy-text "%1 reminders" "$count")
|
||||
tooltip="$count reminders"
|
||||
fi
|
||||
|
||||
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
|
||||
@@ -130,7 +129,7 @@ clear_reminders() {
|
||||
|
||||
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
|
||||
refresh_indicator
|
||||
omarchy-notification-send -g "$(omarchy-text "All reminders have been cleared")"
|
||||
omarchy-notification-send -g "All reminders have been cleared"
|
||||
}
|
||||
|
||||
usage() {
|
||||
@@ -177,7 +176,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
|
||||
fi
|
||||
|
||||
if [[ -z $message ]]; then
|
||||
message=$(omarchy-text "Your %1 minutes are up" "$minutes")
|
||||
message="Your ${minutes} minutes are up"
|
||||
fi
|
||||
|
||||
set_at=$(date +%s)
|
||||
@@ -185,18 +184,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
|
||||
unit="omarchy-reminder-${minutes}m-$set_at"
|
||||
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||
message_file="$reminder_dir/$unit.message"
|
||||
confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
|
||||
confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
|
||||
confirmation="You'll be reminded at $remind_at"
|
||||
confirmation_title="Reminder set for ${minutes} minutes"
|
||||
|
||||
mkdir -p "$reminder_dir"
|
||||
|
||||
if [[ -n $custom_message ]]; then
|
||||
printf "%s" "$custom_message" >"$message_file"
|
||||
confirmation_title=$(omarchy-text "%1 in %2 minutes" "$custom_message" "$minutes")
|
||||
confirmation_title="$custom_message in ${minutes} minutes"
|
||||
fi
|
||||
|
||||
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
|
||||
bash -c 'omarchy-notification-send -g "$(omarchy-text "Reminder")" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file"
|
||||
bash -c 'omarchy-notification-send -g "Reminder" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file"
|
||||
|
||||
refresh_indicator
|
||||
omarchy-notification-send -g "$confirmation_title" "$confirmation"
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set up Japanese, Korean, or Chinese typing
|
||||
# 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
|
||||
@@ -14,9 +14,8 @@ fi
|
||||
|
||||
method=${1:-}
|
||||
if [[ -z $method ]]; then
|
||||
choice=$(jq -r 'to_entries[] | select(.key != "none") | .value.label' "$OMARCHY_PATH/default/input-methods/presets.json" |
|
||||
gum choose --header "Select input language")
|
||||
method=$(jq -r --arg label "$choice" 'to_entries[] | select(.value.label == $label) | .key' "$OMARCHY_PATH/default/input-methods/presets.json")
|
||||
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" input
|
||||
exit
|
||||
fi
|
||||
|
||||
case "$method" in
|
||||
@@ -26,5 +25,5 @@ esac
|
||||
|
||||
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
|
||||
omarchy-pkg-add "${packages[@]}"
|
||||
omarchy-input-method configure "$method"
|
||||
echo "Input method added. The default switch is Ctrl+Shift+Space or the Japanese/Korean input key; existing custom keys are preserved."
|
||||
/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
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
||||
|
||||
omarchy-osd -i logout -m "$(omarchy-text "Logging out")" -d 5000
|
||||
omarchy-osd -i logout -m "Logging out" -d 5000
|
||||
|
||||
# Now close all windows
|
||||
omarchy-hyprland-window-close-all
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# scope cannot terminate it before it runs.
|
||||
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
|
||||
|
||||
omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
|
||||
omarchy-osd -i reboot -m "Rebooting" -d 5000
|
||||
|
||||
omarchy-state clear re*-required
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
if [[ ${1:-} == "--inhibited" ]]; then
|
||||
# Close windows in the background so they cannot delay power-off.
|
||||
(
|
||||
omarchy-osd -i shutdown -m "$(omarchy-text "Shutting down")" -d 5000
|
||||
omarchy-osd -i shutdown -m "Shutting down" -d 5000
|
||||
omarchy-state clear re*-required
|
||||
omarchy-hyprland-window-close-all
|
||||
) &
|
||||
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
|
||||
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
||||
"$0" --inhibited || {
|
||||
status=$?
|
||||
omarchy-notification-send -u critical "$(omarchy-text "Shutdown failed")" "$(omarchy-text "Could not complete shutdown. Please try again.")"
|
||||
omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again."
|
||||
exit "$status"
|
||||
}
|
||||
else
|
||||
|
||||
@@ -98,8 +98,8 @@ report_unsecured() {
|
||||
"$1" >&2
|
||||
|
||||
omarchy-notification-send -u critical -g \
|
||||
"$(omarchy-text "Screen did not lock before suspend")" \
|
||||
"$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
|
||||
"Screen did not lock before suspend" \
|
||||
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ claim_path() {
|
||||
announce() {
|
||||
local path="$1"
|
||||
local name="${path##*/}"
|
||||
local args=("$(omarchy-text "Received %1" "$name")" "$(omarchy-text "Saved to %1" "${dir/#$HOME/~}")" -u critical)
|
||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical)
|
||||
|
||||
case "${name,,}" in
|
||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||
|
||||
@@ -27,8 +27,8 @@ if ((${#files[@]} == 0)); then
|
||||
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
||||
|
||||
if ((${status:-0} > 1)); then
|
||||
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
|
||||
"$(omarchy-text "The file chooser did not open")"
|
||||
omarchy-notification-send -g "" -u critical "Could not send to $name" \
|
||||
"The file chooser did not open"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -43,8 +43,8 @@ else
|
||||
fi
|
||||
|
||||
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
||||
omarchy-notification-send -g "" "$(omarchy-text "Sent to %1" "$name")" "$what"
|
||||
omarchy-notification-send -g "" "Sent to $name" "$what"
|
||||
else
|
||||
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" "${error:-$(omarchy-text "Taildrop transfer failed")}"
|
||||
omarchy-notification-send -g "" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Print an Omarchy message in the session's language, with %1.. filled from the arguments
|
||||
# omarchy:args=<english> [args...]
|
||||
# omarchy:examples=omarchy-text "Sent to %1" phone
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if (($# < 1)); then
|
||||
echo "Usage: omarchy-text <english> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
text=$1
|
||||
shift
|
||||
|
||||
# The shell's table is the one source: one JSON-encoded "english": "chinese" pair per line.
|
||||
if [[ ${LANG:-} == zh_CN* ]]; then
|
||||
translated=$(grep -E '^ ".*": ".*",?$' "$OMARCHY_PATH/shell/Commons/I18nZh.js" | sed 's/,$//' | paste -sd, |
|
||||
sed 's/^/{/; s/$/}/' | jq -r --arg key "$text" '.[$key] // empty')
|
||||
[[ -z $translated ]] || text=$translated
|
||||
fi
|
||||
|
||||
# One pass over the template, so an argument that holds %2 is printed as given.
|
||||
out=""
|
||||
rest=$text
|
||||
while [[ $rest =~ ^([^%]*)%(.)(.*)$ ]]; do
|
||||
out+=${BASH_REMATCH[1]}
|
||||
mark=${BASH_REMATCH[2]}
|
||||
rest=${BASH_REMATCH[3]}
|
||||
if [[ $mark == [1-9] ]] && ((mark <= $#)); then
|
||||
out+=${!mark}
|
||||
else
|
||||
out+="%$mark"
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "$out$rest"
|
||||
@@ -10,12 +10,12 @@ toggle="$HOME/.local/state/omarchy/toggles/background-intros-off"
|
||||
case ${1:-} in
|
||||
enable)
|
||||
rm -f "$toggle"
|
||||
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Enabled")"
|
||||
omarchy-notification-send "Background Intros" "Enabled"
|
||||
;;
|
||||
disable)
|
||||
mkdir -p "$(dirname "$toggle")"
|
||||
touch "$toggle"
|
||||
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Disabled")"
|
||||
omarchy-notification-send "Background Intros" "Disabled"
|
||||
;;
|
||||
toggle)
|
||||
if [[ -f $toggle ]]; then
|
||||
|
||||
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
|
||||
TOTAL=${#BACKGROUNDS[@]}
|
||||
|
||||
if (( TOTAL == 0 )); then
|
||||
omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
|
||||
omarchy-notification-send "No background was found for theme" -t 2000
|
||||
else
|
||||
# Get current background from symlink
|
||||
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
||||
|
||||
@@ -35,5 +35,5 @@ fi
|
||||
|
||||
# Now remove the theme directory for THEME_NAME
|
||||
rm -rf "$THEME_PATH"
|
||||
omarchy-notification-send "$(omarchy-text "Theme removed")" "$THEME_NAME" || true
|
||||
omarchy-notification-send "Theme removed" "$THEME_NAME" || true
|
||||
echo "Removed $THEME_NAME"
|
||||
Loaded 100 of 436 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user