Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75d9ec05e9
|
||
|
|
45a27629de
|
||
|
|
d6bea2aeba
|
||
|
|
70fb355250
|
||
|
|
c9494b27ec
|
||
|
|
a77ea36fe5
|
||
|
|
e9a3f13b9a
|
||
|
|
e2dab6ef14
|
||
|
|
7dbaa4914f
|
||
|
|
1dbe97d9b7
|
||
|
|
84450ffc19
|
||
|
|
b4de1c0d34
|
||
|
|
c90afe24dc
|
||
|
|
f5ab724a1c
|
||
|
|
096982fe69
|
||
|
|
96e2effbe1
|
||
|
|
846f7d491f
|
||
|
|
a9b39710a2
|
||
|
|
defbf90e89 | ||
|
|
16faf06fcf
|
||
|
|
466c2ca14a
|
||
|
|
ec4579a461 | ||
|
|
d9a781e3c9 | ||
|
|
a6c3480ff8 | ||
|
|
545190596d | ||
|
|
b381093e32 | ||
|
|
9d74c813f1 | ||
|
|
fce85abf48 | ||
|
|
3c68a1bb04 | ||
|
|
b89def6c7f | ||
|
|
0de49b231a | ||
|
|
3e00a48c00 | ||
|
|
a1bdc46119 | ||
|
|
7a60c8dea4 | ||
|
|
6be88d14a5 | ||
|
|
50a26be6a7 | ||
|
|
d5ef1c1d15 | ||
|
|
d024c82a9d | ||
|
|
d7f17a0424 | ||
|
|
87a2931209 | ||
|
|
647979bde7 | ||
|
|
123535ea14 | ||
|
|
645cfc2a36 | ||
|
|
e989960895 | ||
|
|
48f7c411c0 | ||
|
|
7727d40c92 |
No files matched your search
@@ -124,6 +124,19 @@ Exceptions are allowed for migration and package-helper scripts where the helper
|
|||||||
- Do not add `aliases` to new menu entries. Aliases are reserved for
|
- Do not add `aliases` to new menu entries. Aliases are reserved for
|
||||||
established alternate names users already type, kept for compatibility.
|
established alternate names users already type, kept for compatibility.
|
||||||
|
|
||||||
|
# Language
|
||||||
|
|
||||||
|
- `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`; the installer sets it from its first screen. Nothing in `omarchy update` or migrations may change `/etc/locale.conf`.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
# Region
|
||||||
|
|
||||||
|
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
|
||||||
|
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. It serves both the install (the marker) and `omarchy region cn|global` (`OMARCHY_REGION`), which also records or removes the marker; Global removes only the links it made.
|
||||||
|
- `omarchy-region` prints the region (`Global` or `China`) and switches it. Nothing in `omarchy update` or migrations may change the region.
|
||||||
|
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. User-facing docs: `manual/52-china.md`.
|
||||||
|
|
||||||
# Config Structure
|
# Config Structure
|
||||||
|
|
||||||
- `config/` - default configs copied to `~/.config/`
|
- `config/` - default configs copied to `~/.config/`
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ The manual lives in [`manual/`](manual/), which is its authoritative source.
|
|||||||
- [Omarchy on...](manual/49-omarchy-on.md)
|
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||||
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||||
- [Unattended Installs](manual/51-unattended-installs.md)
|
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||||
|
- [China](manual/52-china.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
|||||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
`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
|
## IPC
|
||||||
|
|
||||||
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
|
|||||||
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
||||||
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
||||||
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
||||||
|
GROUP_DESCRIPTIONS[language]="System language"
|
||||||
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
||||||
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
||||||
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
||||||
@@ -74,6 +75,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
|
|||||||
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
||||||
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
||||||
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
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[reinstall]="Reinstall and reset workflows"
|
||||||
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
||||||
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ finish() {
|
|||||||
fail() {
|
fail() {
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
printf '@@omarchy error %s\n' "$1"
|
printf '@@omarchy error %s\n' "$1"
|
||||||
omarchy-notification-send -g -u critical "Couldn't add the $name account" "$1"
|
omarchy-notification-send -g -u critical "$(omarchy-text "Couldn't add the %1 account" "$name")" "$1"
|
||||||
else
|
else
|
||||||
echo "$1" >&2
|
echo "$1" >&2
|
||||||
fi
|
fi
|
||||||
@@ -113,9 +113,9 @@ interruptible() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if omarchy-cmd-missing "$provider"; then
|
if omarchy-cmd-missing "$provider"; then
|
||||||
say "Installing the $name CLI…"
|
say "$(omarchy-text "Installing the %1 CLI…" "$name")"
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
mise use -g "$package" >/dev/null 2>&1 || fail "Installing the $name CLI didn't finish."
|
mise use -g "$package" >/dev/null 2>&1 || fail "$(omarchy-text "Installing the %1 CLI didn't finish." "$name")"
|
||||||
else
|
else
|
||||||
mise use -g "$package" || exit 1
|
mise use -g "$package" || exit 1
|
||||||
fi
|
fi
|
||||||
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
|
|||||||
else
|
else
|
||||||
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
||||||
fi
|
fi
|
||||||
[[ -n $account_home ]] || fail "No $name account named $reauth."
|
[[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")"
|
||||||
|
|
||||||
if [[ $account_home == "$HOME/.$provider" ]]; then
|
if [[ $account_home == "$HOME/.$provider" ]]; then
|
||||||
say "Sign in to $name again in the browser window that opens."
|
say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
|
||||||
login || fail "The $name sign-in didn't finish."
|
login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
else
|
else
|
||||||
say "Sign in again as this account in the private window that opens."
|
say "$(omarchy-text "Sign in again as this account in the private window that opens.")"
|
||||||
private_browser
|
private_browser
|
||||||
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish."
|
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
finish "Signed in to $name again."
|
finish "$(omarchy-text "Signed in to %1 again." "$name")"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The first subscription signs in where the CLI always looks, through your
|
# 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.
|
# normal browser: nothing is signed in there yet that it could be mistaken for.
|
||||||
if ! primary_signed_in "$provider"; then
|
if ! primary_signed_in "$provider"; then
|
||||||
say "Sign in to $name in the browser window that opens."
|
say "$(omarchy-text "Sign in to %1 in the browser window that opens." "$name")"
|
||||||
if login && primary_signed_in "$provider"; then
|
if login && primary_signed_in "$provider"; then
|
||||||
# The first agent signed in on a machine with none chosen becomes the
|
# 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
|
# 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"
|
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
|
||||||
fi
|
fi
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
finish "Signed in to $name."
|
finish "$(omarchy-text "Signed in to %1." "$name")"
|
||||||
else
|
else
|
||||||
fail "The $name sign-in didn't finish."
|
fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
||||||
@@ -189,7 +189,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
say "Sign in as the account you're adding in the private window that opens."
|
say "$(omarchy-text "Sign in as the account you're adding in the private window that opens.")"
|
||||||
else
|
else
|
||||||
cat <<EOF
|
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
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account."
|
home=$(omarchy-agent-account-state begin "$provider") || fail "$(omarchy-text "Couldn't set up a home for the new account.")"
|
||||||
|
|
||||||
private_browser
|
private_browser
|
||||||
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
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")
|
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
finish "Added $added."
|
finish "$(omarchy-text "Added %1." "$added")"
|
||||||
else
|
else
|
||||||
finish "Added $added."
|
finish "$(omarchy-text "Added %1." "$added")"
|
||||||
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ class AccountError(Exception):
|
|||||||
pass
|
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
|
# ------------------------------------------------------------------- paths
|
||||||
|
|
||||||
|
|
||||||
@@ -335,12 +340,12 @@ def begin(provider: str) -> Path:
|
|||||||
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
||||||
pending = pending.resolve()
|
pending = pending.resolve()
|
||||||
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
||||||
raise AccountError(f"{pending} is not a pending {PROVIDERS[provider]['name']} login")
|
raise AccountError(text("%1 is not a pending %2 login", pending, PROVIDERS[provider]['name']))
|
||||||
|
|
||||||
found = identity(provider, pending)
|
found = identity(provider, pending)
|
||||||
if not found["accountId"]:
|
if not found["accountId"]:
|
||||||
shutil.rmtree(pending, ignore_errors=True)
|
shutil.rmtree(pending, ignore_errors=True)
|
||||||
raise AccountError("The login didn't finish, so no account was added.")
|
raise AccountError(text("The login didn't finish, so no account was added."))
|
||||||
|
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
# Each home is asked who it is now, not trusted from when it was added: a
|
# Each home is asked who it is now, not trusted from when it was added: a
|
||||||
@@ -350,10 +355,10 @@ def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
|||||||
for account in registry["accounts"]:
|
for account in registry["accounts"]:
|
||||||
if account.get("accountId") == found["accountId"]:
|
if account.get("accountId") == found["accountId"]:
|
||||||
shutil.rmtree(pending, ignore_errors=True)
|
shutil.rmtree(pending, ignore_errors=True)
|
||||||
raise AccountError(
|
raise AccountError(text(
|
||||||
f"That's {account['label']} ({found['email'] or 'already added'}). "
|
"That's %1 (%2). Try again, and sign in as the other account in the private window.",
|
||||||
"Try again, and sign in as the other account in the private window."
|
account["label"], found["email"] or text("already added"),
|
||||||
)
|
))
|
||||||
|
|
||||||
label = label.strip() or found["email"].split("@")[0] or "Account"
|
label = label.strip() or found["email"].split("@")[0] or "Account"
|
||||||
account_id = unique_id(provider, registry, label)
|
account_id = unique_id(provider, registry, label)
|
||||||
@@ -395,11 +400,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]:
|
def rename(provider: str, target: str, label: str) -> dict[str, Any]:
|
||||||
label = label.strip()
|
label = label.strip()
|
||||||
if not label:
|
if not label:
|
||||||
raise AccountError("Give the account a name")
|
raise AccountError(text("Give the account a name"))
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
new_id = unique_id(provider, registry, label, keep=account)
|
new_id = unique_id(provider, registry, label, keep=account)
|
||||||
if not account.get("primary") and not account.get("home"):
|
if not account.get("primary") and not account.get("home"):
|
||||||
account["home"] = str(account_home(provider, account))
|
account["home"] = str(account_home(provider, account))
|
||||||
@@ -428,7 +433,7 @@ def use(provider: str, target: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|||||||
target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
|
target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
registry["active"] = account["id"]
|
registry["active"] = account["id"]
|
||||||
registry["alert"] = ""
|
registry["alert"] = ""
|
||||||
if not account.get("primary"):
|
if not account.get("primary"):
|
||||||
@@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
|||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
if account.get("primary"):
|
if account.get("primary"):
|
||||||
raise AccountError(f"{account['label']} is the login in {primary_home(provider)}; it can't be removed here.")
|
raise AccountError(text("%1 is the login in %2; it can't be removed here.", account["label"], primary_home(provider)))
|
||||||
home = account_home(provider, account)
|
home = account_home(provider, account)
|
||||||
if home_in_use(provider, home):
|
if home_in_use(provider, home):
|
||||||
raise AccountError(f"A running {PROVIDERS[provider]['name']} session uses {account['label']}. Quit it first, then remove the account.")
|
raise AccountError(text("A running %1 session uses %2. Quit it first, then remove the account.", PROVIDERS[provider]['name'], account["label"]))
|
||||||
# Only the account's own directory goes. Its links point into the primary
|
# Only the account's own directory goes. Its links point into the primary
|
||||||
# home, and rmtree removes a link without following it.
|
# home, and rmtree removes a link without following it.
|
||||||
if home.resolve().parent == (accounts_root() / provider).resolve():
|
if home.resolve().parent == (accounts_root() / provider).resolve():
|
||||||
@@ -474,14 +479,14 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
|
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
|
||||||
if mode not in SWITCH_MODES:
|
if mode not in SWITCH_MODES:
|
||||||
raise AccountError("Switch mode is manual or auto")
|
raise AccountError(text("Switch mode is manual or auto"))
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
registry["switch"] = mode
|
registry["switch"] = mode
|
||||||
if threshold is not None:
|
if threshold is not None:
|
||||||
try:
|
try:
|
||||||
registry["threshold"] = max(50, min(100, int(threshold)))
|
registry["threshold"] = max(50, min(100, int(threshold)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise AccountError("The threshold is a percentage between 50 and 100")
|
raise AccountError(text("The threshold is a percentage between 50 and 100"))
|
||||||
registry["alert"] = ""
|
registry["alert"] = ""
|
||||||
return dict(registry)
|
return dict(registry)
|
||||||
|
|
||||||
@@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str:
|
|||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
days, hours = divmod(hours, 24)
|
days, hours = divmod(hours, 24)
|
||||||
if days:
|
if days:
|
||||||
return f"{days}d {hours}h"
|
return text("%1d %2h", days, hours)
|
||||||
if hours:
|
if hours:
|
||||||
return f"{hours}h {minutes}m"
|
return text("%1h %2m", hours, minutes)
|
||||||
return f"{minutes}m"
|
return text("%1m", minutes)
|
||||||
|
|
||||||
|
|
||||||
# The whole switching policy, with no I/O: given the registry and the per
|
# The whole switching policy, with no I/O: given the registry and the per
|
||||||
@@ -659,21 +664,23 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool:
|
|||||||
threshold = registry["threshold"]
|
threshold = registry["threshold"]
|
||||||
if decision["action"] == "exhausted":
|
if decision["action"] == "exhausted":
|
||||||
soonest = decision["soonest"]
|
soonest = decision["soonest"]
|
||||||
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else ""
|
staying = text("Staying on %1.", source["label"])
|
||||||
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}")
|
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)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target = decision["to"]
|
target = decision["to"]
|
||||||
headline = f"{source['label']} is at {round(decision['fromPeak'])}% of a {name} limit"
|
headline = text("%1 is at %2% of a %3 limit", source["label"], round(decision["fromPeak"]), name)
|
||||||
if decision["action"] == "switch":
|
if decision["action"] == "switch":
|
||||||
registry["active"] = target["id"]
|
registry["active"] = target["id"]
|
||||||
if not target.get("primary"):
|
if not target.get("primary"):
|
||||||
link_shared(provider, account_home(provider, target))
|
link_shared(provider, account_home(provider, target))
|
||||||
notify(headline, f"New {name} sessions now use {target['label']} ({round(decision['toPeak'])}%). Running sessions stay on {source['label']}.")
|
notify(headline, text("New %1 sessions now use %2 (%3%). Running sessions stay on %4.", name, target["label"], round(decision["toPeak"]), source["label"]))
|
||||||
return True
|
return True
|
||||||
notify(
|
notify(
|
||||||
headline,
|
headline,
|
||||||
f"{target['label']} is at {round(decision['toPeak'])}%. Click to switch new sessions to it.",
|
text("%1 is at %2%. Click to switch new sessions to it.", target["label"], round(decision["toPeak"])),
|
||||||
["omarchy-agent-account-use", provider, target["id"]],
|
["omarchy-agent-account-use", provider, target["id"]],
|
||||||
)
|
)
|
||||||
return False
|
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
|
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
|
||||||
echo "New $name sessions already use $active."
|
echo "New $name sessions already use $active."
|
||||||
else
|
else
|
||||||
message="Running sessions stay on $previous."
|
message=$(omarchy-text "Running sessions stay on %1." "$previous")
|
||||||
echo "New $name sessions now use $active. $message"
|
echo "New $name sessions now use $active. $message"
|
||||||
omarchy-notification-send -g "New $name sessions now use $active" "$message"
|
omarchy-notification-send -g "$(omarchy-text "New %1 sessions now use %2" "$name" "$active")" "$message"
|
||||||
# The usage record's own limits describe the active account.
|
# The usage record's own limits describe the active account.
|
||||||
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
||||||
fi
|
fi
|
||||||
@@ -22,6 +22,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -32,6 +33,11 @@ from typing import Any
|
|||||||
|
|
||||||
AGENT_ID = "claude"
|
AGENT_ID = "claude"
|
||||||
AGENT_NAME = "Claude Code"
|
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."
|
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
|
||||||
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
||||||
PROBE_MIN_INTERVAL_SECONDS = 15
|
PROBE_MIN_INTERVAL_SECONDS = 15
|
||||||
@@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
|||||||
except urllib.error.HTTPError as error:
|
except urllib.error.HTTPError as error:
|
||||||
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
||||||
if error.code == 429:
|
if error.code == 429:
|
||||||
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
|
if retry_after:
|
||||||
f" (retry after {retry_after}s)" if retry_after else ""
|
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)
|
||||||
) + ". Local Claude Code stats are still shown."
|
else:
|
||||||
|
help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.")
|
||||||
else:
|
else:
|
||||||
help_text = f"Anthropic's usage endpoint returned status {error.code}. Local Claude Code stats are still shown."
|
help_text = text("Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", error.code)
|
||||||
return {"ok": False, "helpText": help_text}
|
return {"ok": False, "helpText": help_text}
|
||||||
except Exception:
|
except Exception:
|
||||||
# A transport failure reached no server at all — no route, no DNS. Any
|
# A transport failure reached no server at all — no route, no DNS. Any
|
||||||
|
|||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
|
||||||
|
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
|
||||||
|
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --render only builds the two files, so an unprivileged caller can hand root
|
||||||
|
# nothing but plain copies of the finished result.
|
||||||
|
render_dir=""
|
||||||
|
if [[ ${1:-} == "--render" ]]; then
|
||||||
|
(( $# == 3 || $# == 4 )) || usage
|
||||||
|
channel="$2"
|
||||||
|
render_dir="$3"
|
||||||
|
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
|
||||||
|
etc="${4:-/}/etc"
|
||||||
|
else
|
||||||
|
(( $# >= 1 && $# <= 2 )) || usage
|
||||||
|
channel="$1"
|
||||||
|
etc="${2:-/}/etc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$channel" in
|
||||||
|
stable|rc|edge) ;;
|
||||||
|
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The ISO writes this plain-text marker before target finalization. Missing
|
||||||
|
# markers keep existing installations global; never source it as shell code.
|
||||||
|
region=global
|
||||||
|
if [[ -e $etc/omarchy/region ]]; then
|
||||||
|
region=$(<"$etc/omarchy/region")
|
||||||
|
fi
|
||||||
|
case "$region" in
|
||||||
|
global|cn) ;;
|
||||||
|
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Build both files before replacing either. Explicit refreshes retain backups;
|
||||||
|
# ordinary updates do not call this command or enforce regional defaults.
|
||||||
|
if [[ -n $render_dir ]]; then
|
||||||
|
staging="$render_dir"
|
||||||
|
else
|
||||||
|
staging=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$staging"' EXIT
|
||||||
|
fi
|
||||||
|
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
|
||||||
|
if [[ $region == "global" ]]; then
|
||||||
|
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
|
||||||
|
else
|
||||||
|
profile="$OMARCHY_PATH/default/regions/$region/pacman"
|
||||||
|
printf '\n' >> "$staging/pacman.conf"
|
||||||
|
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
|
||||||
|
# pacman tries servers in order, so the regional mirror leads and the
|
||||||
|
# channel's own mirror stays as the fallback.
|
||||||
|
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z $render_dir ]]; then
|
||||||
|
mkdir -p "$etc/pacman.d"
|
||||||
|
for file in pacman.conf pacman.d/mirrorlist; do
|
||||||
|
if [[ -e $etc/$file ]]; then
|
||||||
|
cp -f "$etc/$file" "$etc/$file.bak"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Copying over an existing file keeps its owner and mode, as refresh always has.
|
||||||
|
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
|
||||||
|
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
|
||||||
|
fi
|
||||||
@@ -6,8 +6,8 @@ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
|
|||||||
|
|
||||||
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
||||||
omarchy-brightness-keyboard-mute on
|
omarchy-brightness-keyboard-mute on
|
||||||
omarchy-osd -i microphone-muted -m "Microphone muted"
|
omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
|
||||||
else
|
else
|
||||||
omarchy-brightness-keyboard-mute off
|
omarchy-brightness-keyboard-mute off
|
||||||
omarchy-osd -i microphone -m "Microphone on"
|
omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
|
||||||
fi
|
fi
|
||||||
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
|
|||||||
sinks_count=$(jq 'length' <<<"$sinks")
|
sinks_count=$(jq 'length' <<<"$sinks")
|
||||||
|
|
||||||
if (( sinks_count == 0 )); then
|
if (( sinks_count == 0 )); then
|
||||||
omarchy-osd -m "No audio devices found"
|
omarchy-osd -m "$(omarchy-text "No audio devices found")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ fi
|
|||||||
|
|
||||||
level=$1
|
level=$1
|
||||||
|
|
||||||
omarchy-notification-send -g -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000
|
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-hook battery-low "$level"
|
omarchy-hook battery-low "$level"
|
||||||
@@ -150,9 +150,9 @@ if [[ $charge_holding == "true" ]]; then
|
|||||||
threshold_label="${threshold_end}%"
|
threshold_label="${threshold_end}%"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · Holding at %2 · %3W / %4Wh" "$percentage" "$threshold_label" "$power_rate" "$capacity"
|
||||||
elif [[ $state == "charging" ]]; then
|
elif [[ $state == "charging" ]]; then
|
||||||
echo "Battery ${percentage}% · ${time_remaining} to full · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · %2 to full · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||||
else
|
else
|
||||||
echo "Battery ${percentage}% · ${time_remaining} left · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · %2 left · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||||
fi
|
fi
|
||||||
@@ -23,7 +23,7 @@ SELECTION=$(slurp 2>/dev/null)
|
|||||||
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
||||||
|
|
||||||
if [[ -z $RESULT ]]; then
|
if [[ -z $RESULT ]]; then
|
||||||
omarchy-notification-send -g -u critical "No QR code found" "Select a region containing a QR code"
|
omarchy-notification-send -g -u critical "$(omarchy-text "No QR code found")" "$(omarchy-text "Select a region containing a QR code")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -33,4 +33,4 @@ fi
|
|||||||
# the notification history, and an unmarked copy would be retained by clipboard
|
# the notification history, and an unmarked copy would be retained by clipboard
|
||||||
# history. Pasting still works; only the recorded copy is given up.
|
# history. Pasting still works; only the recorded copy is given up.
|
||||||
printf '%s' "$RESULT" | wl-copy --sensitive
|
printf '%s' "$RESULT" | wl-copy --sensitive
|
||||||
omarchy-notification-send -g "QR code copied to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "QR code copied to clipboard")"
|
||||||
@@ -86,7 +86,7 @@ esac
|
|||||||
# --status only answers whether our recording runs, for the bar and the menu, so
|
# --status only answers whether our recording runs, for the bar and the menu, so
|
||||||
# it needs no recordings directory and never notifies.
|
# it needs no recordings directory and never notifies.
|
||||||
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
|
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
|
||||||
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR"
|
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ start_webcam_overlay() {
|
|||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -235,7 +235,9 @@ wf_audio_device() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wf_audio_unavailable() {
|
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
|
local audio
|
||||||
|
audio=$(omarchy-text "$1")
|
||||||
|
omarchy-notification-send -u critical -t 5000 "$(omarchy-text "Recording without %1 audio" "$audio")" "$(omarchy-text "The %1 audio could not be added to this recording." "$audio")" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
wf_audio_cleanup() {
|
wf_audio_cleanup() {
|
||||||
@@ -424,7 +426,7 @@ stop_screenrecording() {
|
|||||||
|
|
||||||
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
|
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
|
||||||
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL
|
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."
|
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.")"
|
||||||
else
|
else
|
||||||
finalize_recording
|
finalize_recording
|
||||||
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
||||||
@@ -434,7 +436,7 @@ stop_screenrecording() {
|
|||||||
# Generate a preview thumbnail from the first frame
|
# 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
|
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
|
||||||
|
|
||||||
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
omarchy-notification-send "$(omarchy-text "Screen recording saved")" "$(omarchy-text "Open with Super + Alt + , (or click this)")" \
|
||||||
-t 10000 --image "${preview:-$filename}" \
|
-t 10000 --image "${preview:-$filename}" \
|
||||||
--exec mpv -- "$filename"
|
--exec mpv -- "$filename"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
mapfile -t devices < <(omarchy-capture-webcam-list)
|
mapfile -t devices < <(omarchy-capture-webcam-list)
|
||||||
if (( ${#devices[@]} == 0 )); then
|
if (( ${#devices[@]} == 0 )); then
|
||||||
omarchy-notification-send "No webcam devices found" -u critical -t 3000
|
omarchy-notification-send "$(omarchy-text "No webcam devices found")" -u critical -t 3000
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ TEXT=$(grim -g "$SELECTION" - | tesseract stdin stdout --oem 1 --psm 6 -l "${OMA
|
|||||||
[[ -z $TEXT ]] && exit 1
|
[[ -z $TEXT ]] && exit 1
|
||||||
|
|
||||||
printf "%s" "$TEXT" | wl-copy
|
printf "%s" "$TEXT" | wl-copy
|
||||||
omarchy-notification-send -g "Copied text from selection to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "Copied text from selection to clipboard")"
|
||||||
@@ -47,9 +47,31 @@ validate_dev_checkout() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The dev channel refreshes pacman with the checkout's own tools. A checkout
|
||||||
|
# without this machine's region profile would silently drop its repositories.
|
||||||
|
validate_dev_region() {
|
||||||
|
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
|
||||||
|
|
||||||
|
[[ -f $region_file ]] && region=$(<"$region_file")
|
||||||
|
[[ $region == "global" ]] && return 0
|
||||||
|
|
||||||
|
# The refresh after linking needs a runnable helper and every fragment, not
|
||||||
|
# just a directory with the region's name: dev refreshes edge, and leaving
|
||||||
|
# dev later refreshes stable or rc with this checkout's tools.
|
||||||
|
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
|
||||||
|
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
|
||||||
|
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
|
||||||
|
[[ -r $profile/$fragment ]] || supported=false
|
||||||
|
done
|
||||||
|
if ! $supported; then
|
||||||
|
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
link_dev_checkout() {
|
link_dev_checkout() {
|
||||||
local checkout="$1" required
|
local checkout="$1" required
|
||||||
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
|
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
|
||||||
|
validate_dev_region "$checkout"
|
||||||
|
|
||||||
# Check the destination before changing /etc/omarchy.conf or sudo's path.
|
# Check the destination before changing /etc/omarchy.conf or sudo's path.
|
||||||
# An existing checkout is not pulled automatically and may predate this policy.
|
# An existing checkout is not pulled automatically and may predate this policy.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ copy_url() {
|
|||||||
|
|
||||||
[[ -n $url ]] || return 1
|
[[ -n $url ]] || return 1
|
||||||
printf '%s' "$url" | wl-copy --type text/plain
|
printf '%s' "$url" | wl-copy --type text/plain
|
||||||
omarchy-notification-send -g "URL copied to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "URL copied to clipboard")"
|
||||||
}
|
}
|
||||||
|
|
||||||
reply_copied() {
|
reply_copied() {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ download_url() {
|
|||||||
|
|
||||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
# 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
|
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 "No video found for download" "$url"
|
omarchy-notification-send -u critical -g "$(omarchy-text "No video found for download")" "$url"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ download_url() {
|
|||||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||||
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||||
# is one discrete argument, so it never reaches a shell.
|
# is one discrete argument, so it never reaches a shell.
|
||||||
omarchy-notification-send -g "Download complete" "$title" \
|
omarchy-notification-send -g "$(omarchy-text "Download complete")" "$title" \
|
||||||
-t 10000 --image "${preview:-$filepath}" \
|
-t 10000 --image "${preview:-$filepath}" \
|
||||||
--exec mpv -- "$filepath" || true
|
--exec mpv -- "$filepath" || true
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ download_url() {
|
|||||||
rm -f "$preview"
|
rm -f "$preview"
|
||||||
) &
|
) &
|
||||||
else
|
else
|
||||||
omarchy-notification-send -u critical -g "Download failed" "$url"
|
omarchy-notification-send -u critical -g "$(omarchy-text "Download failed")" "$url"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ announce() {
|
|||||||
omarchy-notification-send \
|
omarchy-notification-send \
|
||||||
--urgency critical \
|
--urgency critical \
|
||||||
--glyph "$CRASH_GLYPH" \
|
--glyph "$CRASH_GLYPH" \
|
||||||
"Process crashed: $comm" \
|
"$(omarchy-text "Process crashed: %1" "$comm")" \
|
||||||
"Click to diagnose with AI" \
|
"$(omarchy-text "Click to diagnose with AI")" \
|
||||||
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
--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
|
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default browser"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default browser" "$name")"
|
||||||
@@ -59,4 +59,4 @@ fi
|
|||||||
mkdir -p "$(dirname "$editor_file")"
|
mkdir -p "$(dirname "$editor_file")"
|
||||||
printf '%s\n' "$editor" >"$editor_file"
|
printf '%s\n' "$editor" >"$editor_file"
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default editor"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default editor" "$name")"
|
||||||
@@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <<EOF
|
|||||||
$desktop_id
|
$desktop_id
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default terminal"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default terminal" "$name")"
|
||||||
@@ -169,7 +169,7 @@ set_terminal_size() {
|
|||||||
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
|
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
|
||||||
local new_id
|
local new_id
|
||||||
new_id="$(omarchy-notification-send \
|
new_id="$(omarchy-notification-send \
|
||||||
"Restart Foot to apply the new terminal font size" \
|
"$(omarchy-text "Restart Foot to apply the new terminal font size")" \
|
||||||
"${replace[@]}" -p 2>/dev/null)" || true
|
"${replace[@]}" -p 2>/dev/null)" || true
|
||||||
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ XML
|
|||||||
omarchy-restart-shell
|
omarchy-restart-shell
|
||||||
|
|
||||||
if pgrep -x ghostty; then
|
if pgrep -x ghostty; then
|
||||||
omarchy-notification-send -g "You must restart Ghostty to see font change"
|
omarchy-notification-send -g "$(omarchy-text "You must restart Ghostty to see font change")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if pgrep -x foot; then
|
if pgrep -x foot; then
|
||||||
omarchy-notification-send -g "You must restart Foot to see font change"
|
omarchy-notification-send -g "$(omarchy-text "You must restart Foot to see font change")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-hook font-set "$font_name"
|
omarchy-hook font-set "$font_name"
|
||||||
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
|
|||||||
mapfile -t cores < <(omarchy-games-retro-cores)
|
mapfile -t cores < <(omarchy-games-retro-cores)
|
||||||
|
|
||||||
if (( ${#cores[@]} == 0 )); then
|
if (( ${#cores[@]} == 0 )); then
|
||||||
omarchy-notification-send -g "No RetroArch cores found" "/usr/lib/libretro"
|
omarchy-notification-send -g "$(omarchy-text "No RetroArch cores found")" "/usr/lib/libretro"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,4 +69,4 @@ EOF
|
|||||||
chmod +x "$desktop_file"
|
chmod +x "$desktop_file"
|
||||||
update-desktop-database "$desktop_dir" &>/dev/null || true
|
update-desktop-database "$desktop_dir" &>/dev/null || true
|
||||||
|
|
||||||
omarchy-notification-send -g "$game_name installed" "Start it with Super + Space"
|
omarchy-notification-send -g "$(omarchy-text "%1 installed" "$game_name")" "$(omarchy-text "Start it with Super + Space")"
|
||||||
@@ -20,9 +20,9 @@ if (( $# == 0 )); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
theme) LABEL="Corners follow the theme" ;;
|
theme) LABEL=$(omarchy-text "Corners follow the theme") ;;
|
||||||
sharp) LABEL="Sharp corners for all themes" ;;
|
sharp) LABEL=$(omarchy-text "Sharp corners for all themes") ;;
|
||||||
rounded) LABEL="Rounded corners for all themes" ;;
|
rounded) LABEL=$(omarchy-text "Rounded corners for all themes") ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
|
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ wake() {
|
|||||||
on() {
|
on() {
|
||||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||||
omarchy-hyprland-toggle $TOGGLE off
|
omarchy-hyprland-toggle $TOGGLE off
|
||||||
omarchy-notification-send -g "Laptop display enabled"
|
omarchy-notification-send -g "$(omarchy-text "Laptop display enabled")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wake
|
wake
|
||||||
@@ -24,25 +24,25 @@ on() {
|
|||||||
|
|
||||||
off() {
|
off() {
|
||||||
if [[ -z $INTERNAL ]]; then
|
if [[ -z $INTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No laptop display found"
|
omarchy-notification-send -g "$(omarchy-text "No laptop display found")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The name is written into generated Lua below, so only a plain connector
|
# The name is written into generated Lua below, so only a plain connector
|
||||||
# name may pass; anything else could execute on the next reload.
|
# name may pass; anything else could execute on the next reload.
|
||||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! omarchy-hyprland-monitor-external-active; then
|
if ! omarchy-hyprland-monitor-external-active; then
|
||||||
omarchy-notification-send -g "Can't disable the only active display"
|
omarchy-notification-send -g "$(omarchy-text "Can't disable the only active display")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
||||||
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
||||||
omarchy-notification-send -g "Laptop display disabled"
|
omarchy-notification-send -g "$(omarchy-text "Laptop display disabled")"
|
||||||
hyprctl reload
|
hyprctl reload
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
|
|||||||
|
|
||||||
on() {
|
on() {
|
||||||
if [[ -z $EXTERNAL ]]; then
|
if [[ -z $EXTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No external monitors found for mirror"
|
omarchy-notification-send -g "$(omarchy-text "No external monitors found for mirror")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $INTERNAL ]]; then
|
if [[ -z $INTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No laptop monitor found to mirror"
|
omarchy-notification-send -g "$(omarchy-text "No laptop monitor found to mirror")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ on() {
|
|||||||
# names may pass; a user-created headless output can carry any name.
|
# names may pass; a user-created headless output can carry any name.
|
||||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -35,7 +35,7 @@ on() {
|
|||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
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"
|
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
|
||||||
omarchy-notification-send -g "Mirroring enabled ($EXTERNAL)"
|
omarchy-notification-send -g "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
|
||||||
hyprctl reload
|
hyprctl reload
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ on() {
|
|||||||
off() {
|
off() {
|
||||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||||
omarchy-hyprland-toggle $TOGGLE off
|
omarchy-hyprland-toggle $TOGGLE off
|
||||||
omarchy-notification-send -g "Extended mode restored"
|
omarchy-notification-send -g "$(omarchy-text "Extended mode restored")"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
# omarchy:summary=Toggle single-window square aspect ratio.
|
# omarchy:summary=Toggle single-window square aspect ratio.
|
||||||
|
|
||||||
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
||||||
on) omarchy-notification-send -g "Enable single-window square aspect ratio" ;;
|
on) omarchy-notification-send -g "$(omarchy-text "Enable single-window square aspect ratio")" ;;
|
||||||
off) omarchy-notification-send -g "Disable single-window square aspect ratio" ;;
|
off) omarchy-notification-send -g "$(omarchy-text "Disable single-window square aspect ratio")" ;;
|
||||||
esac
|
esac
|
||||||
@@ -37,9 +37,9 @@ if (( $# == 0 )); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
opaque) LABEL="Opaque windows" ;;
|
opaque) LABEL=$(omarchy-text "Opaque windows") ;;
|
||||||
transparent) LABEL="Transparent windows" ;;
|
transparent) LABEL=$(omarchy-text "Transparent windows") ;;
|
||||||
glassy) LABEL="Glassy windows" ;;
|
glassy) LABEL=$(omarchy-text "Glassy windows") ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
|
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ notify_missing_width() {
|
|||||||
local key="$1"
|
local key="$1"
|
||||||
local workspace="$2"
|
local workspace="$2"
|
||||||
|
|
||||||
omarchy-notification-send -g "No saved width found for $key on workspace $workspace" "Use Super + Alt + Home to save one for this workspace."
|
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."
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_saved_width() {
|
notify_saved_width() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
local workspace="$2"
|
local workspace="$2"
|
||||||
|
|
||||||
omarchy-notification-send -g "Saved width for $key on workspace $workspace" "Restore using Super + Home on this workspace."
|
omarchy-notification-send -g "$(omarchy-text "Saved width for %1 on workspace %2" "$key" "$workspace")" "Restore using Super + Home on this workspace."
|
||||||
}
|
}
|
||||||
|
|
||||||
hypr_dispatch() {
|
hypr_dispatch() {
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
|
|||||||
|
|
||||||
hyprctl reload >/dev/null
|
hyprctl reload >/dev/null
|
||||||
omarchy-shell -q omarchy.workspaces refresh
|
omarchy-shell -q omarchy.workspaces refresh
|
||||||
omarchy-notification-send -g "New workspaces use the $LABEL layout"
|
omarchy-notification-send -g "$(omarchy-text "New workspaces use the %1 layout" "$LABEL")"
|
||||||
@@ -43,17 +43,17 @@ if hyprctl eval "$APPLY" >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if (( ! APPLIED )); then
|
if (( ! APPLIED )); then
|
||||||
omarchy-notification-send -g "Workspace layout needs a Hyprland reload"
|
omarchy-notification-send -g "$(omarchy-text "Workspace layout needs a Hyprland reload")"
|
||||||
elif [[ $NEW_MODE == "$(omarchy-hyprland-workspace-layout-default)" ]] && rm -f "$MODE_FILE"; then
|
elif [[ $NEW_MODE == "$(omarchy-hyprland-workspace-layout-default)" ]] && rm -f "$MODE_FILE"; then
|
||||||
# Cycled onto the default, a workspace goes back to following it, so a later
|
# Cycled onto the default, a workspace goes back to following it, so a later
|
||||||
# change of the default reaches it too.
|
# change of the default reaches it too.
|
||||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
|
||||||
elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
|
elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
|
||||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
|
||||||
else
|
else
|
||||||
# The mode is on screen but nothing will bring it back, and the next press
|
# The mode is on screen but nothing will bring it back, and the next press
|
||||||
# reads the mode from this file, so say so rather than counting it as set.
|
# reads the mode from this file, so say so rather than counting it as set.
|
||||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE but could not be saved"
|
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1 but could not be saved" "$NEW_MODE")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The bar shows the focused workspace's layout.
|
# The bar shows the focused workspace's layout.
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ superwhisper shortcuts set toggle "Alt+Space" >/dev/null
|
|||||||
superwhisper shortcuts set hold none >/dev/null
|
superwhisper shortcuts set hold none >/dev/null
|
||||||
omarchy-dictation-use superwhisper
|
omarchy-dictation-use superwhisper
|
||||||
hyprctl reload >/dev/null
|
hyprctl reload >/dev/null
|
||||||
omarchy-notification-send -g "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
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
|
||||||
superwhisper settings
|
superwhisper settings
|
||||||
@@ -33,4 +33,4 @@ systemctl --user enable --now voxtype.service
|
|||||||
|
|
||||||
omarchy-dictation-use voxtype
|
omarchy-dictation-use voxtype
|
||||||
hyprctl reload >/dev/null
|
hyprctl reload >/dev/null
|
||||||
omarchy-notification-send -g "Voxtype Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
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
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-notification-send -u low "Editing config file" "$path"
|
omarchy-notification-send -u low "$(omarchy-text "Editing config file")" "$path"
|
||||||
exec omarchy-launch-editor "$path"
|
exec omarchy-launch-editor "$path"
|
||||||
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
|
|||||||
case $terminal in
|
case $terminal in
|
||||||
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
|
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
|
||||||
*)
|
*)
|
||||||
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty"
|
omarchy-notification-send -g ✋ "$(omarchy-text "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty")"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
|
|||||||
". as \$plugins
|
". as \$plugins
|
||||||
| .[] | select($filter)
|
| .[] | select($filter)
|
||||||
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
||||||
[[ -n $rows ]] || { omarchy-notification-send "No plugin to ${1}"; exit 0; }
|
[[ -n $rows ]] || { omarchy-notification-send "$(omarchy-text "No plugin to $1")"; exit 0; }
|
||||||
|
|
||||||
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
|
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
|
||||||
[[ -n $selection ]] || exit 0
|
[[ -n $selection ]] || exit 0
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ else
|
|||||||
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
||||||
|
|
||||||
if ((${status:-0} > 1)); then
|
if ((${status:-0} > 1)); then
|
||||||
omarchy-notification-send -g "" -u critical "Could not share" "The file chooser did not open"
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not share")" "$(omarchy-text "The file chooser did not open")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ set -e
|
|||||||
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
||||||
sudo timedatectl set-timezone "$timezone"
|
sudo timedatectl set-timezone "$timezone"
|
||||||
omarchy-shell -q omarchy.clock refresh
|
omarchy-shell -q omarchy.clock refresh
|
||||||
omarchy-notification-send "Timezone is now set to $timezone"
|
omarchy-notification-send "$(omarchy-text "Timezone is now set to %1" "$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)
|
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
|
||||||
|
|
||||||
if (( pending_count == 1 )); then
|
if (( pending_count == 1 )); then
|
||||||
message="Click to run 1 pending migration."
|
message=$(omarchy-text "Click to run 1 pending migration.")
|
||||||
else
|
else
|
||||||
message="Click to run $pending_count pending migrations."
|
message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# This runs from omarchy-migrate-notify.service after graphical-session.target,
|
# 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
|
# 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.
|
# invitation over and exit instead of staying activated until it is answered.
|
||||||
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
omarchy-notification-send -u critical -g "$(omarchy-text "Pending Omarchy Migrations")" "$message" \
|
||||||
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
--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
|
# 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:summary=Show the current time and date notification
|
||||||
|
|
||||||
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y · Week %V")"
|
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y") · $(omarchy-text "Week %1" "$(date +%V)")"
|
||||||
@@ -159,8 +159,8 @@ done
|
|||||||
omarchy-plugin-enable "$new_id" >/dev/null
|
omarchy-plugin-enable "$new_id" >/dev/null
|
||||||
clone_complete=1
|
clone_complete=1
|
||||||
omarchy-notification-send -g \
|
omarchy-notification-send -g \
|
||||||
"Editing Cloned Plugin" \
|
"$(omarchy-text "Editing Cloned Plugin")" \
|
||||||
"Original plugin has been replace by clone."
|
"$(omarchy-text "Original plugin has been replace by clone.")"
|
||||||
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
||||||
if (( edit_clone )); then
|
if (( edit_clone )); then
|
||||||
# Word-splitting is deliberate: EDITOR may carry flags.
|
# Word-splitting is deliberate: EDITOR may carry flags.
|
||||||
|
|||||||
@@ -1174,15 +1174,6 @@ run_provisioning() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if omarchy-pkg-present usbguard; then
|
|
||||||
log_step "enrolling the owner's USB devices"
|
|
||||||
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
|
|
||||||
usb_authorization_provision_owner "$username"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_step "enrolling the owner's Thunderbolt accessories"
|
|
||||||
/usr/bin/omarchy-thunderbolt-authorization-admin owner
|
|
||||||
|
|
||||||
log_step "cleaning up provisioning state"
|
log_step "cleaning up provisioning state"
|
||||||
cleanup_oem_state
|
cleanup_oem_state
|
||||||
log_step "first-boot setup complete"
|
log_step "first-boot setup complete"
|
||||||
|
|||||||
@@ -42,11 +42,29 @@ if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channe
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
conf=$templates/pacman-$channel.conf mirrorlist=$templates/mirrorlist-$channel
|
||||||
|
# Regions render x86 pacman only (omarchy #13690).
|
||||||
|
if [[ $platform == x86 ]]; then
|
||||||
|
# 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"
|
||||||
|
conf=$rendered/pacman.conf mirrorlist=$rendered/mirrorlist
|
||||||
|
fi
|
||||||
|
|
||||||
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
||||||
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||||
echo "Setting channel to $channel"
|
echo "Setting channel to $channel"
|
||||||
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
|
sudo cp -f "$conf" /etc/pacman.conf
|
||||||
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
sudo cp -f "$mirrorlist" /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
|
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
|
||||||
# custom repositories and IgnorePkg entries shape the downgrade-capable
|
# custom repositories and IgnorePkg entries shape the downgrade-capable
|
||||||
|
|||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# The cn profile renders x86 pacman only (omarchy #13690).
|
||||||
|
if [[ $1 == "cn" && $(omarchy-hw-platform) != "x86" ]]; then
|
||||||
|
echo "The China region needs an x86 machine; $(omarchy-hw-platform) has no China profile yet" >&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?")"; then
|
||||||
|
omarchy-system-logout
|
||||||
|
else
|
||||||
|
omarchy-text "Log back in to switch the developer mirrors."
|
||||||
|
fi
|
||||||
+15
-14
@@ -57,16 +57,16 @@ show_reminders() {
|
|||||||
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
||||||
|
|
||||||
if [[ -n $reminder_message ]]; then
|
if [[ -n $reminder_message ]]; then
|
||||||
body+="$reminder_message in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
body+="$(omarchy-text "%1 in %2 (%3)" "$reminder_message" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||||
else
|
else
|
||||||
body+="${reminder_minutes}-min reminder in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
body+="$(omarchy-text "%1-min reminder in %2 (%3)" "$reminder_minutes" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||||
fi
|
fi
|
||||||
done < <(active_reminder_timers "$now")
|
done < <(active_reminder_timers "$now")
|
||||||
|
|
||||||
if [[ -z $body ]]; then
|
if [[ -z $body ]]; then
|
||||||
omarchy-notification-send -g "Upcoming reminders" "No outstanding reminders"
|
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "$(omarchy-text "No outstanding reminders")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Upcoming reminders" "${body%$'\n'}"
|
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,8 @@ show_json() {
|
|||||||
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||||
local now=$(date +%s)
|
local now=$(date +%s)
|
||||||
local count=0
|
local count=0
|
||||||
local tooltip="Set Reminder"
|
local tooltip
|
||||||
|
tooltip=$(omarchy-text "Set Reminder")
|
||||||
|
|
||||||
while IFS=$'\t' read -r timer next; do
|
while IFS=$'\t' read -r timer next; do
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
@@ -91,7 +92,7 @@ show_json() {
|
|||||||
if [[ -n $reminder_message ]]; then
|
if [[ -n $reminder_message ]]; then
|
||||||
label=$reminder_message
|
label=$reminder_message
|
||||||
else
|
else
|
||||||
label="${reminder_minutes}-min reminder"
|
label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
item_json=$(jq -cn \
|
item_json=$(jq -cn \
|
||||||
@@ -109,9 +110,9 @@ show_json() {
|
|||||||
done < <(active_reminder_timers "$now")
|
done < <(active_reminder_timers "$now")
|
||||||
|
|
||||||
if ((count == 1)); then
|
if ((count == 1)); then
|
||||||
tooltip="1 reminder"
|
tooltip=$(omarchy-text "1 reminder")
|
||||||
elif ((count > 1)); then
|
elif ((count > 1)); then
|
||||||
tooltip="$count reminders"
|
tooltip=$(omarchy-text "%1 reminders" "$count")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
|
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
|
||||||
@@ -129,7 +130,7 @@ clear_reminders() {
|
|||||||
|
|
||||||
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
|
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
|
||||||
refresh_indicator
|
refresh_indicator
|
||||||
omarchy-notification-send -g "All reminders have been cleared"
|
omarchy-notification-send -g "$(omarchy-text "All reminders have been cleared")"
|
||||||
}
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -176,7 +177,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $message ]]; then
|
if [[ -z $message ]]; then
|
||||||
message="Your ${minutes} minutes are up"
|
message=$(omarchy-text "Your %1 minutes are up" "$minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_at=$(date +%s)
|
set_at=$(date +%s)
|
||||||
@@ -184,18 +185,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
|
|||||||
unit="omarchy-reminder-${minutes}m-$set_at"
|
unit="omarchy-reminder-${minutes}m-$set_at"
|
||||||
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||||
message_file="$reminder_dir/$unit.message"
|
message_file="$reminder_dir/$unit.message"
|
||||||
confirmation="You'll be reminded at $remind_at"
|
confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
|
||||||
confirmation_title="Reminder set for ${minutes} minutes"
|
confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
|
||||||
|
|
||||||
mkdir -p "$reminder_dir"
|
mkdir -p "$reminder_dir"
|
||||||
|
|
||||||
if [[ -n $custom_message ]]; then
|
if [[ -n $custom_message ]]; then
|
||||||
printf "%s" "$custom_message" >"$message_file"
|
printf "%s" "$custom_message" >"$message_file"
|
||||||
confirmation_title="$custom_message in ${minutes} minutes"
|
confirmation_title=$(omarchy-text "%1 in %2 minutes" "$custom_message" "$minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
|
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
|
||||||
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"
|
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"
|
||||||
|
|
||||||
refresh_indicator
|
refresh_indicator
|
||||||
omarchy-notification-send -g "$confirmation_title" "$confirmation"
|
omarchy-notification-send -g "$confirmation_title" "$confirmation"
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Restore Bolt's default authorization behavior
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
gum confirm "Restore Bolt's previous policy? New accessories may be authorized automatically when IOMMU protection is active." || exit 0
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin disable
|
|
||||||
systemctl --user disable --now omarchy-thunderbolt-authorization.service
|
|
||||||
echo "Previous Bolt policy restored. Saved Omarchy trust is retained for re-enabling."
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Restore saved Thunderbolt firmware boot permissions
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
gum confirm "Restore the previous Thunderbolt firmware boot allowlists? Device approval will remain enabled." || exit 0
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-disable
|
|
||||||
echo "Previous firmware boot permissions restored and verified."
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Stop asking before new USB devices can access the system
|
|
||||||
# omarchy:args=[--boot-only] [--yes]
|
|
||||||
# omarchy:requires-sudo=true
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
assume_yes=0
|
|
||||||
boot_only=0
|
|
||||||
|
|
||||||
while (( $# > 0 )); do
|
|
||||||
case "$1" in
|
|
||||||
--yes) assume_yes=1 ;;
|
|
||||||
--boot-only) boot_only=1 ;;
|
|
||||||
-h | --help)
|
|
||||||
echo "Usage: omarchy-remove-security-usb-authorization [--boot-only] [--yes]"
|
|
||||||
echo
|
|
||||||
echo "Disables Omarchy's USB approval prompts and restores the kernel's"
|
|
||||||
echo "normal default-allow behavior."
|
|
||||||
echo "Use --boot-only to keep USBGuard enabled after the system starts."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "omarchy-remove-security-usb-authorization: unknown option '$1'. Try --help." >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
if (( boot_only )); then
|
|
||||||
if (( ! assume_yes )); then
|
|
||||||
gum style --foreground 212 --bold "Remove USB authorization from boot"
|
|
||||||
echo
|
|
||||||
echo "USBGuard will continue blocking unknown devices after its service starts."
|
|
||||||
echo "Devices can reach kernel drivers earlier during boot."
|
|
||||||
echo
|
|
||||||
gum confirm "Disable USB authorization from kernel boot?" || exit 130
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo omarchy-usb-authorization-boot disable
|
|
||||||
echo
|
|
||||||
echo "Boot-time USB authorization is disabled and takes effect after reboot."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
state_dir="$HOME/.local/state/omarchy/usb-authorization"
|
|
||||||
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
|
|
||||||
|
|
||||||
if (( ! assume_yes )); then
|
|
||||||
gum style --foreground 212 --bold "Remove USB device authorization"
|
|
||||||
echo
|
|
||||||
echo "New USB devices will be allowed to bind to kernel drivers without approval."
|
|
||||||
echo
|
|
||||||
gum confirm "Disable USB device authorization?" || exit 130
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rebuild a permissive boot image before stopping USBGuard. If rebuilding
|
|
||||||
# fails, the command stops here and leaves the complete policy enabled.
|
|
||||||
sudo omarchy-usb-authorization-boot disable
|
|
||||||
|
|
||||||
if omarchy-pkg-present usbguard; then
|
|
||||||
# Keep the watcher and its IPC access until blocking has actually stopped.
|
|
||||||
# In particular, a failed stop must not strand a running daemon without UI.
|
|
||||||
sudo systemctl disable --now usbguard.service
|
|
||||||
if systemctl is-active --quiet usbguard.service || systemctl is-enabled --quiet usbguard.service; then
|
|
||||||
echo "USBGuard is still active or enabled; approval prompts have been preserved." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sudo omarchy-usb-authorization-restore-default
|
|
||||||
sudo usbguard remove-user "$USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
|
|
||||||
rm -f "$unit"
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
|
|
||||||
if [[ -d $state_dir/requests ]]; then
|
|
||||||
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
|
|
||||||
rmdir "$state_dir/requests" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$state_dir/package-installed-by-omarchy" \
|
|
||||||
"$state_dir/policy-generated-by-omarchy" \
|
|
||||||
"$state_dir/service-enabled-by-omarchy"
|
|
||||||
rmdir "$state_dir" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "USB device authorization is disabled."
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Require approval for new Thunderbolt PCIe accessories
|
|
||||||
# omarchy:args=[--quiet|status|recover]
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
|
|
||||||
|
|
||||||
if [[ ${1:-} == "status" ]]; then
|
|
||||||
tb_status
|
|
||||||
elif [[ ${1:-} == "recover" && $# == 1 ]]; then
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin recover
|
|
||||||
elif [[ $# == 0 || ( $# == 1 && $1 == "--quiet" ) ]]; then
|
|
||||||
if [[ ${1:-} != "--quiet" ]]; then
|
|
||||||
if [[ -f $TB_PENDING ]]; then tb_status; fi
|
|
||||||
echo "First setup, including a deferred setup, trusts connected accessories. Re-enabling a completed setup preserves saved trust."
|
|
||||||
gum confirm "Require approval for new Thunderbolt accessories?" || exit 0
|
|
||||||
fi
|
|
||||||
# System units and rules are shipped by omarchy-settings.
|
|
||||||
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
action=enable
|
|
||||||
if [[ ${1:-} == "--quiet" ]]; then
|
|
||||||
action=migrate
|
|
||||||
fi
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin "$action"
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user enable --now omarchy-thunderbolt-authorization.service
|
|
||||||
if [[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]; then
|
|
||||||
for ((attempt=0; attempt<10; attempt++)); do
|
|
||||||
if tb_public_snapshot >/dev/null 2>&1; then break; fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
tb_status
|
|
||||||
elif [[ -f $TB_PENDING ]]; then
|
|
||||||
tb_status
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Usage: omarchy-setup-security-thunderbolt-authorization [--quiet|status|recover]" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Clear supported Thunderbolt firmware boot allowlists
|
|
||||||
# omarchy:args=[recover]
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ ${1:-} == "recover" && $# == 1 ]]; then
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-recover
|
|
||||||
elif [[ $# == 0 ]]; then
|
|
||||||
gum style --bold "Thunderbolt at Boot"
|
|
||||||
echo "This trusts connected accessories for use after Linux starts, then clears the firmware boot allowlist."
|
|
||||||
echo "Trusted Thunderbolt keyboards, storage, and network adapters may be unavailable at disk unlock."
|
|
||||||
echo "You must be able to unlock and recover without Thunderbolt accessories."
|
|
||||||
echo "All controllers must be connected and support a boot allowlist in user or secure mode."
|
|
||||||
echo "Older snapshots or another operating system may restore firmware boot permissions."
|
|
||||||
gum confirm "Clear firmware boot access with these recovery limits?" || exit 0
|
|
||||||
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-enable
|
|
||||||
echo "Firmware boot allowlists cleared and verified. Current connections were not disconnected."
|
|
||||||
else
|
|
||||||
echo "Usage: omarchy-setup-security-thunderbolt-boot [recover]" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Ask before new USB devices can access the system
|
|
||||||
# omarchy:args=[--boot] [--yes] [--review-existing]
|
|
||||||
# omarchy:requires-sudo=true
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
assume_yes=0
|
|
||||||
from_boot=0
|
|
||||||
review_existing=0
|
|
||||||
|
|
||||||
while (( $# > 0 )); do
|
|
||||||
case "$1" in
|
|
||||||
--yes) assume_yes=1 ;;
|
|
||||||
--boot) from_boot=1 ;;
|
|
||||||
--review-existing) review_existing=1 ;;
|
|
||||||
-h | --help)
|
|
||||||
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes] [--review-existing]"
|
|
||||||
echo
|
|
||||||
echo "Trusts USB devices that are connected now, then blocks new devices"
|
|
||||||
echo "until you approve them from an Omarchy notification."
|
|
||||||
echo
|
|
||||||
echo "--boot also blocks USB devices from the start of kernel boot. Connected"
|
|
||||||
echo "devices are added to USBGuard's policy, but remain unavailable before"
|
|
||||||
echo "USBGuard starts, including at an encrypted-disk password prompt."
|
|
||||||
echo "Older snapshots without USBGuard keep all USB disabled after startup,"
|
|
||||||
echo "including keyboards, network adapters, and storage. Use --boot only if"
|
|
||||||
echo "disk unlock and recovery work without any USB devices."
|
|
||||||
echo
|
|
||||||
echo "--review-existing reviews connected devices with older port-bound approvals."
|
|
||||||
echo "Each portable approval requires confirmation; existing rules are preserved."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# Boot-time blocking goes into Limine's own configuration, which a Mac's boot
|
|
||||||
# package owns, so it is not offered there.
|
|
||||||
if (( from_boot )) && omarchy-hw-aarch64-apple; then
|
|
||||||
echo "USB authorization from boot is not available on Apple Silicon yet; USB device authorization after login still works." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
state_dir="$HOME/.local/state/omarchy/usb-authorization"
|
|
||||||
unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service"
|
|
||||||
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
|
|
||||||
rules_file=/etc/usbguard/rules.conf
|
|
||||||
daemon_config=/etc/usbguard/usbguard-daemon.conf
|
|
||||||
|
|
||||||
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
|
|
||||||
|
|
||||||
if (( review_existing )); then
|
|
||||||
# Older Omarchy rules have no per-rule ownership marker. They cannot safely
|
|
||||||
# be distinguished from intentionally restricted administrator rules.
|
|
||||||
(( ! assume_yes && ! from_boot )) || { echo "Review requires individual confirmation; do not combine it with --yes or --boot." >&2; exit 2; }
|
|
||||||
policy=$(usbguard list-rules)
|
|
||||||
declare -A reviewed=()
|
|
||||||
# Keep stdin attached to the terminal for gum (and any sudo prompt).
|
|
||||||
mapfile -t policy_lines <<<"$policy"
|
|
||||||
for line in "${policy_lines[@]}"; do
|
|
||||||
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
|
|
||||||
rule=${line#*: }
|
|
||||||
[[ $rule == allow\ * ]] || continue
|
|
||||||
portable=$(usb_authorization_portable_rule "$rule") || continue
|
|
||||||
[[ ${portable% label *} != "$rule" ]] || continue
|
|
||||||
devices=$(usbguard list-devices "$rule")
|
|
||||||
mapfile -t candidates <<<"$devices"
|
|
||||||
for device in "${candidates[@]}"; do
|
|
||||||
[[ $device =~ ^([0-9]+):[[:space:]](allow[[:space:]].*)$ ]] || continue
|
|
||||||
id=${BASH_REMATCH[1]} current=${BASH_REMATCH[2]}
|
|
||||||
[[ ! -v reviewed[$id] ]] || continue
|
|
||||||
reviewed[$id]=1
|
|
||||||
echo
|
|
||||||
printf '%s\n' "$current" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
|
|
||||||
echo "This device has a location-bound approval. Details are supplied by the device."
|
|
||||||
if gum confirm "Trust this device on any USB port or hub?"; then
|
|
||||||
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "$current"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "Review complete. Disconnected devices can be approved when reconnected."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The watcher runs in the session's environment, which may differ from a
|
|
||||||
# command invoked in a checkout. Fail before enabling blocking on mismatched
|
|
||||||
# or incomplete installations, including dangling dev-link unit symlinks.
|
|
||||||
session_environment=$(systemctl --user show-environment)
|
|
||||||
grep -Fqx -- "OMARCHY_PATH=$OMARCHY_PATH" <<<"$session_environment" || {
|
|
||||||
echo "The session and this command use different Omarchy installations. Update the session before enabling USB protection." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
for required in "$unit_source" /usr/share/omarchy/install/helpers/usb-authorization-policy.sh; do
|
|
||||||
[[ -r $required ]] || { echo "USB authorization installation is incomplete: $required" >&2; exit 1; }
|
|
||||||
done
|
|
||||||
for required in "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch" \
|
|
||||||
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" \
|
|
||||||
"$OMARCHY_PATH/bin/omarchy-usb-authorization-review" \
|
|
||||||
/usr/bin/omarchy-usb-authorization-approve; do
|
|
||||||
[[ -r $required && -x $required ]] || { echo "USB authorization command is unavailable: $required" >&2; exit 1; }
|
|
||||||
done
|
|
||||||
|
|
||||||
if (( ! assume_yes )); then
|
|
||||||
if (( from_boot )); then
|
|
||||||
gum style --foreground 212 --bold "USB authorization from boot"
|
|
||||||
else
|
|
||||||
gum style --foreground 212 --bold "USB device authorization"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo "Devices connected now will be trusted. Disconnect anything you do not trust."
|
|
||||||
echo "New USB devices will remain blocked until you approve them."
|
|
||||||
if (( from_boot )); then
|
|
||||||
echo
|
|
||||||
echo "Boot-time protection blocks all USB devices until USBGuard starts."
|
|
||||||
echo "A USB keyboard will not work at an encrypted-disk password prompt, even"
|
|
||||||
echo "when it is connected now."
|
|
||||||
echo
|
|
||||||
echo "Older snapshots without USBGuard keep ALL USB disabled after startup,"
|
|
||||||
echo "including keyboards, network adapters, and storage."
|
|
||||||
echo "Continue only if disk unlock and recovery work without any USB devices."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
if (( from_boot )); then
|
|
||||||
gum confirm "Enable USB authorization from kernel boot?" || exit 130
|
|
||||||
else
|
|
||||||
gum confirm "Enable USB device authorization?" || exit 130
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
omarchy-pkg-add usbguard
|
|
||||||
|
|
||||||
usb_authorization_require_secure_settings "$daemon_config"
|
|
||||||
|
|
||||||
mkdir -p "$state_dir" "${unit%/*}"
|
|
||||||
chmod 700 "$state_dir"
|
|
||||||
|
|
||||||
if ! usb_authorization_rules_present "$rules_file"; then
|
|
||||||
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX")
|
|
||||||
trap 'rm -f "$policy"' EXIT
|
|
||||||
chmod 600 "$policy"
|
|
||||||
usb_authorization_generate_policy "$policy"
|
|
||||||
sudo install -Dm600 -o root -g root "$policy" "$rules_file"
|
|
||||||
touch "$state_dir/policy-generated-by-omarchy"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This user can inspect USBGuard events and approve the device named by an
|
|
||||||
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
|
|
||||||
sudo usbguard add-user "$USER" \
|
|
||||||
--devices=list,listen,modify \
|
|
||||||
--policy=list \
|
|
||||||
--exceptions=listen
|
|
||||||
|
|
||||||
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
|
|
||||||
|
|
||||||
# Install a real file: a link into a checkout can become dangling on a branch
|
|
||||||
# switch. Start the waiting watcher before USBGuard can block new devices.
|
|
||||||
unit_temporary=$(mktemp "${unit}.XXXXXXXXXX")
|
|
||||||
install -m644 "$unit_source" "$unit_temporary"
|
|
||||||
mv -fT "$unit_temporary" "$unit"
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user enable omarchy-usb-authorization.service
|
|
||||||
systemctl --user restart omarchy-usb-authorization.service
|
|
||||||
systemctl --user is-active --quiet omarchy-usb-authorization.service
|
|
||||||
|
|
||||||
sudo systemctl enable usbguard.service
|
|
||||||
# add-user ACL changes are loaded only when the daemon starts, so restart an
|
|
||||||
# existing service as well as starting a fresh one.
|
|
||||||
sudo systemctl restart usbguard.service
|
|
||||||
systemctl is-active --quiet usbguard.service
|
|
||||||
systemctl --user is-active --quiet omarchy-usb-authorization.service
|
|
||||||
|
|
||||||
if (( from_boot )); then
|
|
||||||
# The fixed helper independently verifies each full live device snapshot.
|
|
||||||
usb_authorization_trust_present_devices
|
|
||||||
sudo omarchy-usb-authorization-boot enable
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "USB device authorization is enabled."
|
|
||||||
echo "Devices connected during setup remain trusted; new devices require approval."
|
|
||||||
echo "To review older port-bound approvals, run: omarchy-setup-security-usb-authorization --review-existing"
|
|
||||||
if (( from_boot )); then
|
|
||||||
echo "Boot-time authorization is enabled and takes effect after reboot."
|
|
||||||
fi
|
|
||||||
@@ -462,12 +462,6 @@ scrub_factory_accounts() {
|
|||||||
sanitize_factory_baseline() {
|
sanitize_factory_baseline() {
|
||||||
local factory="$1"
|
local factory="$1"
|
||||||
btrfs property set -ts "$factory" ro false
|
btrfs property set -ts "$factory" ro false
|
||||||
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
|
|
||||||
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
|
|
||||||
btrfs property set -ts "$factory" ro true
|
|
||||||
fail "could not reset Thunderbolt authorization in the factory baseline"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! scrub_factory_accounts "$factory"; then
|
if ! scrub_factory_accounts "$factory"; then
|
||||||
btrfs property set -ts "$factory" ro true
|
btrfs property set -ts "$factory" ro true
|
||||||
@@ -493,14 +487,6 @@ stage_full_reset() {
|
|||||||
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
|
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
|
||||||
fail "the factory snapshot predates provisioning support; cannot reset from it"
|
fail "the factory snapshot predates provisioning support; cannot reset from it"
|
||||||
|
|
||||||
# A normal install's factory snapshot includes the original owner's USB
|
|
||||||
# policy and enabled daemon. Keep input usable during the new owner's setup;
|
|
||||||
# provisioning enrolls their devices before enabling USBGuard again.
|
|
||||||
if [[ -f $next/usr/lib/systemd/system/usbguard.service ]]; then
|
|
||||||
systemctl --root="$next" disable usbguard.service >>"$LOG_FILE" 2>&1 ||
|
|
||||||
fail "could not disable USBGuard in the factory system (see $LOG_FILE)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Scrubbing machine identity from the factory system"
|
log "Scrubbing machine identity from the factory system"
|
||||||
systemd-id128 new >"$next/etc/machine-id"
|
systemd-id128 new >"$next/etc/machine-id"
|
||||||
rm -f "$next"/etc/ssh/ssh_host_*
|
rm -f "$next"/etc/ssh/ssh_host_*
|
||||||
@@ -516,13 +502,6 @@ stage_full_reset() {
|
|||||||
scrub_factory_accounts "$next" ||
|
scrub_factory_accounts "$next" ||
|
||||||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
|
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
|
||||||
|
|
||||||
# Leave the new owner's input available before interactive enrollment. Clear
|
|
||||||
# inherited accessory identities in both roots; never change the live daemon.
|
|
||||||
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
|
|
||||||
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
|
|
||||||
fail "could not reset Thunderbolt authorization in the staged root"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# @factory itself survives the wipe as the baseline for future resets. If it
|
# @factory itself survives the wipe as the baseline for future resets. If it
|
||||||
# came from a normal install it still holds the seller's account and
|
# came from a normal install it still holds the seller's account and
|
||||||
# /etc/shadow, which the new wheel user could mount and read — and a second
|
# /etc/shadow, which the new wheel user could mount and read — and a second
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
||||||
|
|
||||||
omarchy-osd -i logout -m "Logging out" -d 5000
|
omarchy-osd -i logout -m "$(omarchy-text "Logging out")" -d 5000
|
||||||
|
|
||||||
# Now close all windows
|
# Now close all windows
|
||||||
omarchy-hyprland-window-close-all
|
omarchy-hyprland-window-close-all
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# scope cannot terminate it before it runs.
|
# 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
|
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
|
||||||
|
|
||||||
omarchy-osd -i reboot -m "Rebooting" -d 5000
|
omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
|
||||||
|
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
if [[ ${1:-} == "--inhibited" ]]; then
|
if [[ ${1:-} == "--inhibited" ]]; then
|
||||||
# Close windows in the background so they cannot delay power-off.
|
# Close windows in the background so they cannot delay power-off.
|
||||||
(
|
(
|
||||||
omarchy-osd -i shutdown -m "Shutting down" -d 5000
|
omarchy-osd -i shutdown -m "$(omarchy-text "Shutting down")" -d 5000
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
omarchy-hyprland-window-close-all
|
omarchy-hyprland-window-close-all
|
||||||
) &
|
) &
|
||||||
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
|
|||||||
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
||||||
"$0" --inhibited || {
|
"$0" --inhibited || {
|
||||||
status=$?
|
status=$?
|
||||||
omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again."
|
omarchy-notification-send -u critical "$(omarchy-text "Shutdown failed")" "$(omarchy-text "Could not complete shutdown. Please try again.")"
|
||||||
exit "$status"
|
exit "$status"
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ report_unsecured() {
|
|||||||
"$1" >&2
|
"$1" >&2
|
||||||
|
|
||||||
omarchy-notification-send -u critical -g \
|
omarchy-notification-send -u critical -g \
|
||||||
"Screen did not lock before suspend" \
|
"$(omarchy-text "Screen did not lock before suspend")" \
|
||||||
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
|
"$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ claim_path() {
|
|||||||
announce() {
|
announce() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
local name="${path##*/}"
|
local name="${path##*/}"
|
||||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical)
|
local args=("$(omarchy-text "Received %1" "$name")" "$(omarchy-text "Saved to %1" "${dir/#$HOME/~}")" -u critical)
|
||||||
|
|
||||||
case "${name,,}" in
|
case "${name,,}" in
|
||||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
*.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=$?
|
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
||||||
|
|
||||||
if ((${status:-0} > 1)); then
|
if ((${status:-0} > 1)); then
|
||||||
omarchy-notification-send -g "" -u critical "Could not send to $name" \
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
|
||||||
"The file chooser did not open"
|
"$(omarchy-text "The file chooser did not open")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
||||||
omarchy-notification-send -g "" "Sent to $name" "$what"
|
omarchy-notification-send -g "" "$(omarchy-text "Sent to %1" "$name")" "$what"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}"
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" "${error:-$(omarchy-text "Taildrop transfer failed")}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/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
|
case ${1:-} in
|
||||||
enable)
|
enable)
|
||||||
rm -f "$toggle"
|
rm -f "$toggle"
|
||||||
omarchy-notification-send "Background Intros" "Enabled"
|
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Enabled")"
|
||||||
;;
|
;;
|
||||||
disable)
|
disable)
|
||||||
mkdir -p "$(dirname "$toggle")"
|
mkdir -p "$(dirname "$toggle")"
|
||||||
touch "$toggle"
|
touch "$toggle"
|
||||||
omarchy-notification-send "Background Intros" "Disabled"
|
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Disabled")"
|
||||||
;;
|
;;
|
||||||
toggle)
|
toggle)
|
||||||
if [[ -f $toggle ]]; then
|
if [[ -f $toggle ]]; then
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
|
|||||||
TOTAL=${#BACKGROUNDS[@]}
|
TOTAL=${#BACKGROUNDS[@]}
|
||||||
|
|
||||||
if (( TOTAL == 0 )); then
|
if (( TOTAL == 0 )); then
|
||||||
omarchy-notification-send "No background was found for theme" -t 2000
|
omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
|
||||||
else
|
else
|
||||||
# Get current background from symlink
|
# Get current background from symlink
|
||||||
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ fi
|
|||||||
|
|
||||||
# Now remove the theme directory for THEME_NAME
|
# Now remove the theme directory for THEME_NAME
|
||||||
rm -rf "$THEME_PATH"
|
rm -rf "$THEME_PATH"
|
||||||
omarchy-notification-send "Theme removed" "$THEME_NAME" || true
|
omarchy-notification-send "$(omarchy-text "Theme removed")" "$THEME_NAME" || true
|
||||||
echo "Removed $THEME_NAME"
|
echo "Removed $THEME_NAME"
|
||||||
@@ -190,7 +190,7 @@ set_theme_background() {
|
|||||||
|
|
||||||
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
||||||
if ! choose_theme_background; then
|
if ! choose_theme_background; then
|
||||||
omarchy-notification-send "No background was found for theme" -t 2000
|
omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
|
||||||
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
|
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash -p
|
|
||||||
|
|
||||||
# omarchy:summary=Manage system Thunderbolt authorization settings
|
|
||||||
# omarchy:hidden=true
|
|
||||||
|
|
||||||
# This root entrypoint uses installed code and rejects unsafe Bash startup.
|
|
||||||
if [[ $- != *p* ]]; then exit 126; fi
|
|
||||||
source /usr/bin/omarchy-security-functions || exit 126
|
|
||||||
omarchy_security_require_privileged_bash_startup || exit 126
|
|
||||||
omarchy_security_sanitize_bash_environment "$0" "$@"
|
|
||||||
set -euo pipefail
|
|
||||||
(( EUID == 0 )) || exit 126
|
|
||||||
export PATH=/usr/bin:/bin LC_ALL=C
|
|
||||||
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
|
|
||||||
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
|
|
||||||
source /usr/share/omarchy/install/helpers/thunderbolt-setup.sh
|
|
||||||
tb_admin "$@"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash -p
|
|
||||||
|
|
||||||
# omarchy:summary=Approve one verified Thunderbolt connection
|
|
||||||
# omarchy:hidden=true
|
|
||||||
|
|
||||||
# pkexec permits this exact program for an active local wheel user. Suppress
|
|
||||||
# startup injection before reading input; it cannot dispatch general root work.
|
|
||||||
if [[ $- != *p* ]]; then exit 126; fi
|
|
||||||
source /usr/bin/omarchy-security-functions || exit 126
|
|
||||||
omarchy_security_require_privileged_bash_startup || exit 126
|
|
||||||
omarchy_security_sanitize_bash_environment "$0" "$@"
|
|
||||||
set -euo pipefail
|
|
||||||
(( EUID == 0 )) || exit 126
|
|
||||||
export PATH=/usr/bin:/bin LC_ALL=C
|
|
||||||
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
|
|
||||||
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
|
|
||||||
|
|
||||||
[[ $# == 2 && ( $1 == "true" || $1 == "false" ) ]] || exit 64
|
|
||||||
(( ${#2} <= 16384 )) || exit 64
|
|
||||||
jq -e 'type == "object"' <<< "$2" >/dev/null
|
|
||||||
exec 9> "$TB_LOCK"
|
|
||||||
flock -x 9
|
|
||||||
result=$(tb_approve "$2" "$1" true)
|
|
||||||
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$result"
|
|
||||||
printf '%s\n' "$result"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Check whether Thunderbolt firmware boot access is restricted
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
|
|
||||||
tb_public_snapshot | jq -e .boot_protection >/dev/null
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash -p
|
|
||||||
|
|
||||||
# omarchy:summary=Restore trusted Thunderbolt devices and publish their status
|
|
||||||
# omarchy:hidden=true
|
|
||||||
|
|
||||||
# Keep root startup independent of a caller's environment or checkout.
|
|
||||||
if [[ $- != *p* ]]; then exit 126; fi
|
|
||||||
source /usr/bin/omarchy-security-functions || exit 126
|
|
||||||
omarchy_security_require_privileged_bash_startup || exit 126
|
|
||||||
omarchy_security_sanitize_bash_environment "$0" "$@"
|
|
||||||
set -euo pipefail
|
|
||||||
(( EUID == 0 )) || exit 126
|
|
||||||
export PATH=/usr/bin:/bin LC_ALL=C
|
|
||||||
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
|
|
||||||
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
|
|
||||||
tb_daemon
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Handle Thunderbolt device approval review
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
|
|
||||||
[[ $# == 1 ]] || exit 64
|
|
||||||
tb_review "$1"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Handle Thunderbolt device approval watch
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
|
|
||||||
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
|
|
||||||
tb_watch
|
|
||||||
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-enabled no-animations; then
|
if omarchy-hyprland-toggle-enabled no-animations; then
|
||||||
omarchy-notification-send -g "Animations disabled"
|
omarchy-notification-send -g "$(omarchy-text "Animations disabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Animations enabled"
|
omarchy-notification-send -g "$(omarchy-text "Animations enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -11,8 +11,8 @@ omarchy-toggle crash-capture-off
|
|||||||
# starting only settles this session; the next login reads the flag itself.
|
# starting only settles this session; the next login reads the flag itself.
|
||||||
if omarchy-toggle-enabled crash-capture-off; then
|
if omarchy-toggle-enabled crash-capture-off; then
|
||||||
systemctl --user stop omarchy-crash-watch.service
|
systemctl --user stop omarchy-crash-watch.service
|
||||||
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture disabled"
|
omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture disabled")"
|
||||||
else
|
else
|
||||||
systemctl --user start omarchy-crash-watch.service
|
systemctl --user start omarchy-crash-watch.service
|
||||||
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture enabled"
|
omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle screensaver-off
|
omarchy-toggle screensaver-off
|
||||||
|
|
||||||
if omarchy-toggle-enabled screensaver-off; then
|
if omarchy-toggle-enabled screensaver-off; then
|
||||||
omarchy-notification-send -g "Screensaver disabled"
|
omarchy-notification-send -g "$(omarchy-text "Screensaver disabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Screensaver enabled"
|
omarchy-notification-send -g "$(omarchy-text "Screensaver enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle suspend-off
|
omarchy-toggle suspend-off
|
||||||
|
|
||||||
if omarchy-toggle-enabled suspend-off; then
|
if omarchy-toggle-enabled suspend-off; then
|
||||||
omarchy-notification-send -g "Suspend removed from system menu"
|
omarchy-notification-send -g "$(omarchy-text "Suspend removed from system menu")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Suspend now available in system menu"
|
omarchy-notification-send -g "$(omarchy-text "Suspend now available in system menu")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle herdr-theme-sync
|
omarchy-toggle herdr-theme-sync
|
||||||
|
|
||||||
if omarchy-toggle-enabled herdr-theme-sync; then
|
if omarchy-toggle-enabled herdr-theme-sync; then
|
||||||
omarchy-notification-send -g "Herdr theme sync enabled"
|
omarchy-notification-send -g "$(omarchy-text "Herdr theme sync enabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Herdr theme sync disabled"
|
omarchy-notification-send -g "$(omarchy-text "Herdr theme sync disabled")"
|
||||||
fi
|
fi
|
||||||
@@ -193,14 +193,14 @@ main() {
|
|||||||
output=$(output_path "$input" "$format" "$resolution")
|
output=$(output_path "$input" "$format" "$resolution")
|
||||||
|
|
||||||
if [[ $type == "video" ]]; then
|
if [[ $type == "video" ]]; then
|
||||||
omarchy-notification-send -g "Transcoding video…" "$(basename -- "$input") to $format ($resolution)"
|
omarchy-notification-send -g "$(omarchy-text "Transcoding video…")" "$(omarchy-text "%1 to %2 (%3)" "$(basename -- "$input")" "$format" "$resolution")"
|
||||||
transcode_video "$input" "$format" "$resolution" "$output"
|
transcode_video "$input" "$format" "$resolution" "$output"
|
||||||
copy_to_clipboard "$output"
|
copy_to_clipboard "$output"
|
||||||
omarchy-notification-send -g "Transcoded to $resolution $format" "Saved and copied to clipboard."
|
omarchy-notification-send -g "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
|
||||||
else
|
else
|
||||||
transcode_picture "$input" "$format" "$resolution" "$output"
|
transcode_picture "$input" "$format" "$resolution" "$output"
|
||||||
copy_to_clipboard "$output"
|
copy_to_clipboard "$output"
|
||||||
omarchy-notification-send -g "Transcoded to $resolution $format" "Saved and copied to clipboard."
|
omarchy-notification-send -g "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:
|
|||||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
||||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||||
omarchy-notification-send -g "TUI removed" "$APP_NAME"
|
omarchy-notification-send -g "$(omarchy-text "TUI removed")" "$APP_NAME"
|
||||||
fi
|
fi
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash -p
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Save portable trust for one verified USB device
|
|
||||||
|
|
||||||
# This narrow Polkit boundary accepts a live device snapshot, never a policy or
|
|
||||||
# file path. Privileged Bash suppresses startup code from the caller's environment.
|
|
||||||
if [[ $- != *p* ]]; then exit 126; fi
|
|
||||||
source /usr/bin/omarchy-security-functions || exit 126
|
|
||||||
omarchy_security_require_privileged_bash_startup || exit 126
|
|
||||||
omarchy_security_sanitize_bash_environment "$0" "$@"
|
|
||||||
set -euo pipefail
|
|
||||||
(( EUID == 0 )) || exit 126
|
|
||||||
export PATH=/usr/bin:/bin LC_ALL=C
|
|
||||||
unset USBGUARD_DAEMON_CONF USBGUARD_DEBUG
|
|
||||||
source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh
|
|
||||||
|
|
||||||
(( $# == 2 )) || exit 64
|
|
||||||
# Omarchy persists to this file. Refuse custom daemon storage rather than
|
|
||||||
# mistaking a stale copy of a rule for a successful persistent decision.
|
|
||||||
rules_file=$(awk -F= '/^[[:space:]]*RuleFile[[:space:]]*=/ { value=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value; exit }' /etc/usbguard/usbguard-daemon.conf)
|
|
||||||
[[ $rules_file == "/etc/usbguard/rules.conf" ]] || exit 1
|
|
||||||
exec 8>/run/omarchy-usb-authorization.lock
|
|
||||||
flock -x 8
|
|
||||||
usb_authorization_permanent "$1" "$2"
|
|
||||||
@@ -1,827 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Manage USB authorization from the start of kernel boot
|
|
||||||
# omarchy:args=<enable|disable>
|
|
||||||
# omarchy:requires-sudo=true
|
|
||||||
# omarchy:hidden=true
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
drop_in=/etc/limine-entry-tool.d/usb-authorization.conf
|
|
||||||
setting='KERNEL_CMDLINE[default]+=" usbcore.authorized_default=0"'
|
|
||||||
limine_conf=/boot/limine.conf
|
|
||||||
limine_defaults=/etc/default/limine
|
|
||||||
boot_path=/boot
|
|
||||||
kernel_parameter=usbcore.authorized_default=0
|
|
||||||
snapshot_begin='# Omarchy USB authorization begin'
|
|
||||||
snapshot_setting='SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0'
|
|
||||||
snapshot_end='# Omarchy USB authorization end'
|
|
||||||
|
|
||||||
usb_authorization_cmdline_has_parameter() {
|
|
||||||
local cmdline="$1" parameter="$2" word
|
|
||||||
|
|
||||||
for word in $cmdline; do
|
|
||||||
[[ $word == "$parameter" ]] && return 0
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_secure_boot_enabled() {
|
|
||||||
local value variable
|
|
||||||
|
|
||||||
for variable in /sys/firmware/efi/efivars/SecureBoot-*; do
|
|
||||||
[[ -r $variable ]] || continue
|
|
||||||
value=$(od -An -t u1 -j 4 -N 1 "$variable")
|
|
||||||
value=${value//[[:space:]]/}
|
|
||||||
[[ $value == "1" ]] && return 0
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_verify_boot_entries() {
|
|
||||||
local expected="$1" config="$2" esp="$3"
|
|
||||||
local machine_id="${4:-$(</etc/machine-id)}"
|
|
||||||
local secure_boot="${5:-auto}"
|
|
||||||
local actual_hash embedded_cmdline expected_hash image mode path paths section
|
|
||||||
|
|
||||||
if [[ $secure_boot == "auto" ]]; then
|
|
||||||
if usb_authorization_secure_boot_enabled; then
|
|
||||||
secure_boot=enabled
|
|
||||||
else
|
|
||||||
secure_boot=disabled
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -r $config ]]; then
|
|
||||||
echo "Cannot verify the generated Limine boot entries in $config." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
paths=$(mktemp /tmp/omarchy-usb-boot-paths.XXXXXXXXXX)
|
|
||||||
section=$(mktemp /tmp/omarchy-usb-boot-cmdline.XXXXXXXXXX)
|
|
||||||
|
|
||||||
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
|
||||||
function contains_parameter(value, count, fields, field_index) {
|
|
||||||
count = split(value, fields, /[[:space:]]+/)
|
|
||||||
for (field_index = 1; field_index <= count; field_index++) {
|
|
||||||
if (fields[field_index] == parameter) return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function finish_entry(found) {
|
|
||||||
if (!target_os || !is_kernel) return
|
|
||||||
entries++
|
|
||||||
|
|
||||||
if (has_cmdline) {
|
|
||||||
found = contains_parameter(cmdline)
|
|
||||||
if ((expected == "enabled" && !found) || (expected == "disabled" && found)) bad = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path == "") {
|
|
||||||
bad = 1
|
|
||||||
} else {
|
|
||||||
print (has_cmdline ? "external" : "embedded") "\t" path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/^\/[^/]/ {
|
|
||||||
finish_entry()
|
|
||||||
target_os = 0
|
|
||||||
is_kernel = 0
|
|
||||||
has_cmdline = 0
|
|
||||||
cmdline = ""
|
|
||||||
path = ""
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
|
||||||
target_os = index($0, "machine-id=" machine_id) > 0
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/^[[:space:]]*\/\// {
|
|
||||||
finish_entry()
|
|
||||||
is_kernel = 0
|
|
||||||
has_cmdline = 0
|
|
||||||
cmdline = ""
|
|
||||||
path = ""
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/^[[:space:]]*comment:[[:space:]]*kernel-id=/ {
|
|
||||||
is_kernel = target_os
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
is_kernel && /^[[:space:]]*path:[[:space:]]*/ {
|
|
||||||
path = $0
|
|
||||||
sub(/^[[:space:]]*path:[[:space:]]*/, "", path)
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
is_kernel && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
|
||||||
has_cmdline = 1
|
|
||||||
cmdline = $0
|
|
||||||
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", cmdline)
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
END {
|
|
||||||
finish_entry()
|
|
||||||
if (entries == 0) bad = 1
|
|
||||||
exit bad
|
|
||||||
}
|
|
||||||
' "$config" >"$paths"; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "The generated Limine kernel entries do not all have USB authorization $expected." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS=$'\t' read -r mode path; do
|
|
||||||
if [[ $path != 'boot():/'* ]]; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "Cannot verify the kernel image referenced by Limine: $path" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
image="$esp/${path#'boot():/'}"
|
|
||||||
image=${image%%#*}
|
|
||||||
if [[ ! -r $image ]]; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "Cannot verify the kernel image referenced by Limine: $image" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $path == *'#'* ]]; then
|
|
||||||
expected_hash=${path##*#}
|
|
||||||
actual_hash=$(b2sum "$image")
|
|
||||||
actual_hash=${actual_hash%% *}
|
|
||||||
if [[ $actual_hash != "$expected_hash" ]]; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "The verification hash for $image does not match the generated Limine entry." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
: >"$section"
|
|
||||||
if ! objcopy -O binary --only-section=.cmdline "$image" "$section" 2>/dev/null; then
|
|
||||||
[[ $mode == "external" ]] && continue
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "Cannot inspect the embedded kernel command line in $image." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
embedded_cmdline=$(tr '\0' '\n' <"$section")
|
|
||||||
|
|
||||||
# Without Secure Boot, Limine's external command line takes precedence.
|
|
||||||
# Under Secure Boot, systemd-stub uses a nonempty embedded .cmdline.
|
|
||||||
if [[ $mode == "external" && $secure_boot == "disabled" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A signed UKI can ignore Limine's external cmdline and use its embedded
|
|
||||||
# .cmdline instead. If one exists, both representations must agree.
|
|
||||||
if [[ -z $embedded_cmdline ]]; then
|
|
||||||
if [[ $mode == "external" || $expected == "disabled" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "The generated kernel image $image has no verifiable kernel command line." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $expected == "enabled" ]]; then
|
|
||||||
if ! usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "The generated kernel image $image does not enable boot-time USB authorization." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
elif usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
echo "The generated kernel image $image still enables boot-time USB authorization." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done <"$paths"
|
|
||||||
|
|
||||||
rm -f "$paths" "$section"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_rebuild_and_verify() {
|
|
||||||
local expected="$1" config="$2" esp="$3"
|
|
||||||
local machine_id="${4:-$(</etc/machine-id)}" secure_boot="${5:-auto}"
|
|
||||||
|
|
||||||
if ! limine-mkinitcpio; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# limine-mkinitcpio-hook can swallow an individual kernel build failure and
|
|
||||||
# still return success, so its exit status alone is not a safety boundary.
|
|
||||||
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id" "$secure_boot"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_snapshot_block_status() {
|
|
||||||
local config="$1"
|
|
||||||
|
|
||||||
[[ -r $config ]] || return 2
|
|
||||||
awk -v begin="$snapshot_begin" -v setting="$snapshot_setting" -v end="$snapshot_end" '
|
|
||||||
$0 == begin { begin_count++; state = 1; next }
|
|
||||||
$0 == setting { setting_count++; if (state == 1) state = 2; next }
|
|
||||||
$0 == end { end_count++; if (state == 2) valid_count++; state = 0; next }
|
|
||||||
{ state = 0 }
|
|
||||||
END {
|
|
||||||
if (begin_count == 0 && setting_count == 0 && end_count == 0) exit 1
|
|
||||||
if (begin_count == 1 && setting_count == 1 && end_count == 1 && valid_count == 1) exit 0
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
' "$config"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_enable_snapshot_setting() {
|
|
||||||
local config="$1" status temporary
|
|
||||||
|
|
||||||
if usb_authorization_snapshot_block_status "$config"; then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
fi
|
|
||||||
if (( status != 1 )); then
|
|
||||||
echo "Refusing to modify the USB authorization block in $config." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
|
||||||
cp --preserve=mode,ownership "$config" "$temporary"
|
|
||||||
{
|
|
||||||
cat "$config"
|
|
||||||
printf '\n%s\n%s\n%s\n' "$snapshot_begin" "$snapshot_setting" "$snapshot_end"
|
|
||||||
} >"$temporary"
|
|
||||||
mv -f "$temporary" "$config"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_disable_snapshot_setting() {
|
|
||||||
local config="$1" status temporary
|
|
||||||
|
|
||||||
if usb_authorization_snapshot_block_status "$config"; then
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
fi
|
|
||||||
(( status == 1 )) && return 0
|
|
||||||
if (( status != 0 )); then
|
|
||||||
echo "Refusing to modify the USB authorization block in $config." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
temporary=$(mktemp "${config}.XXXXXXXXXX")
|
|
||||||
cp --preserve=mode,ownership "$config" "$temporary"
|
|
||||||
awk -v begin="$snapshot_begin" '
|
|
||||||
$0 == begin { skip = 2; next }
|
|
||||||
skip > 0 { skip--; next }
|
|
||||||
{ print }
|
|
||||||
' "$config" >"$temporary"
|
|
||||||
mv -f "$temporary" "$config"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_rewrite_boot_cmdlines() {
|
|
||||||
local expected="$1" config="$2" machine_id="${3:-$(</etc/machine-id)}"
|
|
||||||
local backup temporary
|
|
||||||
|
|
||||||
backup=$(mktemp "${config}.omarchy-usb-backup.XXXXXXXXXX")
|
|
||||||
temporary=$(mktemp "${config}.omarchy-usb-new.XXXXXXXXXX")
|
|
||||||
cp --preserve=mode,ownership "$config" "$backup"
|
|
||||||
cp --preserve=mode,ownership "$config" "$temporary"
|
|
||||||
|
|
||||||
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
|
|
||||||
function rewrite_cmdline(line, prefix, value, count, fields, field_index, result, found) {
|
|
||||||
prefix = line
|
|
||||||
sub(/cmdline:.*/, "cmdline:", prefix)
|
|
||||||
value = line
|
|
||||||
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", value)
|
|
||||||
count = split(value, fields, /[[:space:]]+/)
|
|
||||||
result = ""
|
|
||||||
found = 0
|
|
||||||
for (field_index = 1; field_index <= count; field_index++) {
|
|
||||||
if (fields[field_index] == parameter) {
|
|
||||||
found = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (fields[field_index] != "") result = result (result == "" ? "" : " ") fields[field_index]
|
|
||||||
}
|
|
||||||
if (expected == "enabled") result = result (result == "" ? "" : " ") parameter
|
|
||||||
return prefix (result == "" ? "" : " ") result
|
|
||||||
}
|
|
||||||
|
|
||||||
/^\/[^/]/ { target_os = 0 }
|
|
||||||
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
|
|
||||||
target_os = index($0, "machine-id=" machine_id) > 0
|
|
||||||
}
|
|
||||||
target_os && /^[[:space:]]*cmdline:[[:space:]]*/ {
|
|
||||||
print rewrite_cmdline($0)
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
' "$config" >"$temporary"; then
|
|
||||||
rm -f "$backup" "$temporary"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv -f "$temporary" "$config"
|
|
||||||
if ! usb_authorization_enroll_config "$config" "${config%/*}"; then
|
|
||||||
mv -f "$backup" "$config"
|
|
||||||
if ! usb_authorization_secure_boot_enabled; then
|
|
||||||
limine-enroll-config >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
echo "Restored $config because its bootloader verification data could not be updated." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$backup"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_rewrite_snapshot_manifest() {
|
|
||||||
local expected="$1" manifest="$2" temporary
|
|
||||||
|
|
||||||
[[ -e $manifest ]] || return 0
|
|
||||||
temporary=$(mktemp "${manifest}.omarchy-usb.XXXXXXXXXX")
|
|
||||||
if ! cp --preserve=mode,ownership "$manifest" "$temporary"; then
|
|
||||||
rm -f "$temporary"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Limine uses the cached lines when present, and structured command lines
|
|
||||||
# otherwise. Update both, including nested kernels and restore entries.
|
|
||||||
if ! jq --arg expected "$expected" --arg parameter "$kernel_parameter" '
|
|
||||||
def cmdline:
|
|
||||||
if type != "string" then error("Invalid snapshot command line") else
|
|
||||||
[splits("[[:space:]]+") | select(length > 0 and . != $parameter)]
|
|
||||||
| if $expected == "enabled" then . + [$parameter] else . end
|
|
||||||
| join(" ")
|
|
||||||
end;
|
|
||||||
def lines:
|
|
||||||
map(if test("^\\s*(cmdline|kernel_cmdline):"; "i") then
|
|
||||||
capture("^(?<prefix>\\s*(?:cmdline|kernel_cmdline):\\s*)(?<value>.*)$"; "i")
|
|
||||||
| .prefix + (.value | cmdline)
|
|
||||||
else . end);
|
|
||||||
def kernel:
|
|
||||||
.cmdlineDetails |= map(.cmdline |= cmdline | .snapshotCmdline |= cmdline)
|
|
||||||
| .allInConfig |= lines
|
|
||||||
| .allInSnapshotConfig |= lines
|
|
||||||
| .subKernels |= map(kernel);
|
|
||||||
if .jsonFormatVersion != "1.3.0" or (.snapshotEntries | type) != "array" then
|
|
||||||
error("Unsupported Limine snapshot manifest format")
|
|
||||||
else
|
|
||||||
.snapshotEntries |= map(.kernelEntries |= map(kernel))
|
|
||||||
end
|
|
||||||
' "$manifest" >"$temporary"; then
|
|
||||||
rm -f "$temporary"
|
|
||||||
echo "Cannot update stored USB boot policy in $manifest." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv -f "$temporary" "$manifest"
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_verify_signature() {
|
|
||||||
# sbctl verify can return zero for unsigned or unreadable files. Check its
|
|
||||||
# structured result, including the exact path, instead of just the exit code.
|
|
||||||
if ! sbctl verify --json "$1" | jq -e --arg file "$1" \
|
|
||||||
'length == 1 and .[0].file_name == $file and .[0].is_signed == 1' >/dev/null; then
|
|
||||||
echo "Cannot verify the Secure Boot signature of $1 using this machine's signing keys." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read the same architecture and enrollment setting as the installed Limine tool.
|
|
||||||
usb_authorization_limine_settings() (
|
|
||||||
source /usr/lib/limine/limine-common-functions
|
|
||||||
load_config || return 1
|
|
||||||
printf '%s %s\n' "$(limine_efi_arch | tr '[:upper:]' '[:lower:]')" "${ENABLE_ENROLL_LIMINE_CONFIG:-no}"
|
|
||||||
)
|
|
||||||
|
|
||||||
usb_authorization_verify_bootloader() (
|
|
||||||
local config="$1" esp="$2" architecture enrollment image check digest
|
|
||||||
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
|
|
||||||
[[ -n $architecture ]] || return 1
|
|
||||||
image="$esp/EFI/limine/limine_$architecture.efi"
|
|
||||||
usb_authorization_verify_signature "$image" || return 1
|
|
||||||
check=$(mktemp) || return 1
|
|
||||||
trap 'rm -f "$check"' EXIT
|
|
||||||
cp "$image" "$check" || return 1
|
|
||||||
if [[ $enrollment == "yes" ]]; then
|
|
||||||
digest=$(b2sum "$config") || return 1
|
|
||||||
limine enroll-config "$check" "${digest%% *}" --quiet || return 1
|
|
||||||
else
|
|
||||||
limine enroll-config "$check" --reset --quiet || return 1
|
|
||||||
fi
|
|
||||||
# Enrollment edits fixed bytes in the executable. Reapplying the expected
|
|
||||||
# value must be a no-op, including when enrollment is explicitly disabled.
|
|
||||||
if ! cmp -s "$image" "$check"; then
|
|
||||||
echo "Limine's signed executable does not match the configured menu enrollment." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
|
|
||||||
usb_authorization_enroll_config() {
|
|
||||||
limine-enroll-config || return 1
|
|
||||||
if usb_authorization_secure_boot_enabled; then
|
|
||||||
usb_authorization_verify_bootloader "$1" "$2" || return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Kernel rebuild hooks also reset and re-sign Limine. Checkpoint the complete
|
|
||||||
# boot files, not just the menu: rebuilding can overwrite images it references.
|
|
||||||
# The outer process holds the real boot lock; the private mount namespace gives
|
|
||||||
# nested Limine tools their own lock inode so they cannot deadlock against it.
|
|
||||||
usb_authorization_boot_transaction() (
|
|
||||||
local action="$1" state architecture enrollment index manifest changed=0 committed=0 restored=1
|
|
||||||
local machine_id="$(</etc/machine-id)"
|
|
||||||
local cache="${2:-/var/cache/boot/$machine_id}"
|
|
||||||
local -a paths=("$boot_path" "$cache" "$limine_defaults" "$drop_in")
|
|
||||||
exec 8>/run/lock/boot-partition.lock
|
|
||||||
flock --timeout 30 8 || return 1
|
|
||||||
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
|
|
||||||
[[ -n $architecture ]] || return 1
|
|
||||||
usb_authorization_verify_signature "$boot_path/EFI/limine/limine_$architecture.efi" || return 1
|
|
||||||
state=$(mktemp -d /var/tmp/omarchy-usb-boot.XXXXXXXXXX) || return 1
|
|
||||||
trap '
|
|
||||||
if (( changed && ! committed )); then
|
|
||||||
echo "USB boot policy failed; restoring the previous boot files and settings." >&2
|
|
||||||
for ((index=0; index<${#paths[@]}; index++)); do
|
|
||||||
if [[ -d $state/$index && ! -L $state/$index ]]; then
|
|
||||||
mkdir -p "${paths[index]}" && cp -a "$state/$index/." "${paths[index]}/" || restored=0
|
|
||||||
elif [[ -e $state/$index || -L $state/$index ]]; then
|
|
||||||
cp -a --remove-destination "$state/$index" "${paths[index]}" || restored=0
|
|
||||||
else
|
|
||||||
rm -rf -- "${paths[index]}" || restored=0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# New unreferenced images are harmless, but a newly created cached backup
|
|
||||||
# manifest must not reintroduce the failed policy on a later recovery.
|
|
||||||
for manifest in snapshots.json snapshots.json.old; do
|
|
||||||
[[ -e $state/0/$machine_id/limine_history/$manifest ]] ||
|
|
||||||
rm -f "$boot_path/$machine_id/limine_history/$manifest" || restored=0
|
|
||||||
[[ -e $state/1/lss/$manifest ]] || rm -f "$cache/lss/$manifest" || restored=0
|
|
||||||
done
|
|
||||||
sync -f "$boot_path" || restored=0
|
|
||||||
fi
|
|
||||||
if (( restored )); then
|
|
||||||
rm -rf "$state"
|
|
||||||
else
|
|
||||||
echo "Boot recovery was incomplete. Preserve and restore the checkpoint at $state before rebooting." >&2
|
|
||||||
fi
|
|
||||||
' EXIT
|
|
||||||
trap 'exit 1' HUP INT TERM
|
|
||||||
for ((index=0; index<${#paths[@]}; index++)); do
|
|
||||||
if [[ -d ${paths[index]} ]]; then
|
|
||||||
cp -a "${paths[index]}/." "$state/$index" || return 1
|
|
||||||
elif [[ -e ${paths[index]} || -L ${paths[index]} ]]; then
|
|
||||||
cp -a "${paths[index]}" "$state/$index" || return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
: >"$state/lock"
|
|
||||||
changed=1
|
|
||||||
usb_authorization_run_transaction "$state" "$action" || return 1
|
|
||||||
usb_authorization_verify_bootloader "$limine_conf" "$boot_path" || return 1
|
|
||||||
sync -f "$boot_path" || return 1
|
|
||||||
committed=1
|
|
||||||
)
|
|
||||||
|
|
||||||
usb_authorization_run_transaction() {
|
|
||||||
unshare --mount --propagation private /bin/bash -c '
|
|
||||||
mount --bind "$1/lock" /run/lock/boot-partition.lock || exit 1
|
|
||||||
source "$2"
|
|
||||||
usb_authorization_main "$3"
|
|
||||||
' bash "$1" "${BASH_SOURCE[0]}" "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Called with Limine's boot-partition lock held. Prepare signed copies of
|
|
||||||
# conflicting historical UKIs before changing the selected boot policy.
|
|
||||||
usb_authorization_reconcile_archived_images() (
|
|
||||||
local expected="$1" config="$2" esp="$3" machine_id="$4"
|
|
||||||
local cache="${5:-/var/cache/boot/$machine_id/lss/snapshots.json}"
|
|
||||||
local history="$esp/$machine_id/limine_history" staging manifest names name image
|
|
||||||
local cmdline hash_command old_digest new_digest old_hash new_hash replacement index=0
|
|
||||||
local published=0 committed=0 section layout image_base image_size alignment address
|
|
||||||
local -a manifests=() originals=()
|
|
||||||
|
|
||||||
usb_authorization_secure_boot_enabled || return 0
|
|
||||||
[[ -d $history ]] || return 0
|
|
||||||
staging=$(mktemp -d "$esp/.omarchy-usb-uki.XXXXXXXXXX") || return 1
|
|
||||||
trap '
|
|
||||||
if (( published && ! committed )); then
|
|
||||||
for ((index=0; index<${#originals[@]}; index++)); do
|
|
||||||
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
rm -rf "$staging"
|
|
||||||
' EXIT
|
|
||||||
|
|
||||||
for manifest in "$history/snapshots.json" "$history/snapshots.json.old" "$cache"; do
|
|
||||||
[[ -f $manifest ]] || continue
|
|
||||||
jq -e '.jsonFormatVersion == "1.3.0" and (.snapshotEntries | type == "array")' "$manifest" >/dev/null || return 1
|
|
||||||
manifests+=("$manifest")
|
|
||||||
done
|
|
||||||
(( ${#manifests[@]} )) || return 0
|
|
||||||
names=$(jq -sr '[.[] | .snapshotEntries[] | .. | objects | .imageDetails? // empty | .[] | .fileHashName] | unique[]' "${manifests[@]}") || return 1
|
|
||||||
printf '[]\n' >"$staging/replacements.json"
|
|
||||||
|
|
||||||
while IFS= read -r name; do
|
|
||||||
[[ -n $name ]] || continue
|
|
||||||
# Only image names, never arbitrary paths supplied by a manifest.
|
|
||||||
if [[ $name == */* || $name == "." || $name == ".." ]]; then
|
|
||||||
echo "Invalid archived kernel image name: $name" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
image="$history/$name"
|
|
||||||
[[ -f $image && ! -L $image ]] || continue
|
|
||||||
: >"$staging/cmdline"
|
|
||||||
objcopy -O binary --only-section=.cmdline "$image" "$staging/cmdline" 2>/dev/null || continue
|
|
||||||
cmdline=$(tr '\0' '\n' <"$staging/cmdline")
|
|
||||||
[[ -n $cmdline ]] || continue
|
|
||||||
if usb_authorization_cmdline_has_parameter "$cmdline" "$kernel_parameter"; then
|
|
||||||
[[ $expected == "disabled" ]] || continue
|
|
||||||
else
|
|
||||||
[[ $expected == "enabled" ]] || continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Multi-profile images and signed PCR policies need their original builder.
|
|
||||||
# Do not silently invalidate their additional boot/unlock guarantees.
|
|
||||||
for section in .profile .pcrsig; do
|
|
||||||
objcopy -O binary --only-section="$section" "$image" "$staging/section" || return 1
|
|
||||||
if [[ -s $staging/section ]]; then
|
|
||||||
echo "Rebuild archived image $image with its original signing policy before changing USB boot protection ($section)." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ ! $name =~ ^(.+)_(sha1|sha256|b3|xxh)_([0-9a-f]+)$ ]]; then
|
|
||||||
echo "Cannot verify the content hash of archived image $image." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
case "${BASH_REMATCH[2]}" in
|
|
||||||
sha1) hash_command=sha1sum ;;
|
|
||||||
sha256) hash_command=sha256sum ;;
|
|
||||||
b3) hash_command=b3sum ;;
|
|
||||||
xxh) hash_command=xxhsum ;;
|
|
||||||
esac
|
|
||||||
old_digest=${BASH_REMATCH[3]}
|
|
||||||
new_digest=$("$hash_command" "$image") || return 1
|
|
||||||
if [[ ${new_digest%% *} != "$old_digest" ]]; then
|
|
||||||
echo "Archived image hash mismatch: $image" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# Verify the old signature before signing a modified copy with this machine's key.
|
|
||||||
usb_authorization_verify_signature "$image" || return 1
|
|
||||||
cp "$image" "$staging/unsigned.efi" || return 1
|
|
||||||
sbattach --remove "$staging/unsigned.efi" || return 1
|
|
||||||
# Preserve the embedded boot arguments and change only our exact USB
|
|
||||||
# parameter. Do not turn a signed command line into an external override.
|
|
||||||
printf '%s' "$cmdline" | jq -Rrs --arg parameter "$kernel_parameter" --arg expected "$expected" '
|
|
||||||
[scan("[^[:space:]]+|[[:space:]]+") | select(. != $parameter)]
|
|
||||||
| join("") | sub("[[:space:]]+$"; "")
|
|
||||||
| if $expected == "enabled" then . + " " + $parameter else . end
|
|
||||||
' >"$staging/new-cmdline" || return 1
|
|
||||||
printf '\0' >>"$staging/new-cmdline"
|
|
||||||
# objcopy --update-section retains PE VirtualSize and can truncate a longer
|
|
||||||
# command line. Add a fresh section after the existing virtual image instead.
|
|
||||||
layout=$(objdump -p "$staging/unsigned.efi" | awk '
|
|
||||||
$1 == "ImageBase" { base = $2 }
|
|
||||||
$1 == "SizeOfImage" { size = $2 }
|
|
||||||
$1 == "SectionAlignment" { alignment = $2 }
|
|
||||||
END {
|
|
||||||
if (base !~ /^[0-9a-fA-F]+$/ || size !~ /^[0-9a-fA-F]+$/ || alignment !~ /^[0-9a-fA-F]+$/) exit 1
|
|
||||||
print "0x" base, "0x" size, "0x" alignment
|
|
||||||
}
|
|
||||||
') || return 1
|
|
||||||
read -r image_base image_size alignment <<<"$layout"
|
|
||||||
(( alignment > 0 )) || return 1
|
|
||||||
address=$((image_base + (image_size + alignment - 1) / alignment * alignment))
|
|
||||||
objcopy --remove-section=.cmdline "$staging/unsigned.efi" "$staging/without-cmdline.efi" || return 1
|
|
||||||
objcopy --add-section .cmdline="$staging/new-cmdline" \
|
|
||||||
--change-section-vma .cmdline="$address" \
|
|
||||||
--set-section-flags .cmdline=alloc,load,readonly,data,contents \
|
|
||||||
"$staging/without-cmdline.efi" "$staging/image.efi" || return 1
|
|
||||||
sbctl sign "$staging/image.efi" || return 1
|
|
||||||
usb_authorization_verify_signature "$staging/image.efi" || return 1
|
|
||||||
objcopy -O binary --only-section=.cmdline "$staging/image.efi" "$staging/cmdline" || return 1
|
|
||||||
cmp -s "$staging/cmdline" "$staging/new-cmdline" || return 1
|
|
||||||
new_digest=$("$hash_command" "$staging/image.efi") || return 1
|
|
||||||
replacement="${name%"$old_digest"}${new_digest%% *}"
|
|
||||||
old_hash=$(b2sum "$image") || return 1
|
|
||||||
new_hash=$(b2sum "$staging/image.efi") || return 1
|
|
||||||
# Publish copies first: every old reference remains usable until its manifest changes.
|
|
||||||
if [[ -e $history/$replacement || -L $history/$replacement ]]; then
|
|
||||||
[[ ! -L $history/$replacement ]] && cmp -s "$staging/image.efi" "$history/$replacement" || return 1
|
|
||||||
else
|
|
||||||
mv "$staging/image.efi" "$history/$replacement" || return 1
|
|
||||||
fi
|
|
||||||
jq --arg old "$name" --arg new "$replacement" \
|
|
||||||
--arg old_hash "${old_hash%% *}" --arg new_hash "${new_hash%% *}" \
|
|
||||||
'. + [{old: $old, new: $new, old_hash: $old_hash, new_hash: $new_hash}]' \
|
|
||||||
"$staging/replacements.json" >"$staging/next.json" || return 1
|
|
||||||
mv "$staging/next.json" "$staging/replacements.json" || return 1
|
|
||||||
done <<<"$names"
|
|
||||||
jq -e 'length > 0' "$staging/replacements.json" >/dev/null || return 0
|
|
||||||
|
|
||||||
originals=("$config" "${manifests[@]}")
|
|
||||||
for ((index=0; index<${#originals[@]}; index++)); do
|
|
||||||
cp --preserve=mode,ownership "${originals[index]}" "$staging/original.$index" || return 1
|
|
||||||
cp --preserve=mode,ownership "${originals[index]}" "$staging/updated.$index" || return 1
|
|
||||||
# Update cached menu lines, structured image metadata, restore paths and
|
|
||||||
# both hashes. The new file name remains a valid Limine content address.
|
|
||||||
if (( index == 0 )); then
|
|
||||||
jq -Rrs --slurpfile replacements "$staging/replacements.json" '
|
|
||||||
split("\n") | map(reduce $replacements[0][] as $r (. ;
|
|
||||||
if contains($r.old) then split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash)
|
|
||||||
else . end)) | join("\n")
|
|
||||||
' "$staging/original.$index" >"$staging/updated.$index" || return 1
|
|
||||||
else
|
|
||||||
jq --slurpfile replacements "$staging/replacements.json" '
|
|
||||||
walk(if type == "string" then
|
|
||||||
reduce $replacements[0][] as $r (. ; split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash))
|
|
||||||
else . end)
|
|
||||||
' "$staging/original.$index" >"$staging/updated.$index" || return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
published=1
|
|
||||||
for ((index=0; index<${#originals[@]}; index++)); do
|
|
||||||
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
|
|
||||||
done
|
|
||||||
usb_authorization_enroll_config "$config" "$esp" || return 1
|
|
||||||
committed=1
|
|
||||||
)
|
|
||||||
|
|
||||||
usb_authorization_prepare_archived_images() (
|
|
||||||
local expected="$1" config="$2" esp="$3" machine_id="${4:-$(</etc/machine-id)}"
|
|
||||||
|
|
||||||
usb_authorization_secure_boot_enabled || return 0
|
|
||||||
# Package hooks acquire the boot lock too; install this optional tool before
|
|
||||||
# taking it, never while rewriting archived images.
|
|
||||||
if omarchy-cmd-missing sbattach; then
|
|
||||||
omarchy-pkg-add sbsigntools || return 1
|
|
||||||
fi
|
|
||||||
exec 9>/run/lock/boot-partition.lock
|
|
||||||
flock --timeout 30 9 || return 1
|
|
||||||
limine-snapper-sync --no-mutex --no-hooks || return 1
|
|
||||||
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
usb_authorization_sync_and_verify() (
|
|
||||||
local expected="$1" config="$2" esp="$3"
|
|
||||||
local machine_id="${4:-$(</etc/machine-id)}" manifest
|
|
||||||
|
|
||||||
# Share Limine's lock so a concurrent watcher cannot restore an old manifest
|
|
||||||
# while we update it. The sync wrapper must not reacquire our lock.
|
|
||||||
exec 9>/run/lock/boot-partition.lock
|
|
||||||
flock --timeout 30 9 || return 1
|
|
||||||
|
|
||||||
# Recover/upgrade the manifest first, including backups if necessary.
|
|
||||||
limine-snapper-sync --no-mutex --no-hooks || return 1
|
|
||||||
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id" || return 1
|
|
||||||
for manifest in \
|
|
||||||
"$esp/$machine_id/limine_history/snapshots.json" \
|
|
||||||
"$esp/$machine_id/limine_history/snapshots.json.old" \
|
|
||||||
"/var/cache/boot/$machine_id/lss/snapshots.json"; do
|
|
||||||
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest" || return 1
|
|
||||||
done
|
|
||||||
|
|
||||||
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine_id" || return 1
|
|
||||||
# Verify the regenerated menu, so synchronization cannot mask stale stored
|
|
||||||
# command lines with a one-time edit to limine.conf.
|
|
||||||
limine-snapper-sync --no-mutex --no-hooks || return 1
|
|
||||||
usb_authorization_enroll_config "$config" "$esp" || return 1
|
|
||||||
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
# A Mac's boot chain belongs to its boot package, so boot-time denial is not
|
|
||||||
# managed here. Disabling what is not configured succeeds, so turning USB
|
|
||||||
# authorization off still works; anything configured by another route is left
|
|
||||||
# to the owner, since this cannot rebuild a Mac's boot files.
|
|
||||||
usb_authorization_apple_boot() {
|
|
||||||
if [[ ${1:-} == "enable" ]]; then
|
|
||||||
echo "USB authorization from boot is not available on Apple Silicon yet." >&2
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
if [[ -e $drop_in ]] || grep -Fqx "$snapshot_begin" "$limine_defaults" 2>/dev/null; then
|
|
||||||
echo "Boot-time USB authorization is configured in $drop_in or $limine_defaults, which this command cannot remove on Apple Silicon." >&2
|
|
||||||
echo "Remove it by hand: delete $drop_in and the lines from '$snapshot_begin' to '$snapshot_end' in $limine_defaults, then rebuild the boot files." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usb_authorization_main() {
|
|
||||||
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
|
|
||||||
|
|
||||||
if (( EUID != 0 )); then
|
|
||||||
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
enable)
|
|
||||||
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
|
||||||
snapshot_status=0
|
|
||||||
else
|
|
||||||
snapshot_status=$?
|
|
||||||
fi
|
|
||||||
if (( snapshot_status == 2 )); then
|
|
||||||
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Quote the expected value because the Limine key contains glob brackets.
|
|
||||||
if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then
|
|
||||||
echo "Refusing to replace the existing $drop_in." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
usb_authorization_prepare_archived_images enabled "$limine_conf" "$boot_path" || exit 1
|
|
||||||
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
|
||||||
|
|
||||||
if [[ ! -e $drop_in ]]; then
|
|
||||||
install -d -m755 "${drop_in%/*}"
|
|
||||||
temporary=$(mktemp "${drop_in}.XXXXXXXXXX")
|
|
||||||
trap 'rm -f "$temporary"' EXIT
|
|
||||||
printf '%s\n' "$setting" >"$temporary"
|
|
||||||
chmod 644 "$temporary"
|
|
||||||
mv -f "$temporary" "$drop_in"
|
|
||||||
trap - EXIT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! limine-mkinitcpio ||
|
|
||||||
! usb_authorization_sync_and_verify enabled "$limine_conf" "$boot_path"; then
|
|
||||||
echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
disable)
|
|
||||||
if usb_authorization_snapshot_block_status "$limine_defaults"; then
|
|
||||||
had_snapshot_setting=1
|
|
||||||
else
|
|
||||||
snapshot_status=$?
|
|
||||||
if (( snapshot_status == 2 )); then
|
|
||||||
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -e $drop_in ]]; then
|
|
||||||
if [[ $(<"$drop_in") != "$setting" ]]; then
|
|
||||||
echo "Refusing to remove the modified $drop_in." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
had_drop_in=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
usb_authorization_prepare_archived_images disabled "$limine_conf" "$boot_path" || exit 1
|
|
||||||
if (( had_drop_in )); then
|
|
||||||
rm -f "$drop_in"
|
|
||||||
fi
|
|
||||||
|
|
||||||
usb_authorization_disable_snapshot_setting "$limine_defaults"
|
|
||||||
if ! limine-mkinitcpio ||
|
|
||||||
! usb_authorization_sync_and_verify disabled "$limine_conf" "$boot_path"; then
|
|
||||||
if (( had_drop_in )); then
|
|
||||||
printf '%s\n' "$setting" >"$drop_in"
|
|
||||||
chmod 644 "$drop_in"
|
|
||||||
fi
|
|
||||||
if (( had_snapshot_setting )); then
|
|
||||||
usb_authorization_enable_snapshot_setting "$limine_defaults"
|
|
||||||
fi
|
|
||||||
echo "USBGuard remains required because the boot images could not be verified as default-allow." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
if (( EUID != 0 )); then
|
|
||||||
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
case "${1:-}" in
|
|
||||||
enable|disable)
|
|
||||||
if /usr/bin/omarchy-hw-aarch64-apple; then
|
|
||||||
usb_authorization_apple_boot "$1"
|
|
||||||
elif usb_authorization_secure_boot_enabled; then
|
|
||||||
usb_authorization_boot_transaction "$1"
|
|
||||||
else
|
|
||||||
usb_authorization_main "$1"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
|
|
||||||
return 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
||||||
main "$@"
|
|
||||||
fi
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Create an approval request for a blocked USB device
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
case "${USBGUARD_IPC_SIGNAL:-}" in
|
|
||||||
IPC.Connected) ;;
|
|
||||||
Device.PolicyApplied)
|
|
||||||
# PresenceChanged precedes policy evaluation, even for trusted devices.
|
|
||||||
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
|
|
||||||
;;
|
|
||||||
Device.PresenceChanged)
|
|
||||||
[[ ${USBGUARD_DEVICE_EVENT:-} == "Remove" ]] || exit 0
|
|
||||||
;;
|
|
||||||
*) exit 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
|
|
||||||
mkdir -p "$requests"
|
|
||||||
chmod 700 "${requests%/*}" "$requests"
|
|
||||||
exec 9>"$requests/.lock"
|
|
||||||
|
|
||||||
request_generation() {
|
|
||||||
local daemon_id
|
|
||||||
|
|
||||||
# USBGuard reuses device IDs after restarting. Include both the daemon
|
|
||||||
# invocation and watcher lifetime so old prompts cannot suppress new ones.
|
|
||||||
daemon_id=$(systemctl show usbguard.service --property=InvocationID --value) || return 1
|
|
||||||
[[ $daemon_id =~ ^[0-9a-f]{32}$ ]] || return 1
|
|
||||||
printf '%s:%s' "${OMARCHY_USB_AUTHORIZATION_WATCH_ID:-standalone}" "$daemon_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ $USBGUARD_IPC_SIGNAL == "IPC.Connected" ]]; then
|
|
||||||
flock 9
|
|
||||||
generation=$(request_generation) || exit 1
|
|
||||||
for pending in "$requests"/request-*.json; do
|
|
||||||
[[ -f $pending ]] || continue
|
|
||||||
if ! jq -e --arg generation "$generation" '.generation == $generation' "$pending" >/dev/null 2>&1; then
|
|
||||||
rm -f "$pending"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
flock -u 9
|
|
||||||
|
|
||||||
# USBGuard scans present devices before accepting IPC connections. Repeat
|
|
||||||
# this on every reconnect: temporary approvals disappear on daemon restart.
|
|
||||||
blocked=$(usbguard list-devices --blocked) || exit 1
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then
|
|
||||||
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
|
|
||||||
USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \
|
|
||||||
USBGUARD_DEVICE_TARGET_NEW=block \
|
|
||||||
USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \
|
|
||||||
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" || true
|
|
||||||
fi
|
|
||||||
done <<<"$blocked"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
|
|
||||||
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
|
|
||||||
|
|
||||||
# A removed device's final target may be allow even though a blocked request
|
|
||||||
# remains. Its identity attributes are unchanged; normalize the target.
|
|
||||||
USBGUARD_DEVICE_RULE="block ${USBGUARD_DEVICE_RULE#* }"
|
|
||||||
|
|
||||||
# The token is safe to carry through the presentation launcher's command
|
|
||||||
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
|
|
||||||
# is never interpolated into a shell command.
|
|
||||||
token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" | sha256sum | cut -d' ' -f1)"
|
|
||||||
request="$requests/$token.json"
|
|
||||||
# watch executes handlers concurrently. A reconnect scan and a policy event
|
|
||||||
# can refer to the same device; publish only one pending request.
|
|
||||||
if [[ $USBGUARD_IPC_SIGNAL == "Device.PresenceChanged" ]]; then
|
|
||||||
flock 9
|
|
||||||
blocked=$(usbguard list-devices --blocked) || exit 1
|
|
||||||
# A delayed removal event must not erase a request for a reappeared device.
|
|
||||||
if ! grep -Fqx -- "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" <<<"$blocked"; then
|
|
||||||
rm -f "$request"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Keep retrying without needing another USB event. Handlers belong to the
|
|
||||||
# watcher service and are stopped with the graphical session.
|
|
||||||
while true; do
|
|
||||||
flock 9
|
|
||||||
generation=$(request_generation) || exit 1
|
|
||||||
if [[ -f $request ]] && jq -e --arg generation "$generation" '.generation == $generation' "$request" >/dev/null 2>&1; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Discard events overtaken by another policy decision or device replacement.
|
|
||||||
blocked=$(usbguard list-devices --blocked) || exit 1
|
|
||||||
current=0
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ $line == "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" ]]; then
|
|
||||||
current=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done <<<"$blocked"
|
|
||||||
(( current )) || exit 0
|
|
||||||
|
|
||||||
# Login and shell restarts can leave D-Bus alive before notifications are ready.
|
|
||||||
# Bound each wait so disappeared devices are rechecked between attempts.
|
|
||||||
if ! omarchy-notification-wait 1; then
|
|
||||||
flock -u 9
|
|
||||||
sleep 1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
temporary=$(mktemp "$requests/.request.XXXXXXXXXX")
|
|
||||||
trap 'rm -f "$temporary"' EXIT
|
|
||||||
|
|
||||||
jq -n \
|
|
||||||
--arg id "$USBGUARD_DEVICE_ID" \
|
|
||||||
--arg rule "$USBGUARD_DEVICE_RULE" \
|
|
||||||
--arg generation "$generation" \
|
|
||||||
'{id: $id, rule: $rule, generation: $generation}' >"$temporary"
|
|
||||||
chmod 600 "$temporary"
|
|
||||||
mv -f "$temporary" "$request"
|
|
||||||
|
|
||||||
if omarchy-notification-send \
|
|
||||||
--app-name omarchy-action \
|
|
||||||
--urgency critical \
|
|
||||||
--glyph \
|
|
||||||
"USB accessory blocked" \
|
|
||||||
"Click to review it. Keep it blocked unless you recognize the device." \
|
|
||||||
--exec omarchy-launch-floating-terminal-with-presentation \
|
|
||||||
omarchy-usb-authorization-review "$token"; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -f "$request"
|
|
||||||
flock -u 9
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Restore the kernel's default USB authorization state
|
|
||||||
# omarchy:requires-sudo=true
|
|
||||||
# omarchy:hidden=true
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if (( EUID != 0 )); then
|
|
||||||
echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# USBGuard intentionally leaves root hubs in default-deny mode when it exits.
|
|
||||||
# Restore both the hub defaults and devices that are still connected.
|
|
||||||
shopt -s nullglob
|
|
||||||
for attribute in /sys/bus/usb/devices/usb*/authorized_default; do
|
|
||||||
echo 1 >"$attribute"
|
|
||||||
done
|
|
||||||
for attribute in /sys/bus/usb/devices/*/authorized; do
|
|
||||||
echo 1 >"$attribute"
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Review a blocked USB device authorization request
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
token="${1:-}"
|
|
||||||
if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
|
|
||||||
echo "This USB authorization request is invalid or expired." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
|
|
||||||
# Serialize request updates with watcher events, but release the lock while the
|
|
||||||
# user chooses. A changed request must never inherit an old dialog's approval.
|
|
||||||
if [[ ! -d ${request%/*} ]]; then
|
|
||||||
echo "This USB authorization request is no longer available." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec 9>"${request%/*}/.lock"
|
|
||||||
flock 9
|
|
||||||
if [[ ! -f $request || -L $request ]]; then
|
|
||||||
echo "This USB authorization request is no longer available." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
snapshot=$(cat "$request")
|
|
||||||
id=$(jq -er '.id | select(test("^[0-9]+$"))' <<<"$snapshot")
|
|
||||||
expected_rule=$(jq -er '.rule | select(type == "string" and startswith("block "))' <<<"$snapshot")
|
|
||||||
choice=$(jq -er '.approval // "" | select(. == "" or . == "Allow once" or . == "Always allow this device")' <<<"$snapshot")
|
|
||||||
allowed_rule="allow ${expected_rule#block }"
|
|
||||||
|
|
||||||
approval_failed() {
|
|
||||||
echo "Could not confirm USB approval. The request has been kept; try again." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
read_current_rule() {
|
|
||||||
local devices line
|
|
||||||
|
|
||||||
current_rule=""
|
|
||||||
devices=$(usbguard list-devices) || approval_failed
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ $line == "$id: "* ]]; then
|
|
||||||
current_rule="${line#"$id: "}"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done <<<"$devices"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_device() {
|
|
||||||
read_current_rule
|
|
||||||
if [[ $current_rule == "$expected_rule" ]] ||
|
|
||||||
[[ -n $choice && $current_rule == "$allowed_rule" ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
rm -f "$request"
|
|
||||||
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_device
|
|
||||||
|
|
||||||
if [[ -z $choice ]]; then
|
|
||||||
flock -u 9
|
|
||||||
gum style --foreground 212 --bold "Blocked USB accessory"
|
|
||||||
echo
|
|
||||||
echo "The following details come from the device and can be forged:"
|
|
||||||
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
|
|
||||||
echo
|
|
||||||
|
|
||||||
choice=$(gum choose \
|
|
||||||
"Allow once" \
|
|
||||||
"Always allow this device" \
|
|
||||||
"Keep blocked") || exit 130
|
|
||||||
|
|
||||||
flock 9
|
|
||||||
if [[ ! -f $request || -L $request || $(cat "$request") != "$snapshot" ]]; then
|
|
||||||
echo "This USB authorization request changed. Open the latest notification to review it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# A fresh choice can authorize only the blocked identity shown in the dialog.
|
|
||||||
read_current_rule
|
|
||||||
if [[ $current_rule != "$expected_rule" ]]; then
|
|
||||||
rm -f "$request"
|
|
||||||
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$choice" in
|
|
||||||
"Allow once" | "Always allow this device")
|
|
||||||
# Save intent before applying policy: a later query can fail after the device
|
|
||||||
# was authorized. Retry must verify that approval, including persistent trust.
|
|
||||||
temporary=$(mktemp "${request%/*}/.request.XXXXXXXXXX")
|
|
||||||
trap 'rm -f "$temporary"' EXIT
|
|
||||||
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
|
|
||||||
mv -f "$temporary" "$request"
|
|
||||||
|
|
||||||
if [[ $choice == "Always allow this device" ]]; then
|
|
||||||
pkexec /usr/bin/omarchy-usb-authorization-approve "$id" "$current_rule" || approval_failed
|
|
||||||
elif [[ $current_rule == "$expected_rule" ]]; then
|
|
||||||
# Match the complete identity, since numeric IDs can be reassigned.
|
|
||||||
usbguard allow-device "$expected_rule" || approval_failed
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
|
|
||||||
# return success without authorizing anything. Verify the exact device.
|
|
||||||
allowed=$(usbguard list-devices --allowed) || approval_failed
|
|
||||||
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
|
|
||||||
|
|
||||||
if [[ $choice == "Always allow this device" ]]; then
|
|
||||||
# Let USBGuard match policy rules: saved rules can omit the device's port.
|
|
||||||
# --show-devices indents the devices matching each policy rule.
|
|
||||||
policy=$(usbguard list-rules --label omarchy-usb-authorization-v1 --show-devices) || approval_failed
|
|
||||||
policy_allows=0
|
|
||||||
matched=0
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
|
|
||||||
policy_allows=0
|
|
||||||
if [[ $line =~ ^[0-9]+:[[:space:]]allow[[:space:]] ]]; then
|
|
||||||
policy_allows=1
|
|
||||||
fi
|
|
||||||
elif (( policy_allows )) && [[ $line == $'\t'"$id: $allowed_rule" ]]; then
|
|
||||||
matched=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done <<<"$policy"
|
|
||||||
(( matched )) || approval_failed
|
|
||||||
echo "USB accessory added to the trusted policy."
|
|
||||||
else
|
|
||||||
echo "USB accessory allowed until it is disconnected."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"Keep blocked")
|
|
||||||
echo "USB accessory remains blocked."
|
|
||||||
;;
|
|
||||||
*) exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
rm -f "$request"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:hidden=true
|
|
||||||
# omarchy:summary=Watch USBGuard for devices awaiting user approval
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Starting a new graphical-session watcher must offer fresh prompts even if
|
|
||||||
# their old notification actions were dismissed and aged out of history.
|
|
||||||
OMARCHY_USB_AUTHORIZATION_WATCH_ID=$(cat /proc/sys/kernel/random/uuid)
|
|
||||||
export OMARCHY_USB_AUTHORIZATION_WATCH_ID
|
|
||||||
|
|
||||||
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
|
|
||||||
|
|
||||||
# The handler scans blocked devices on IPC.Connected, including reconnects
|
|
||||||
# after a daemon restart, once the event subscription is established.
|
|
||||||
exec usbguard watch --wait --exec "$event_handler"
|
|
||||||
@@ -56,6 +56,6 @@ rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
|||||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||||
|
|
||||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||||
omarchy-notification-send -g "Web app removed" "$APP_NAME"
|
omarchy-notification-send -g "$(omarchy-text "Web app removed")" "$APP_NAME"
|
||||||
fi
|
fi
|
||||||
update-desktop-database "$DESKTOP_DIR" &>/dev/null
|
update-desktop-database "$DESKTOP_DIR" &>/dev/null
|
||||||
@@ -1420,7 +1420,7 @@ launch_windows() {
|
|||||||
if ! priv up_wait; then
|
if ! priv up_wait; then
|
||||||
echo "❌ Failed to start Windows VM!"
|
echo "❌ Failed to start Windows VM!"
|
||||||
echo " Try checking: omarchy-windows-vm status"
|
echo " Try checking: omarchy-windows-vm status"
|
||||||
omarchy-notification-send -u critical "Windows VM" "Failed to start Windows VM"
|
omarchy-notification-send -u critical "$(omarchy-text "Windows VM")" "$(omarchy-text "Failed to start Windows VM")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import sys
|
|||||||
import configure as setup
|
import configure as setup
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
def keyboard_catalog():
|
def keyboard_catalog():
|
||||||
# Read the same choices as the ISO and first-boot form, then convert console
|
# Read the same choices as the ISO and first-boot form, then convert console
|
||||||
# keymaps through systemd's mapping, just as localectl does during install.
|
# keymaps through systemd's mapping, just as localectl does during install.
|
||||||
@@ -64,7 +69,7 @@ def choose(title, catalog, selected, kind, overrides=None):
|
|||||||
if result.returncode == 1:
|
if result.returncode == 1:
|
||||||
return None
|
return None
|
||||||
if result.returncode:
|
if result.returncode:
|
||||||
raise RuntimeError(result.stderr.strip() or "The selection menu could not open")
|
raise RuntimeError(result.stderr.strip() or text("The selection menu could not open"))
|
||||||
|
|
||||||
|
|
||||||
def input_catalog(items):
|
def input_catalog(items):
|
||||||
@@ -78,7 +83,7 @@ def input_catalog(items):
|
|||||||
|
|
||||||
def input_items(items, selected, overrides=None):
|
def input_items(items, selected, overrides=None):
|
||||||
if not any(item[0].startswith("keyboard-") for item in items):
|
if not any(item[0].startswith("keyboard-") for item in items):
|
||||||
raise RuntimeError("The current input group has no keyboard input")
|
raise RuntimeError(text("The current input group has no keyboard input"))
|
||||||
# Retained entries keep their switching order; new engines follow them.
|
# Retained entries keep their switching order; new engines follow them.
|
||||||
kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected]
|
kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected]
|
||||||
current = dict(items)
|
current = dict(items)
|
||||||
@@ -93,7 +98,7 @@ def set_inputs(group, layout, before, after):
|
|||||||
setup.run(["fcitx5-remote", "-c"])
|
setup.run(["fcitx5-remote", "-c"])
|
||||||
setup.live_set(group, layout, after)
|
setup.live_set(group, layout, after)
|
||||||
if setup.live_group() != (group, layout, after):
|
if setup.live_group() != (group, layout, after):
|
||||||
raise RuntimeError("Fcitx did not retain the input selection")
|
raise RuntimeError(text("Fcitx did not retain the input selection"))
|
||||||
except BaseException:
|
except BaseException:
|
||||||
setup.live_set(group, layout, before)
|
setup.live_set(group, layout, before)
|
||||||
raise
|
raise
|
||||||
@@ -103,7 +108,7 @@ def configure_inputs():
|
|||||||
group, layout, items = setup.live_group()
|
group, layout, items = setup.live_group()
|
||||||
catalog, available = input_catalog(items)
|
catalog, available = input_catalog(items)
|
||||||
current = [name for name, override in items if not name.startswith("keyboard-")]
|
current = [name for name, override in items if not name.startswith("keyboard-")]
|
||||||
choose("Input Methods", catalog, current, "input", dict(items))
|
choose(text("Input Methods"), catalog, current, "input", dict(items))
|
||||||
|
|
||||||
|
|
||||||
def load_engines(names, catalog):
|
def load_engines(names, catalog):
|
||||||
@@ -112,7 +117,7 @@ def load_engines(names, catalog):
|
|||||||
if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode]
|
if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode]
|
||||||
if absent or any(name not in setup.PRESETS for name in names):
|
if absent or any(name not in setup.PRESETS for name in names):
|
||||||
labels = ", ".join(catalog.get(name, name) for name in names)
|
labels = ", ".join(catalog.get(name, name) for name in names)
|
||||||
raise RuntimeError(f"Install {' '.join(absent) or 'its engine'} to use {labels}")
|
raise RuntimeError(text("Install %1 to use %2", " ".join(absent) or text("its engine"), labels))
|
||||||
# Fcitx discovers engines only at startup, so one installed while it was
|
# Fcitx discovers engines only at startup, so one installed while it was
|
||||||
# running (by an update, say) needs a restart before it can be selected.
|
# running (by an update, say) needs a restart before it can be selected.
|
||||||
setup.run(["omarchy-restart-xcompose"])
|
setup.run(["omarchy-restart-xcompose"])
|
||||||
@@ -149,7 +154,7 @@ def add_input(method):
|
|||||||
|
|
||||||
def keyboard_values(selected):
|
def keyboard_values(selected):
|
||||||
if not selected:
|
if not selected:
|
||||||
raise ValueError("Select at least one keyboard layout")
|
raise ValueError(text("Select at least one keyboard layout"))
|
||||||
parts = [value.split(":", 1) for value in selected]
|
parts = [value.split(":", 1) for value in selected]
|
||||||
# Preserve the installer's Latin-leading rule for desktop keybindings.
|
# Preserve the installer's Latin-leading rule for desktop keybindings.
|
||||||
if parts[0][0] in setup.NON_LATIN:
|
if parts[0][0] in setup.NON_LATIN:
|
||||||
@@ -176,19 +181,19 @@ def save_keyboard(selected):
|
|||||||
expected = [layout + (":" + variant if variant else "")
|
expected = [layout + (":" + variant if variant else "")
|
||||||
for layout, variant in zip(layouts.split(","), variants.split(","))]
|
for layout, variant in zip(layouts.split(","), variants.split(","))]
|
||||||
if keyboard_selection() != expected:
|
if keyboard_selection() != expected:
|
||||||
raise RuntimeError("Your personal keyboard layout override takes precedence over this selection")
|
raise RuntimeError(text("Your personal keyboard layout override takes precedence over this selection"))
|
||||||
first_layout = layouts.split(",")[0]
|
first_layout = layouts.split(",")[0]
|
||||||
first_variant = variants.split(",")[0]
|
first_variant = variants.split(",")[0]
|
||||||
group_layout = first_layout + ("-" + first_variant if first_variant else "")
|
group_layout = first_layout + ("-" + first_variant if first_variant else "")
|
||||||
primary = next((item[0] for item in before if item[0].startswith("keyboard-")), None)
|
primary = next((item[0] for item in before if item[0].startswith("keyboard-")), None)
|
||||||
if primary is None:
|
if primary is None:
|
||||||
raise RuntimeError("The current input group has no keyboard input")
|
raise RuntimeError(text("The current input group has no keyboard input"))
|
||||||
keyboard = "keyboard-" + group_layout
|
keyboard = "keyboard-" + group_layout
|
||||||
after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)]
|
after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)]
|
||||||
group_changed = True
|
group_changed = True
|
||||||
setup.live_set(group, group_layout, after)
|
setup.live_set(group, group_layout, after)
|
||||||
if setup.live_group() != (group, group_layout, after):
|
if setup.live_group() != (group, group_layout, after):
|
||||||
raise RuntimeError("Fcitx did not retain the keyboard selection")
|
raise RuntimeError(text("Fcitx did not retain the keyboard selection"))
|
||||||
except BaseException:
|
except BaseException:
|
||||||
if original is None:
|
if original is None:
|
||||||
path.resolve().unlink(missing_ok=True)
|
path.resolve().unlink(missing_ok=True)
|
||||||
@@ -205,7 +210,7 @@ def save_keyboard(selected):
|
|||||||
def configure_keyboard():
|
def configure_keyboard():
|
||||||
current = keyboard_selection()
|
current = keyboard_selection()
|
||||||
catalog = keyboard_catalog()
|
catalog = keyboard_catalog()
|
||||||
choose("Keyboard Layouts", catalog, current, "keyboard")
|
choose(text("Keyboard Layouts"), catalog, current, "keyboard")
|
||||||
|
|
||||||
|
|
||||||
def apply_selection(kind, context, values):
|
def apply_selection(kind, context, values):
|
||||||
@@ -213,7 +218,7 @@ def apply_selection(kind, context, values):
|
|||||||
rows = picker_rows(catalog, [])
|
rows = picker_rows(catalog, [])
|
||||||
valid = {row[1:]: key for key, row in rows.items()}
|
valid = {row[1:]: key for key, row in rows.items()}
|
||||||
if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values):
|
if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values):
|
||||||
raise ValueError("The menu returned an invalid selection")
|
raise ValueError(text("The menu returned an invalid selection"))
|
||||||
selected = list(dict.fromkeys(valid[value] for value in values))
|
selected = list(dict.fromkeys(valid[value] for value in values))
|
||||||
if kind == "input":
|
if kind == "input":
|
||||||
save_inputs(selected, context.get("overrides"))
|
save_inputs(selected, context.get("overrides"))
|
||||||
@@ -228,13 +233,13 @@ def main():
|
|||||||
try:
|
try:
|
||||||
add_input(sys.argv[2])
|
add_input(sys.argv[2])
|
||||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||||
raise SystemExit(f"Input method could not be added: {error}")
|
raise SystemExit(text("Input method could not be added: %1", error))
|
||||||
return
|
return
|
||||||
applying = len(sys.argv) == 5 and sys.argv[1] == "apply"
|
applying = len(sys.argv) == 5 and sys.argv[1] == "apply"
|
||||||
kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else ""
|
kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else ""
|
||||||
if kind not in ("input", "keyboard"):
|
if kind not in ("input", "keyboard"):
|
||||||
raise ValueError("Choose keyboard or input setup")
|
raise ValueError("Choose keyboard or input setup")
|
||||||
title = "Input methods" if kind == "input" else "Keyboard layouts"
|
title = text("Input methods could not be updated") if kind == "input" else text("Keyboard layouts could not be updated")
|
||||||
try:
|
try:
|
||||||
if applying:
|
if applying:
|
||||||
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
|
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
|
||||||
@@ -243,7 +248,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
configure_keyboard()
|
configure_keyboard()
|
||||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||||
subprocess.run(["omarchy-notification-send", title + " could not be updated", str(error)], check=True)
|
subprocess.run(["omarchy-notification-send", title, str(error)], check=True)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,12 @@
|
|||||||
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
||||||
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
|
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
|
||||||
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
|
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
|
||||||
|
"setup.language": {"icon":"","label":"Language"},
|
||||||
|
"setup.language.en": {"icon":"","label":"English","checked":"[[ \"$(omarchy-language)\" == \"en_US.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language en'"},
|
||||||
|
"setup.language.zh": {"icon":"","label":"简体中文","checked":"[[ \"$(omarchy-language)\" == \"zh_CN.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language zh'"},
|
||||||
|
"setup.region": {"icon":"","label":"Region"},
|
||||||
|
"setup.region.global": {"icon":"","label":"Global","checked":"[[ \"$(omarchy-region)\" == \"Global\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region global'"},
|
||||||
|
"setup.region.china": {"icon":"","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region cn'"},
|
||||||
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
||||||
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
||||||
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
||||||
@@ -220,12 +226,8 @@
|
|||||||
"setup.plugin.remove": {"icon":"","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"},
|
"setup.plugin.remove": {"icon":"","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"},
|
||||||
"setup.security": {"icon":"","label":"Security"},
|
"setup.security": {"icon":"","label":"Security"},
|
||||||
"setup.config": {"icon":"","label":"Config"},
|
"setup.config": {"icon":"","label":"Config"},
|
||||||
"setup.security.thunderbolt": {"icon":"","label":"Thunderbolt","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization"},
|
|
||||||
"setup.security.thunderbolt-boot": {"icon":"","label":"Thunderbolt Boot","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-boot"},
|
|
||||||
"setup.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
|
"setup.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
|
||||||
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
||||||
"setup.security.usb-authorization": {"icon":"","label":"USB Device Authorization","checked":"systemctl is-enabled --quiet usbguard.service && systemctl --user is-enabled --quiet omarchy-usb-authorization.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-usb-authorization"},
|
|
||||||
"setup.security.usb-authorization-boot": {"icon":"","label":"USB at Boot","when":"! omarchy-hw-aarch64-apple","checked":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-security-usb-authorization --boot'"},
|
|
||||||
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
||||||
"setup.security.ssh-agent": {"icon":"","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
|
"setup.security.ssh-agent": {"icon":"","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
|
||||||
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
||||||
@@ -259,6 +261,16 @@
|
|||||||
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
|
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
|
||||||
"install.windows": {"icon":"","label":"Windows","when":"omarchy-hw-x86","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
"install.windows": {"icon":"","label":"Windows","when":"omarchy-hw-x86","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
||||||
"install.aur": {"icon":"","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
|
"install.aur": {"icon":"","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
|
||||||
|
"install.china": {"icon":"","label":"China Apps","when":"[[ \"$(omarchy-region)\" == \"China\" ]]"},
|
||||||
|
"install.china.wechat": {"icon":"","label":"WeChat","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wechat-universal-bwrap","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wechat-universal-bwrap'"},
|
||||||
|
"install.china.qq": {"icon":"","label":"QQ","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present linuxqq","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add linuxqq'"},
|
||||||
|
"install.china.feishu": {"icon":"","label":"Feishu","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present feishu-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add feishu-bin'"},
|
||||||
|
"install.china.dingtalk": {"icon":"","label":"DingTalk","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present dingtalk-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add dingtalk-bin'"},
|
||||||
|
"install.china.wemeet": {"icon":"","label":"Tencent Meeting","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wemeet-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wemeet-bin'"},
|
||||||
|
"install.china.wps": {"icon":"","label":"WPS Office","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wps-office-cn","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wps-office-cn'"},
|
||||||
|
"install.china.tencent-docs": {"icon":"","label":"Tencent Docs","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/腾讯文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 腾讯文档 https://docs.qq.com \"\"'"},
|
||||||
|
"install.china.yuque": {"icon":"","label":"Yuque","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/语雀.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 语雀 https://www.yuque.com/dashboard \"\"'"},
|
||||||
|
"install.china.shimo": {"icon":"","label":"Shimo","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/石墨文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 石墨文档 https://shimo.im/desktop \"\"'"},
|
||||||
"install.preinstalls": {"icon":"","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
|
"install.preinstalls": {"icon":"","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
|
||||||
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
||||||
"install.browser.edge": {"icon":"","label":"Edge","when":"omarchy-hw-x86","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
"install.browser.edge": {"icon":"","label":"Edge","when":"omarchy-hw-x86","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
||||||
@@ -344,12 +356,8 @@
|
|||||||
"remove.windows": {"icon":"","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"},
|
"remove.windows": {"icon":"","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"},
|
||||||
"remove.preinstalls": {"icon":"","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"},
|
"remove.preinstalls": {"icon":"","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"},
|
||||||
"remove.security": {"icon":"","label":"Security","title":"Remove"},
|
"remove.security": {"icon":"","label":"Security","title":"Remove"},
|
||||||
"remove.security.thunderbolt": {"icon":"","label":"Thunderbolt","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled || -f /etc/omarchy/thunderbolt-authorization.pending ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-authorization"},
|
|
||||||
"remove.security.thunderbolt-boot": {"icon":"","label":"Thunderbolt Boot","when":"omarchy-thunderbolt-authorization-boot-enabled","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-boot"},
|
|
||||||
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
||||||
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
||||||
"remove.security.usb-authorization": {"icon":"","label":"USB Device Authorization","when":"systemctl is-enabled --quiet usbguard.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-usb-authorization"},
|
|
||||||
"remove.security.usb-authorization-boot": {"icon":"","label":"USB at Boot","when":"! omarchy-hw-aarch64-apple && grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-security-usb-authorization --boot-only'"},
|
|
||||||
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
||||||
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
||||||
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Chinese menu labels, overlaid on omarchy-menu.jsonc when LANG is zh_CN.
|
||||||
|
{
|
||||||
|
"apps": {"label":"应用"},
|
||||||
|
"learn": {"label":"学习"},
|
||||||
|
"trigger": {"label":"触发"},
|
||||||
|
"style": {"label":"风格"},
|
||||||
|
"setup": {"label":"设置"},
|
||||||
|
"install": {"label":"安装"},
|
||||||
|
"remove": {"label":"移除"},
|
||||||
|
"update": {"label":"更新"},
|
||||||
|
"about": {"label":"关于"},
|
||||||
|
"system": {"label":"系统"},
|
||||||
|
"system.screensaver": {"label":"屏保"},
|
||||||
|
"system.lock": {"label":"锁屏"},
|
||||||
|
"system.suspend": {"label":"睡眠"},
|
||||||
|
"system.hibernate": {"label":"休眠"},
|
||||||
|
"system.logout": {"label":"注销"},
|
||||||
|
"system.reboot": {"label":"重启"},
|
||||||
|
"system.shutdown": {"label":"关机"},
|
||||||
|
"learn.keybindings": {"label":"快捷键"},
|
||||||
|
"learn.omarchy": {"label":"Omarchy 手册"},
|
||||||
|
"learn.community": {"label":"社区"},
|
||||||
|
"trigger.emoji": {"label":"表情"},
|
||||||
|
"trigger.reminder": {"label":"提醒"},
|
||||||
|
"trigger.capture": {"label":"截取"},
|
||||||
|
"trigger.capture.screenshot": {"label":"截图"},
|
||||||
|
"trigger.capture.screenrecord.stop": {"label":"停止录屏"},
|
||||||
|
"trigger.capture.screenrecord": {"label":"录屏"},
|
||||||
|
"trigger.capture.text": {"label":"OCR 取字"},
|
||||||
|
"trigger.capture.qr": {"label":"二维码"},
|
||||||
|
"trigger.capture.color": {"label":"取色"},
|
||||||
|
"trigger.capture.screenrecord.no-audio": {"label":"无音频"},
|
||||||
|
"trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"},
|
||||||
|
"trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"},
|
||||||
|
"trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"},
|
||||||
|
"trigger.transcode": {"label":"转码"},
|
||||||
|
"trigger.share": {"label":"分享"},
|
||||||
|
"trigger.toggle": {"label":"开关"},
|
||||||
|
"trigger.hardware": {"label":"硬件"},
|
||||||
|
"trigger.tests": {"label":"测速"},
|
||||||
|
"trigger.hardware.laptop-display": {"label":"笔记本屏幕"},
|
||||||
|
"trigger.hardware.mirror-display": {"label":"镜像显示"},
|
||||||
|
"trigger.hardware.hybrid-gpu": {"label":"混合显卡"},
|
||||||
|
"trigger.hardware.touchpad": {"label":"触控板"},
|
||||||
|
"trigger.hardware.touchpad-haptics": {"label":"触控板振动"},
|
||||||
|
"trigger.hardware.touchpad-haptics.low": {"label":"低"},
|
||||||
|
"trigger.hardware.touchpad-haptics.mid": {"label":"中"},
|
||||||
|
"trigger.hardware.touchpad-haptics.high": {"label":"高"},
|
||||||
|
"trigger.hardware.touchscreen": {"label":"触摸屏"},
|
||||||
|
"trigger.reminder.set": {"label":"设一个提醒"},
|
||||||
|
"trigger.reminder.show": {"label":"查看全部"},
|
||||||
|
"trigger.reminder.clear": {"label":"清除全部"},
|
||||||
|
"trigger.share.clipboard": {"label":"剪贴板"},
|
||||||
|
"trigger.share.file": {"label":"文件"},
|
||||||
|
"trigger.share.folder": {"label":"文件夹"},
|
||||||
|
"trigger.share.receive": {"label":"接收"},
|
||||||
|
"trigger.toggle.idle-lock": {"label":"保持唤醒"},
|
||||||
|
"trigger.toggle.notifications": {"label":"通知"},
|
||||||
|
"trigger.toggle.crash-capture": {"label":"崩溃捕获"},
|
||||||
|
"trigger.toggle.screensaver": {"label":"屏保"},
|
||||||
|
"trigger.toggle.nightlight": {"label":"夜间模式"},
|
||||||
|
"trigger.tests.network-speedtest": {"label":"网络测速"},
|
||||||
|
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
|
||||||
|
"style.theme": {"label":"主题"},
|
||||||
|
"style.background": {"label":"壁纸"},
|
||||||
|
"style.unlock": {"label":"解锁画面"},
|
||||||
|
"style.font": {"label":"字体"},
|
||||||
|
"style.screensaver": {"label":"屏保"},
|
||||||
|
"style.about": {"label":"关于"},
|
||||||
|
"style.about.text": {"label":"编辑文字"},
|
||||||
|
"style.about.image": {"label":"取自图片"},
|
||||||
|
"style.about.default": {"label":"恢复默认"},
|
||||||
|
"style.screensaver.text": {"label":"编辑文字"},
|
||||||
|
"style.screensaver.image": {"label":"取自图片"},
|
||||||
|
"style.screensaver.default": {"label":"恢复默认"},
|
||||||
|
"setup.monitors": {"label":"显示器"},
|
||||||
|
"setup.keybindings": {"label":"快捷键"},
|
||||||
|
"setup.input": {"label":"输入设备"},
|
||||||
|
"setup.language": {"label":"语言"},
|
||||||
|
"setup.network": {"label":"网络"},
|
||||||
|
"setup.network.dns.custom": {"label":"自定义"},
|
||||||
|
"setup.network.qr": {"label":"二维码"},
|
||||||
|
"setup.default": {"label":"默认程序"},
|
||||||
|
"setup.default.agent": {"title":"默认 Agent"},
|
||||||
|
"setup.default.dictation": {"label":"语音听写","title":"默认语音听写"},
|
||||||
|
"setup.default.browser": {"label":"浏览器","title":"默认浏览器"},
|
||||||
|
"setup.default.terminal": {"label":"终端","title":"默认终端"},
|
||||||
|
"setup.default.editor": {"label":"编辑器","title":"默认编辑器"},
|
||||||
|
"setup.plugin": {"label":"插件"},
|
||||||
|
"setup.plugin.enable": {"label":"启用插件"},
|
||||||
|
"setup.plugin.disable": {"label":"停用插件"},
|
||||||
|
"setup.plugin.add": {"label":"添加插件"},
|
||||||
|
"setup.plugin.clone": {"label":"克隆插件"},
|
||||||
|
"setup.plugin.remove": {"label":"移除插件"},
|
||||||
|
"setup.security": {"label":"安全"},
|
||||||
|
"setup.config": {"label":"配置"},
|
||||||
|
"setup.security.fingerprint": {"label":"指纹"},
|
||||||
|
"setup.security.passwordless-sudo": {"label":"免密 sudo"},
|
||||||
|
"setup.security.sudoless-docker": {"label":"免 sudo Docker"},
|
||||||
|
"setup.direct-boot": {"label":"直接启动"},
|
||||||
|
"setup.reset": {"label":"恢复出厂设置"},
|
||||||
|
"install.package": {"label":"软件包"},
|
||||||
|
"install.service": {"label":"服务"},
|
||||||
|
"install.development": {"label":"开发环境"},
|
||||||
|
"install.editor": {"label":"编辑器"},
|
||||||
|
"install.style": {"label":"风格"},
|
||||||
|
"install.style.theme": {"label":"主题"},
|
||||||
|
"install.style.background": {"label":"壁纸"},
|
||||||
|
"install.style.font": {"label":"字体"},
|
||||||
|
"install.gaming": {"label":"游戏"},
|
||||||
|
"install.browser": {"label":"浏览器"},
|
||||||
|
"install.webapp": {"label":"网页应用"},
|
||||||
|
"install.terminal": {"label":"终端"},
|
||||||
|
"install.preinstalls": {"label":"预装应用"},
|
||||||
|
"install.service.chromium-account": {"label":"Chromium 账号"},
|
||||||
|
"install.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
|
||||||
|
"install.gaming.xbox-controllers": {"label":"Xbox 手柄"},
|
||||||
|
"install.gaming.retro-launcher": {"label":"RetroArch 启动器"},
|
||||||
|
"install.development.docker-dbs": {"label":"Docker 数据库"},
|
||||||
|
"remove.package": {"label":"软件包"},
|
||||||
|
"remove.ai": {"title":"移除"},
|
||||||
|
"remove.service": {"label":"服务","title":"移除"},
|
||||||
|
"remove.development": {"label":"开发环境","title":"移除"},
|
||||||
|
"remove.theme": {"label":"主题"},
|
||||||
|
"remove.gaming": {"label":"游戏","title":"移除"},
|
||||||
|
"remove.browser": {"label":"浏览器","title":"移除"},
|
||||||
|
"remove.webapp": {"label":"网页应用"},
|
||||||
|
"remove.preinstalls": {"label":"预装应用"},
|
||||||
|
"remove.security": {"label":"安全","title":"移除"},
|
||||||
|
"remove.security.fingerprint": {"label":"指纹"},
|
||||||
|
"remove.security.sudoless-docker": {"label":"免 sudo Docker"},
|
||||||
|
"remove.dictation": {"label":"语音听写"},
|
||||||
|
"remove.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
|
||||||
|
"remove.gaming.xbox-controllers": {"label":"Xbox 手柄 ()"},
|
||||||
|
"remove.development.javascript": {"title":"移除"},
|
||||||
|
"remove.development.php": {"title":"移除"},
|
||||||
|
"remove.development.elixir": {"title":"移除"},
|
||||||
|
"update.channel": {"label":"通道"},
|
||||||
|
"update.config": {"label":"配置","title":"重置为默认"},
|
||||||
|
"update.themes": {"label":"附加主题"},
|
||||||
|
"update.process": {"label":"进程","title":"重启"},
|
||||||
|
"update.hardware": {"label":"硬件","title":"重启"},
|
||||||
|
"update.firmware": {"label":"固件"},
|
||||||
|
"update.password": {"label":"密码"},
|
||||||
|
"update.timezone": {"label":"时区"},
|
||||||
|
"update.time": {"label":"时间"},
|
||||||
|
"update.hardware.audio": {"label":"音频"},
|
||||||
|
"update.hardware.bluetooth": {"label":"蓝牙"},
|
||||||
|
"update.hardware.trackpad": {"label":"触控板"},
|
||||||
|
"update.password.drive": {"label":"磁盘加密"},
|
||||||
|
"update.password.user": {"label":"用户"},
|
||||||
|
"update.channel.stable": {"label":"稳定版"},
|
||||||
|
"update.channel.rc": {"label":"候选版"},
|
||||||
|
"update.channel.edge": {"label":"尝鲜版"},
|
||||||
|
"update.channel.dev": {"label":"开发版"},
|
||||||
|
"trigger.appearance": {"label":"外观"},
|
||||||
|
"trigger.appearance.animations": {"label":"动画"},
|
||||||
|
"trigger.appearance.one-window-ratio": {"label":"单窗口比例"},
|
||||||
|
"trigger.appearance.theme-sync": {"label":"Herdr 主题同步"},
|
||||||
|
"trigger.appearance.top-bar": {"label":"菜单栏"},
|
||||||
|
"trigger.appearance.top-bar.show": {"label":"显示菜单栏"},
|
||||||
|
"trigger.appearance.top-bar.hide": {"label":"隐藏菜单栏"},
|
||||||
|
"trigger.appearance.top-bar.position": {"label":"菜单栏位置"},
|
||||||
|
"trigger.appearance.top-bar.position.top": {"label":"顶部"},
|
||||||
|
"trigger.appearance.top-bar.position.bottom": {"label":"底部"},
|
||||||
|
"trigger.appearance.top-bar.position.left": {"label":"左侧"},
|
||||||
|
"trigger.appearance.top-bar.position.right": {"label":"右侧"},
|
||||||
|
"trigger.appearance.top-bar.background": {"label":"菜单栏背景"},
|
||||||
|
"trigger.appearance.top-bar.background.opaque": {"label":"不透明"},
|
||||||
|
"trigger.appearance.top-bar.background.glassy": {"label":"毛玻璃"},
|
||||||
|
"trigger.appearance.top-bar.background.clear": {"label":"全透明"},
|
||||||
|
"trigger.appearance.top-bar.indicators": {"label":"菜单栏指示"},
|
||||||
|
"trigger.appearance.top-bar.indicators.battery-percentage": {"label":"电量百分比"},
|
||||||
|
"trigger.appearance.top-bar.indicators.clock": {"label":"时钟"},
|
||||||
|
"trigger.appearance.window-corners": {"label":"窗口圆角"},
|
||||||
|
"trigger.appearance.window-corners.theme": {"label":"跟随主题"},
|
||||||
|
"trigger.appearance.window-corners.rounded": {"label":"圆角"},
|
||||||
|
"trigger.appearance.window-corners.sharp": {"label":"直角"},
|
||||||
|
"trigger.appearance.window-gaps": {"label":"窗口间距"},
|
||||||
|
"trigger.appearance.window-transparency": {"label":"窗口透明度"},
|
||||||
|
"trigger.appearance.window-transparency.opaque": {"label":"不透明"},
|
||||||
|
"trigger.appearance.window-transparency.glassy": {"label":"毛玻璃"},
|
||||||
|
"trigger.appearance.window-transparency.transparent": {"label":"透明"},
|
||||||
|
"trigger.appearance.workspace-layout": {"label":"工作区布局","title":"默认工作区布局"},
|
||||||
|
"trigger.appearance.workspace-layout.dwindle": {"label":"平铺"},
|
||||||
|
"trigger.appearance.workspace-layout.scrolling": {"label":"滚动"},
|
||||||
|
"trigger.appearance.workspace-layout.floating": {"label":"浮动"},
|
||||||
|
"trigger.clipboard-history": {"label":"剪贴板历史"},
|
||||||
|
"setup.typing": {"label":"输入"},
|
||||||
|
"setup.typing.keyboard": {"label":"键盘布局"},
|
||||||
|
"setup.typing.input": {"label":"输入法"},
|
||||||
|
"setup.region": {"label":"地区"},
|
||||||
|
"setup.region.global": {"label":"全球"},
|
||||||
|
"setup.region.china": {"label":"中国"},
|
||||||
|
"install.china": {"label":"国内应用"},
|
||||||
|
"install.china.wechat": {"label":"微信"},
|
||||||
|
"install.china.feishu": {"label":"飞书"},
|
||||||
|
"install.china.dingtalk": {"label":"钉钉"},
|
||||||
|
"install.china.wemeet": {"label":"腾讯会议"},
|
||||||
|
"install.china.tencent-docs": {"label":"腾讯文档"},
|
||||||
|
"install.china.yuque": {"label":"语雀"},
|
||||||
|
"install.china.shimo": {"label":"石墨文档"},
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<action id="org.omarchy.thunderbolt.approve">
|
|
||||||
<description>Approve a connected Thunderbolt accessory</description>
|
|
||||||
<message>Authentication is required to approve this Thunderbolt accessory</message>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-thunderbolt-authorization-approve</annotate>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>auth_admin</allow_any>
|
|
||||||
<allow_inactive>auth_admin</allow_inactive>
|
|
||||||
<allow_active>auth_admin</allow_active>
|
|
||||||
</defaults>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<action id="org.omarchy.usb.approve">
|
|
||||||
<description>Trust a connected USB accessory on any port</description>
|
|
||||||
<message>Authentication is required to trust this USB accessory</message>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-usb-authorization-approve</annotate>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>auth_admin</allow_any>
|
|
||||||
<allow_inactive>auth_admin</allow_inactive>
|
|
||||||
<allow_active>auth_admin</allow_active>
|
|
||||||
</defaults>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[source.crates-io]
|
||||||
|
replace-with = "aliyun"
|
||||||
|
|
||||||
|
[source.aliyun]
|
||||||
|
registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
|
||||||
|
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
|
||||||
|
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
||||||
|
RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup
|
||||||
|
BUNDLE_MIRROR__ALL=https://mirrors.aliyun.com/rubygems/
|
||||||
|
MAVEN_ARGS=--global-settings /usr/share/omarchy/default/regions/cn/maven-settings.xml
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
:sources:
|
||||||
|
- https://mirrors.aliyun.com/rubygems/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<settings>
|
||||||
|
<mirrors>
|
||||||
|
<mirror>
|
||||||
|
<id>aliyun</id>
|
||||||
|
<mirrorOf>central</mirrorOf>
|
||||||
|
<url>https://maven.aliyun.com/repository/public</url>
|
||||||
|
</mirror>
|
||||||
|
</mirrors>
|
||||||
|
</settings>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Installed from the ISO's offline repository before enabling ArchLinuxCN.
|
||||||
|
archlinuxcn-keyring
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Server = https://mirror.omarchy.cn/$repo/os/$arch
|
||||||
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user