Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9b39710a2
|
||
|
|
defbf90e89 |
No files matched your search
@@ -124,6 +124,12 @@ Exceptions are allowed for migration and package-helper scripts where the helper
|
||||
- Do not add `aliases` to new menu entries. Aliases are reserved for
|
||||
established alternate names users already type, kept for compatibility.
|
||||
|
||||
# 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.
|
||||
|
||||
# Config Structure
|
||||
|
||||
- `config/` - default configs copied to `~/.config/`
|
||||
|
||||
@@ -24,6 +24,10 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||
|
||||
## Text
|
||||
|
||||
Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it.
|
||||
|
||||
## IPC
|
||||
|
||||
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
||||
|
||||
@@ -59,6 +59,7 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
|
||||
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
||||
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
||||
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
||||
GROUP_DESCRIPTIONS[language]="System language"
|
||||
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
||||
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
||||
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
||||
|
||||
@@ -85,7 +85,7 @@ finish() {
|
||||
fail() {
|
||||
if [[ $events == "true" ]]; then
|
||||
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
|
||||
echo "$1" >&2
|
||||
fi
|
||||
@@ -113,9 +113,9 @@ interruptible() {
|
||||
}
|
||||
|
||||
if omarchy-cmd-missing "$provider"; then
|
||||
say "Installing the $name CLI…"
|
||||
say "$(omarchy-text "Installing the %1 CLI…" "$name")"
|
||||
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
|
||||
mise use -g "$package" || exit 1
|
||||
fi
|
||||
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
|
||||
else
|
||||
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
||||
fi
|
||||
[[ -n $account_home ]] || fail "No $name account named $reauth."
|
||||
[[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")"
|
||||
|
||||
if [[ $account_home == "$HOME/.$provider" ]]; then
|
||||
say "Sign in to $name again in the browser window that opens."
|
||||
login || fail "The $name sign-in didn't finish."
|
||||
say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
|
||||
login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
# The first subscription signs in where the CLI always looks, through your
|
||||
# normal browser: nothing is signed in there yet that it could be mistaken for.
|
||||
if ! primary_signed_in "$provider"; then
|
||||
say "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
|
||||
# The first agent signed in on a machine with none chosen becomes the
|
||||
# default, so `omarchy agent` and the panel's starter prompts have one to
|
||||
@@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then
|
||||
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
|
||||
fi
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
finish "Signed in to $name."
|
||||
finish "$(omarchy-text "Signed in to %1." "$name")"
|
||||
else
|
||||
fail "The $name sign-in didn't finish."
|
||||
fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||
fi
|
||||
else
|
||||
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
||||
@@ -189,7 +189,7 @@ else
|
||||
fi
|
||||
|
||||
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
|
||||
cat <<EOF
|
||||
|
||||
@@ -201,7 +201,7 @@ Don't log out of the $name CLI to do this: logging out revokes the saved sign-in
|
||||
EOF
|
||||
fi
|
||||
|
||||
home=$(omarchy-agent-account-state begin "$provider") || fail "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
|
||||
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
||||
@@ -211,9 +211,9 @@ EOF
|
||||
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
if [[ $events == "true" ]]; then
|
||||
finish "Added $added."
|
||||
finish "$(omarchy-text "Added %1." "$added")"
|
||||
else
|
||||
finish "Added $added."
|
||||
finish "$(omarchy-text "Added %1." "$added")"
|
||||
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -73,6 +73,11 @@ class AccountError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def text(english: str, *args: object) -> str:
|
||||
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- paths
|
||||
|
||||
|
||||
@@ -335,12 +340,12 @@ def begin(provider: str) -> Path:
|
||||
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
||||
pending = pending.resolve()
|
||||
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
||||
raise AccountError(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)
|
||||
if not found["accountId"]:
|
||||
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:
|
||||
# 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"]:
|
||||
if account.get("accountId") == found["accountId"]:
|
||||
shutil.rmtree(pending, ignore_errors=True)
|
||||
raise AccountError(
|
||||
f"That's {account['label']} ({found['email'] or 'already added'}). "
|
||||
"Try again, and sign in as the other account in the private window."
|
||||
)
|
||||
raise AccountError(text(
|
||||
"That's %1 (%2). Try again, and sign in as the other account in the private window.",
|
||||
account["label"], found["email"] or text("already added"),
|
||||
))
|
||||
|
||||
label = label.strip() or found["email"].split("@")[0] or "Account"
|
||||
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]:
|
||||
label = label.strip()
|
||||
if not label:
|
||||
raise AccountError("Give the account a name")
|
||||
raise AccountError(text("Give the account a name"))
|
||||
with Locked(provider) as registry:
|
||||
account = find(registry, target)
|
||||
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)
|
||||
if not account.get("primary") and not account.get("home"):
|
||||
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)]
|
||||
account = find(registry, target)
|
||||
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["alert"] = ""
|
||||
if not account.get("primary"):
|
||||
@@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
||||
with Locked(provider) as registry:
|
||||
account = find(registry, target)
|
||||
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"):
|
||||
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)
|
||||
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
|
||||
# home, and rmtree removes a link without following it.
|
||||
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]:
|
||||
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:
|
||||
registry["switch"] = mode
|
||||
if threshold is not None:
|
||||
try:
|
||||
registry["threshold"] = max(50, min(100, int(threshold)))
|
||||
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"] = ""
|
||||
return dict(registry)
|
||||
|
||||
@@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str:
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
if days:
|
||||
return f"{days}d {hours}h"
|
||||
return text("%1d %2h", days, hours)
|
||||
if hours:
|
||||
return f"{hours}h {minutes}m"
|
||||
return f"{minutes}m"
|
||||
return text("%1h %2m", hours, minutes)
|
||||
return text("%1m", minutes)
|
||||
|
||||
|
||||
# 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"]
|
||||
if decision["action"] == "exhausted":
|
||||
soonest = decision["soonest"]
|
||||
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else ""
|
||||
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}")
|
||||
staying = text("Staying on %1.", source["label"])
|
||||
if soonest:
|
||||
staying = text("Staying on %1. %2 resets in %3.", source["label"], soonest[1]["label"], duration(soonest[0] - now))
|
||||
notify(text("All %1 accounts are over %2%", name, threshold), staying)
|
||||
return False
|
||||
|
||||
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":
|
||||
registry["active"] = target["id"]
|
||||
if not target.get("primary"):
|
||||
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
|
||||
notify(
|
||||
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"]],
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -31,9 +31,9 @@ active=$(jq -r '.active.label + (if (.active.plan // "") != "" then " (" + .acti
|
||||
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
|
||||
echo "New $name sessions already use $active."
|
||||
else
|
||||
message="Running sessions stay on $previous."
|
||||
message=$(omarchy-text "Running sessions stay on %1." "$previous")
|
||||
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.
|
||||
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
||||
fi
|
||||
@@ -22,6 +22,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@@ -32,6 +33,11 @@ from typing import Any
|
||||
|
||||
AGENT_ID = "claude"
|
||||
AGENT_NAME = "Claude Code"
|
||||
def text(english, *args):
|
||||
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||
|
||||
|
||||
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
|
||||
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
||||
PROBE_MIN_INTERVAL_SECONDS = 15
|
||||
@@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
||||
except urllib.error.HTTPError as error:
|
||||
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
||||
if error.code == 429:
|
||||
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
|
||||
f" (retry after {retry_after}s)" if retry_after else ""
|
||||
) + ". Local Claude Code stats are still shown."
|
||||
if retry_after:
|
||||
help_text = text("Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", retry_after)
|
||||
else:
|
||||
help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.")
|
||||
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}
|
||||
except Exception:
|
||||
# A transport failure reached no server at all — no route, no DNS. Any
|
||||
|
||||
@@ -6,8 +6,8 @@ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
|
||||
|
||||
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
||||
omarchy-brightness-keyboard-mute on
|
||||
omarchy-osd -i microphone-muted -m "Microphone muted"
|
||||
omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
|
||||
else
|
||||
omarchy-brightness-keyboard-mute off
|
||||
omarchy-osd -i microphone -m "Microphone on"
|
||||
omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
|
||||
fi
|
||||
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
|
||||
sinks_count=$(jq 'length' <<<"$sinks")
|
||||
|
||||
if (( sinks_count == 0 )); then
|
||||
omarchy-osd -m "No audio devices found"
|
||||
omarchy-osd -m "$(omarchy-text "No audio devices found")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@ fi
|
||||
|
||||
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"
|
||||
@@ -150,9 +150,9 @@ if [[ $charge_holding == "true" ]]; then
|
||||
threshold_label="${threshold_end}%"
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -23,7 +23,7 @@ SELECTION=$(slurp 2>/dev/null)
|
||||
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
||||
|
||||
if [[ -z $RESULT ]]; then
|
||||
omarchy-notification-send -g -u critical "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
|
||||
fi
|
||||
|
||||
@@ -33,4 +33,4 @@ fi
|
||||
# the notification history, and an unmarked copy would be retained by clipboard
|
||||
# history. Pasting still works; only the recorded copy is given up.
|
||||
printf '%s' "$RESULT" | wl-copy --sensitive
|
||||
omarchy-notification-send -g "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
|
||||
# it needs no recordings directory and never notifies.
|
||||
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
|
||||
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR"
|
||||
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -97,7 +97,7 @@ start_webcam_overlay() {
|
||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
||||
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
@@ -235,7 +235,9 @@ wf_audio_device() {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -424,7 +426,7 @@ stop_screenrecording() {
|
||||
|
||||
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
|
||||
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL
|
||||
omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted."
|
||||
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
|
||||
finalize_recording
|
||||
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
||||
@@ -434,7 +436,7 @@ stop_screenrecording() {
|
||||
# Generate a preview thumbnail from the first frame
|
||||
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
|
||||
|
||||
omarchy-notification-send "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}" \
|
||||
--exec mpv -- "$filename"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
mapfile -t devices < <(omarchy-capture-webcam-list)
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ TEXT=$(grim -g "$SELECTION" - | tesseract stdin stdout --oem 1 --psm 6 -l "${OMA
|
||||
[[ -z $TEXT ]] && exit 1
|
||||
|
||||
printf "%s" "$TEXT" | wl-copy
|
||||
omarchy-notification-send -g "Copied text from selection to clipboard"
|
||||
omarchy-notification-send -g "$(omarchy-text "Copied text from selection to clipboard")"
|
||||
@@ -18,7 +18,7 @@ copy_url() {
|
||||
|
||||
[[ -n $url ]] || return 1
|
||||
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() {
|
||||
|
||||
@@ -87,7 +87,7 @@ download_url() {
|
||||
|
||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
||||
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
||||
omarchy-notification-send -u critical -g "$(omarchy-text "No video found for download")" "$url"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -148,7 +148,7 @@ download_url() {
|
||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||
# is one discrete argument, so it never reaches a shell.
|
||||
omarchy-notification-send -g "Download complete" "$title" \
|
||||
omarchy-notification-send -g "$(omarchy-text "Download complete")" "$title" \
|
||||
-t 10000 --image "${preview:-$filepath}" \
|
||||
--exec mpv -- "$filepath" || true
|
||||
|
||||
@@ -160,7 +160,7 @@ download_url() {
|
||||
rm -f "$preview"
|
||||
) &
|
||||
else
|
||||
omarchy-notification-send -u critical -g "Download failed" "$url"
|
||||
omarchy-notification-send -u critical -g "$(omarchy-text "Download failed")" "$url"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -40,8 +40,8 @@ announce() {
|
||||
omarchy-notification-send \
|
||||
--urgency critical \
|
||||
--glyph "$CRASH_GLYPH" \
|
||||
"Process crashed: $comm" \
|
||||
"Click to diagnose with AI" \
|
||||
"$(omarchy-text "Process crashed: %1" "$comm")" \
|
||||
"$(omarchy-text "Click to diagnose with AI")" \
|
||||
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
||||
}
|
||||
|
||||
|
||||
@@ -48,4 +48,4 @@ fi
|
||||
|
||||
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
|
||||
|
||||
omarchy-notification-send -g $glyph "$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")"
|
||||
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
|
||||
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")
|
||||
local new_id
|
||||
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
|
||||
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
||||
fi
|
||||
|
||||
@@ -74,11 +74,11 @@ XML
|
||||
omarchy-restart-shell
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
omarchy-hook font-set "$font_name"
|
||||
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
|
||||
mapfile -t cores < <(omarchy-games-retro-cores)
|
||||
|
||||
if (( ${#cores[@]} == 0 )); then
|
||||
omarchy-notification-send -g "No RetroArch cores found" "/usr/lib/libretro"
|
||||
omarchy-notification-send -g "$(omarchy-text "No RetroArch cores found")" "/usr/lib/libretro"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -69,4 +69,4 @@ EOF
|
||||
chmod +x "$desktop_file"
|
||||
update-desktop-database "$desktop_dir" &>/dev/null || true
|
||||
|
||||
omarchy-notification-send -g "$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
|
||||
|
||||
case $1 in
|
||||
theme) LABEL="Corners follow the theme" ;;
|
||||
sharp) LABEL="Sharp corners for all themes" ;;
|
||||
rounded) LABEL="Rounded corners for all themes" ;;
|
||||
theme) LABEL=$(omarchy-text "Corners follow the theme") ;;
|
||||
sharp) LABEL=$(omarchy-text "Sharp corners for all themes") ;;
|
||||
rounded) LABEL=$(omarchy-text "Rounded corners for all themes") ;;
|
||||
*)
|
||||
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
|
||||
exit 1
|
||||
|
||||
@@ -16,7 +16,7 @@ wake() {
|
||||
on() {
|
||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||
omarchy-hyprland-toggle $TOGGLE off
|
||||
omarchy-notification-send -g "Laptop display enabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Laptop display enabled")"
|
||||
fi
|
||||
|
||||
wake
|
||||
@@ -24,25 +24,25 @@ on() {
|
||||
|
||||
off() {
|
||||
if [[ -z $INTERNAL ]]; then
|
||||
omarchy-notification-send -g "No laptop display found"
|
||||
omarchy-notification-send -g "$(omarchy-text "No laptop display found")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The name is written into generated Lua below, so only a plain connector
|
||||
# name may pass; anything else could execute on the next reload.
|
||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
||||
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
||||
omarchy-notification-send -g "Laptop display disabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Laptop display disabled")"
|
||||
hyprctl reload
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
|
||||
|
||||
on() {
|
||||
if [[ -z $EXTERNAL ]]; then
|
||||
omarchy-notification-send -g "No external monitors found for mirror"
|
||||
omarchy-notification-send -g "$(omarchy-text "No external monitors found for mirror")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -26,7 +26,7 @@ on() {
|
||||
# names may pass; a user-created headless output can carry any name.
|
||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -35,7 +35,7 @@ on() {
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
|
||||
omarchy-notification-send -g "Mirroring enabled ($EXTERNAL)"
|
||||
omarchy-notification-send -g "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
|
||||
hyprctl reload
|
||||
fi
|
||||
}
|
||||
@@ -43,7 +43,7 @@ on() {
|
||||
off() {
|
||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||
omarchy-hyprland-toggle $TOGGLE off
|
||||
omarchy-notification-send -g "Extended mode restored"
|
||||
omarchy-notification-send -g "$(omarchy-text "Extended mode restored")"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
# omarchy:summary=Toggle single-window square aspect ratio.
|
||||
|
||||
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
||||
on) omarchy-notification-send -g "Enable single-window square aspect ratio" ;;
|
||||
off) omarchy-notification-send -g "Disable single-window square aspect ratio" ;;
|
||||
on) omarchy-notification-send -g "$(omarchy-text "Enable single-window square aspect ratio")" ;;
|
||||
off) omarchy-notification-send -g "$(omarchy-text "Disable single-window square aspect ratio")" ;;
|
||||
esac
|
||||
@@ -37,9 +37,9 @@ if (( $# == 0 )); then
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
opaque) LABEL="Opaque windows" ;;
|
||||
transparent) LABEL="Transparent windows" ;;
|
||||
glassy) LABEL="Glassy windows" ;;
|
||||
opaque) LABEL=$(omarchy-text "Opaque windows") ;;
|
||||
transparent) LABEL=$(omarchy-text "Transparent windows") ;;
|
||||
glassy) LABEL=$(omarchy-text "Glassy windows") ;;
|
||||
*)
|
||||
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
|
||||
exit 1
|
||||
|
||||
@@ -40,14 +40,14 @@ notify_missing_width() {
|
||||
local key="$1"
|
||||
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() {
|
||||
local key="$1"
|
||||
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() {
|
||||
|
||||
@@ -32,4 +32,4 @@ printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
|
||||
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-shell -q omarchy.workspaces refresh
|
||||
omarchy-notification-send -g "New workspaces use the $LABEL layout"
|
||||
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
|
||||
|
||||
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
|
||||
# Cycled onto the default, a workspace goes back to following it, so a later
|
||||
# change of the default reaches it too.
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
||||
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
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE"
|
||||
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
|
||||
else
|
||||
# The mode is on screen but nothing will bring it back, and the next press
|
||||
# reads the mode from this file, so say so rather than counting it as set.
|
||||
omarchy-notification-send -g "Workspace layout set to $NEW_MODE but could not be saved"
|
||||
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1 but could not be saved" "$NEW_MODE")"
|
||||
fi
|
||||
|
||||
# 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
|
||||
omarchy-dictation-use superwhisper
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-notification-send -g "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
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
|
||||
@@ -33,4 +33,4 @@ systemctl --user enable --now voxtype.service
|
||||
|
||||
omarchy-dictation-use voxtype
|
||||
hyprctl reload >/dev/null
|
||||
omarchy-notification-send -g "Voxtype Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||
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
|
||||
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"
|
||||
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
|
||||
case $terminal in
|
||||
*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
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
|
||||
". as \$plugins
|
||||
| .[] | select($filter)
|
||||
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
||||
[[ -n $rows ]] || { omarchy-notification-send "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
|
||||
[[ -n $selection ]] || exit 0
|
||||
|
||||
@@ -33,7 +33,7 @@ else
|
||||
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@ set -e
|
||||
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
||||
sudo timedatectl set-timezone "$timezone"
|
||||
omarchy-shell -q omarchy.clock refresh
|
||||
omarchy-notification-send "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)
|
||||
|
||||
if (( pending_count == 1 )); then
|
||||
message="Click to run 1 pending migration."
|
||||
message=$(omarchy-text "Click to run 1 pending migration.")
|
||||
else
|
||||
message="Click to run $pending_count pending migrations."
|
||||
message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
|
||||
fi
|
||||
|
||||
# This runs from omarchy-migrate-notify.service after graphical-session.target,
|
||||
@@ -40,7 +40,7 @@ fi
|
||||
|
||||
# The shell keeps the click command with the toast, so this oneshot can hand the
|
||||
# invitation over and exit instead of staying activated until it is answered.
|
||||
omarchy-notification-send -u critical -g "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
|
||||
|
||||
# Reached when the notification could not be handed off, so fall back to telling
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
# omarchy:summary=Show the current time and date notification
|
||||
|
||||
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y · 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
|
||||
clone_complete=1
|
||||
omarchy-notification-send -g \
|
||||
"Editing Cloned Plugin" \
|
||||
"Original plugin has been replace by clone."
|
||||
"$(omarchy-text "Editing Cloned Plugin")" \
|
||||
"$(omarchy-text "Original plugin has been replace by clone.")"
|
||||
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
||||
if (( edit_clone )); then
|
||||
# Word-splitting is deliberate: EDITOR may carry flags.
|
||||
|
||||
@@ -1174,15 +1174,6 @@ run_provisioning() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if omarchy-pkg-present usbguard; then
|
||||
log_step "enrolling the owner's USB devices"
|
||||
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
|
||||
usb_authorization_provision_owner "$username"
|
||||
fi
|
||||
|
||||
log_step "enrolling the owner's Thunderbolt accessories"
|
||||
/usr/bin/omarchy-thunderbolt-authorization-admin owner
|
||||
|
||||
log_step "cleaning up provisioning state"
|
||||
cleanup_oem_state
|
||||
log_step "first-boot setup complete"
|
||||
|
||||
+15
-14
@@ -57,16 +57,16 @@ show_reminders() {
|
||||
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
||||
|
||||
if [[ -n $reminder_message ]]; then
|
||||
body+="$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
|
||||
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
|
||||
done < <(active_reminder_timers "$now")
|
||||
|
||||
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
|
||||
omarchy-notification-send -g "Upcoming reminders" "${body%$'\n'}"
|
||||
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ show_json() {
|
||||
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||
local now=$(date +%s)
|
||||
local count=0
|
||||
local tooltip="Set Reminder"
|
||||
local tooltip
|
||||
tooltip=$(omarchy-text "Set Reminder")
|
||||
|
||||
while IFS=$'\t' read -r timer next; do
|
||||
count=$((count + 1))
|
||||
@@ -91,7 +92,7 @@ show_json() {
|
||||
if [[ -n $reminder_message ]]; then
|
||||
label=$reminder_message
|
||||
else
|
||||
label="${reminder_minutes}-min reminder"
|
||||
label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
|
||||
fi
|
||||
|
||||
item_json=$(jq -cn \
|
||||
@@ -109,9 +110,9 @@ show_json() {
|
||||
done < <(active_reminder_timers "$now")
|
||||
|
||||
if ((count == 1)); then
|
||||
tooltip="1 reminder"
|
||||
tooltip=$(omarchy-text "1 reminder")
|
||||
elif ((count > 1)); then
|
||||
tooltip="$count reminders"
|
||||
tooltip=$(omarchy-text "%1 reminders" "$count")
|
||||
fi
|
||||
|
||||
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
|
||||
refresh_indicator
|
||||
omarchy-notification-send -g "All reminders have been cleared"
|
||||
omarchy-notification-send -g "$(omarchy-text "All reminders have been cleared")"
|
||||
}
|
||||
|
||||
usage() {
|
||||
@@ -176,7 +177,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
|
||||
fi
|
||||
|
||||
if [[ -z $message ]]; then
|
||||
message="Your ${minutes} minutes are up"
|
||||
message=$(omarchy-text "Your %1 minutes are up" "$minutes")
|
||||
fi
|
||||
|
||||
set_at=$(date +%s)
|
||||
@@ -184,18 +185,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
|
||||
unit="omarchy-reminder-${minutes}m-$set_at"
|
||||
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||
message_file="$reminder_dir/$unit.message"
|
||||
confirmation="You'll be reminded at $remind_at"
|
||||
confirmation_title="Reminder set for ${minutes} minutes"
|
||||
confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
|
||||
confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
|
||||
|
||||
mkdir -p "$reminder_dir"
|
||||
|
||||
if [[ -n $custom_message ]]; then
|
||||
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
|
||||
|
||||
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
|
||||
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() {
|
||||
local factory="$1"
|
||||
btrfs property set -ts "$factory" ro false
|
||||
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
|
||||
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
|
||||
btrfs property set -ts "$factory" ro true
|
||||
fail "could not reset Thunderbolt authorization in the factory baseline"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! scrub_factory_accounts "$factory"; then
|
||||
btrfs property set -ts "$factory" ro true
|
||||
@@ -493,14 +487,6 @@ stage_full_reset() {
|
||||
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
|
||||
fail "the factory snapshot predates provisioning support; cannot reset from it"
|
||||
|
||||
# A normal install's factory snapshot includes the original owner's USB
|
||||
# policy and enabled daemon. Keep input usable during the new owner's setup;
|
||||
# provisioning enrolls their devices before enabling USBGuard again.
|
||||
if [[ -f $next/usr/lib/systemd/system/usbguard.service ]]; then
|
||||
systemctl --root="$next" disable usbguard.service >>"$LOG_FILE" 2>&1 ||
|
||||
fail "could not disable USBGuard in the factory system (see $LOG_FILE)"
|
||||
fi
|
||||
|
||||
log "Scrubbing machine identity from the factory system"
|
||||
systemd-id128 new >"$next/etc/machine-id"
|
||||
rm -f "$next"/etc/ssh/ssh_host_*
|
||||
@@ -516,13 +502,6 @@ stage_full_reset() {
|
||||
scrub_factory_accounts "$next" ||
|
||||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
|
||||
|
||||
# Leave the new owner's input available before interactive enrollment. Clear
|
||||
# inherited accessory identities in both roots; never change the live daemon.
|
||||
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
|
||||
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
|
||||
fail "could not reset Thunderbolt authorization in the staged root"
|
||||
fi
|
||||
|
||||
# @factory itself survives the wipe as the baseline for future resets. If it
|
||||
# came from a normal install it still holds the seller's account and
|
||||
# /etc/shadow, which the new wheel user could mount and read — and a second
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
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
|
||||
omarchy-hyprland-window-close-all
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# scope cannot terminate it before it runs.
|
||||
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
|
||||
|
||||
omarchy-osd -i reboot -m "Rebooting" -d 5000
|
||||
omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
|
||||
|
||||
omarchy-state clear re*-required
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
if [[ ${1:-} == "--inhibited" ]]; then
|
||||
# Close windows in the background so they cannot delay power-off.
|
||||
(
|
||||
omarchy-osd -i shutdown -m "Shutting down" -d 5000
|
||||
omarchy-osd -i shutdown -m "$(omarchy-text "Shutting down")" -d 5000
|
||||
omarchy-state clear re*-required
|
||||
omarchy-hyprland-window-close-all
|
||||
) &
|
||||
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
|
||||
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
||||
"$0" --inhibited || {
|
||||
status=$?
|
||||
omarchy-notification-send -u critical "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"
|
||||
}
|
||||
else
|
||||
|
||||
@@ -98,8 +98,8 @@ report_unsecured() {
|
||||
"$1" >&2
|
||||
|
||||
omarchy-notification-send -u critical -g \
|
||||
"Screen did not lock before suspend" \
|
||||
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
|
||||
"$(omarchy-text "Screen did not lock before suspend")" \
|
||||
"$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ claim_path() {
|
||||
announce() {
|
||||
local path="$1"
|
||||
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
|
||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||
|
||||
@@ -27,8 +27,8 @@ if ((${#files[@]} == 0)); then
|
||||
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
||||
|
||||
if ((${status:-0} > 1)); then
|
||||
omarchy-notification-send -g "" -u critical "Could not send to $name" \
|
||||
"The file chooser did not open"
|
||||
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
|
||||
"$(omarchy-text "The file chooser did not open")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -43,8 +43,8 @@ else
|
||||
fi
|
||||
|
||||
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
||||
omarchy-notification-send -g "" "Sent to $name" "$what"
|
||||
omarchy-notification-send -g "" "$(omarchy-text "Sent to %1" "$name")" "$what"
|
||||
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
|
||||
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
|
||||
enable)
|
||||
rm -f "$toggle"
|
||||
omarchy-notification-send "Background Intros" "Enabled"
|
||||
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Enabled")"
|
||||
;;
|
||||
disable)
|
||||
mkdir -p "$(dirname "$toggle")"
|
||||
touch "$toggle"
|
||||
omarchy-notification-send "Background Intros" "Disabled"
|
||||
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Disabled")"
|
||||
;;
|
||||
toggle)
|
||||
if [[ -f $toggle ]]; then
|
||||
|
||||
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
|
||||
TOTAL=${#BACKGROUNDS[@]}
|
||||
|
||||
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
|
||||
# Get current background from symlink
|
||||
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
||||
|
||||
@@ -35,5 +35,5 @@ fi
|
||||
|
||||
# Now remove the theme directory for THEME_NAME
|
||||
rm -rf "$THEME_PATH"
|
||||
omarchy-notification-send "Theme removed" "$THEME_NAME" || true
|
||||
omarchy-notification-send "$(omarchy-text "Theme removed")" "$THEME_NAME" || true
|
||||
echo "Removed $THEME_NAME"
|
||||
@@ -190,7 +190,7 @@ set_theme_background() {
|
||||
|
||||
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_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
|
||||
return
|
||||
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
|
||||
|
||||
if omarchy-hyprland-toggle-enabled no-animations; then
|
||||
omarchy-notification-send -g "Animations disabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Animations disabled")"
|
||||
else
|
||||
omarchy-notification-send -g "Animations enabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Animations enabled")"
|
||||
fi
|
||||
@@ -11,8 +11,8 @@ omarchy-toggle crash-capture-off
|
||||
# starting only settles this session; the next login reads the flag itself.
|
||||
if omarchy-toggle-enabled crash-capture-off; then
|
||||
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
|
||||
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
|
||||
@@ -5,7 +5,7 @@
|
||||
omarchy-toggle screensaver-off
|
||||
|
||||
if omarchy-toggle-enabled screensaver-off; then
|
||||
omarchy-notification-send -g "Screensaver disabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Screensaver disabled")"
|
||||
else
|
||||
omarchy-notification-send -g "Screensaver enabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Screensaver enabled")"
|
||||
fi
|
||||
@@ -5,7 +5,7 @@
|
||||
omarchy-toggle suspend-off
|
||||
|
||||
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
|
||||
omarchy-notification-send -g "Suspend now available in system menu"
|
||||
omarchy-notification-send -g "$(omarchy-text "Suspend now available in system menu")"
|
||||
fi
|
||||
@@ -5,7 +5,7 @@
|
||||
omarchy-toggle herdr-theme-sync
|
||||
|
||||
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
|
||||
omarchy-notification-send -g "Herdr theme sync disabled"
|
||||
omarchy-notification-send -g "$(omarchy-text "Herdr theme sync disabled")"
|
||||
fi
|
||||
@@ -193,14 +193,14 @@ main() {
|
||||
output=$(output_path "$input" "$format" "$resolution")
|
||||
|
||||
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"
|
||||
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
|
||||
transcode_picture "$input" "$format" "$resolution" "$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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
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
|
||||
update-desktop-database "$DESKTOP_DIR" &>/dev/null
|
||||
@@ -1420,7 +1420,7 @@ launch_windows() {
|
||||
if ! priv up_wait; then
|
||||
echo "❌ Failed to start Windows VM!"
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ import sys
|
||||
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():
|
||||
# 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.
|
||||
@@ -64,7 +69,7 @@ def choose(title, catalog, selected, kind, overrides=None):
|
||||
if result.returncode == 1:
|
||||
return None
|
||||
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):
|
||||
@@ -78,7 +83,7 @@ def input_catalog(items):
|
||||
|
||||
def input_items(items, selected, overrides=None):
|
||||
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.
|
||||
kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected]
|
||||
current = dict(items)
|
||||
@@ -93,7 +98,7 @@ def set_inputs(group, layout, before, after):
|
||||
setup.run(["fcitx5-remote", "-c"])
|
||||
setup.live_set(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:
|
||||
setup.live_set(group, layout, before)
|
||||
raise
|
||||
@@ -103,7 +108,7 @@ def configure_inputs():
|
||||
group, layout, items = setup.live_group()
|
||||
catalog, available = input_catalog(items)
|
||||
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):
|
||||
@@ -112,7 +117,7 @@ def load_engines(names, catalog):
|
||||
if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode]
|
||||
if absent or any(name not in setup.PRESETS 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
|
||||
# running (by an update, say) needs a restart before it can be selected.
|
||||
setup.run(["omarchy-restart-xcompose"])
|
||||
@@ -149,7 +154,7 @@ def add_input(method):
|
||||
|
||||
def keyboard_values(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]
|
||||
# Preserve the installer's Latin-leading rule for desktop keybindings.
|
||||
if parts[0][0] in setup.NON_LATIN:
|
||||
@@ -176,19 +181,19 @@ def save_keyboard(selected):
|
||||
expected = [layout + (":" + variant if variant else "")
|
||||
for layout, variant in zip(layouts.split(","), variants.split(","))]
|
||||
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_variant = variants.split(",")[0]
|
||||
group_layout = first_layout + ("-" + first_variant if first_variant else "")
|
||||
primary = next((item[0] for item in before if item[0].startswith("keyboard-")), 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
|
||||
after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)]
|
||||
group_changed = True
|
||||
setup.live_set(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:
|
||||
if original is None:
|
||||
path.resolve().unlink(missing_ok=True)
|
||||
@@ -205,7 +210,7 @@ def save_keyboard(selected):
|
||||
def configure_keyboard():
|
||||
current = keyboard_selection()
|
||||
catalog = keyboard_catalog()
|
||||
choose("Keyboard Layouts", catalog, current, "keyboard")
|
||||
choose(text("Keyboard Layouts"), catalog, current, "keyboard")
|
||||
|
||||
|
||||
def apply_selection(kind, context, values):
|
||||
@@ -213,7 +218,7 @@ def apply_selection(kind, context, values):
|
||||
rows = picker_rows(catalog, [])
|
||||
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):
|
||||
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))
|
||||
if kind == "input":
|
||||
save_inputs(selected, context.get("overrides"))
|
||||
@@ -228,13 +233,13 @@ def main():
|
||||
try:
|
||||
add_input(sys.argv[2])
|
||||
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
|
||||
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 ""
|
||||
if kind not in ("input", "keyboard"):
|
||||
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:
|
||||
if applying:
|
||||
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
|
||||
@@ -243,7 +248,7 @@ def main():
|
||||
else:
|
||||
configure_keyboard()
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -212,6 +212,9 @@
|
||||
"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.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.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
||||
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
||||
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
||||
@@ -220,12 +223,8 @@
|
||||
"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.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.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.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"},
|
||||
@@ -344,12 +343,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.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.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.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.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"},
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// 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":"输入法"},
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=Offer approval for blocked Thunderbolt accessories
|
||||
After=graphical-session.target
|
||||
PartOf=graphical-session.target
|
||||
ConditionEnvironment=WAYLAND_DISPLAY
|
||||
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.enabled
|
||||
ConditionPathExists=|/etc/omarchy/thunderbolt-authorization.pending
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-watch
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=Ask before allowing new USB devices
|
||||
After=graphical-session.target dbus.socket
|
||||
Requires=dbus.socket
|
||||
PartOf=graphical-session.target
|
||||
ConditionEnvironment=OMARCHY_PATH
|
||||
ConditionEnvironment=WAYLAND_DISPLAY
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/bash -lc 'exec "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch"'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
@@ -56,6 +56,8 @@ or re-icon a row without re-declaring its action, and an overridden entry
|
||||
keeps its original position in the list. New ids append. A `root` entry is
|
||||
injected if neither file declares one.
|
||||
|
||||
When `LANG` starts with `zh_CN`, `default/omarchy/omarchy-menu.zh-cn.jsonc` is merged between the defaults and the user extension. It only retitles shipped ids, so a user extension still wins, and the language follows `/etc/locale.conf` instead of any file `omarchy update` rewrites.
|
||||
|
||||
The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc`
|
||||
(refreshed into `~/.config/`) documents the format in its header and ships
|
||||
only comments, so the default state adds nothing.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Thunderbolt authorization
|
||||
|
||||
Omarchy uses Bolt for Thunderbolt/USB4 PCIe authorization. USB functions remain separate and need USB authorization. The end-user controls and firmware limitations are documented in [the Security manual](../manual/48-security.md#thunderbolt-device-authorization).
|
||||
|
||||
## Policy and services
|
||||
|
||||
Bolt's default IOMMU policy can enroll new devices without asking. Omarchy keeps `AuthMode=disabled` in `/var/lib/boltd/boltd.conf` and authorizes individual devices through Bolt's D-Bus API. This does not disable Bolt or prevent explicit authorization. Setting only `DefaultPolicy=manual` would still leave IOMMU auto-enrollment enabled.
|
||||
|
||||
The root `omarchy-thunderbolt-authorization.service` holds explicit consent in `/var/lib/omarchy/thunderbolt-authorization/policy.json`. Bolt can import devices authorized by firmware, so its database is not treated as consent. Initial setup captures currently connected accessories; subsequent enables preserve that inventory, including an initially empty inventory. Always-allow approvals authorize, read back the real status, enroll with manual policy, verify storage, and save consent. Trusted devices are restored even without an unlocked graphical session. No operation globally enables Bolt to authorize one device.
|
||||
|
||||
Omarchy records which Bolt entries it enrolled, including their store timestamps, separately from consent. Removing protection converts only matching, successfully approved entries from manual to automatic policy before restoring Bolt's global mode; this includes disconnected accessories. Existing manual records and externally recreated records retain their policies. Secure keys are kept. The handoff verifies the stored policies after restarting Bolt and checkpoints policies, firmware allowlists, configuration and services for rollback. Automatic policies resume Bolt's normal firmware allowlist behavior, so every controller must be online for a recoverable handoff. A completed handoff clears enrollment ownership while retaining the trust list for re-enabling.
|
||||
|
||||
The Bolt startup drop-in sets the persisted mode before hardware enumeration whenever `/etc/omarchy/thunderbolt-authorization.enabled` exists. The Omarchy service checks the live mode again. Firmware security `none` remains a bypass: software cannot prevent driver loading after firmware has already opened a PCIe tunnel. This produces a warning instead of disconnecting active storage.
|
||||
|
||||
Secure-capable devices require both a stored Bolt record and a saved key for automatic trust. An explicit permanent approval pins the current sysfs directory and key descriptor, seeds an empty kernel key before authorization, and verifies Bolt imported it before saving consent. Automatic reconciliation cannot initialize a missing key and retains its read-only sysfs sandbox. First activation is deferred when a captured secure-capable device lacks a saved Bolt key: the `.pending` marker starts a setup notification, while existing Bolt configuration and services remain untouched. Installation can finish; only explicit setup after safe disconnection clears the pending state. Initial identity capture uses Bolt's numeric device/vendor fallback when descriptive sysfs attributes are absent.
|
||||
|
||||
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. Both loops tick every 2 s without forking. While Linux sees no Thunderbolt controller and nothing is connected or boot-protected, the daemon reconciles Bolt every 10 s instead of every tick, still well inside a snapshot's 20 s lifetime; a controller appearing, a policy change or a boot recovery checkpoint restores the 2 s cadence at once. The user service skips an unchanged, fresh snapshot with no devices, warnings or requests in flight. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
|
||||
|
||||
The terminal dialog saves an approval intent; the background watcher submits it through `pkexec` to the fixed installed `/usr/bin/omarchy-thunderbolt-authorization-approve` helper. Polkit grants active local wheel users this individual approval action. The helper validates its arguments and the live connection, then independently verifies authorization and any saved trust before returning success. Root entrypoints use protected Bash startup and fixed installed libraries; they discard caller-supplied D-Bus addresses. A shared lock serializes setup, reconciliation, and approvals. General setup uses a separate administrator command invoked with `sudo` from the terminal. Blanket Bolt management requires administrator authentication while protection is enabled. These controls protect against unsolicited accessories, not malicious software already running as an administrator. Identifiers can be forged; Bolt's secure connection keys provide additional verification on supported hardware.
|
||||
|
||||
## Firmware boot access
|
||||
|
||||
The optional boot control applies only to online controllers with a firmware BootACL in `user` or `secure` mode. It captures connected accessories, changes stored policies to manual, clears each allowlist, and independently reads back the results. Reconciliation keeps firmware imports and returning controllers from silently restoring automatic boot permissions. Unsupported or offline controllers produce explicit warnings/errors.
|
||||
|
||||
Changes retain the previous policy and firmware lists. A failed transition restores matching state, or leaves a recovery checkpoint and reports failure. Device-authorization setup has a separate checkpoint that includes service state and firmware settings when boot protection was active. Restore commands are described in the manual. Firmware modified by another OS or an older snapshot is outside the current system's control; this is not an early-boot kernel authorization switch.
|
||||
|
||||
Deferred owner setup leaves ordinary Bolt behavior in place until the owner finishes provisioning. Factory reset disables the copied service and removes previous-owner trust, recovery data, Bolt identities, and keys from both staged and retained roots. It does not change the live daemon.
|
||||
|
||||
## Validation
|
||||
|
||||
Run policy, notification, rollback, deferred-owner, and real offline-systemctl tests with:
|
||||
|
||||
```sh
|
||||
bash test/shell.d/thunderbolt-authorization-test.sh
|
||||
```
|
||||
|
||||
The optional integration suite runs the installed `boltd` against upstream's UMockdev Thunderbolt fixture and a private D-Bus. Its hardware simulator uses Python/GObject; all Omarchy policy code and test assertions execute Bash. It needs `python-gobject`, `python-dbusmock`, `umockdev`, Bolt, and a Bolt source checkout (validated with 0.9.11):
|
||||
|
||||
```sh
|
||||
umockdev-wrapper env BOLT_TEST_SOURCE=/path/to/bolt bash test/shell.d/thunderbolt-authorization-test.sh
|
||||
```
|
||||
|
||||
It covers user/secure modes, IOMMU default denial, verified permanent and one-time approval, daemon restart, stale requests, firmware ACL restoration, and a separate Bash daemon publishing status after reconnecting to Bolt. Removal cases exercise the real setup and approval functions, automatic reconnection with retained secure keys, preservation of existing manual policies, disconnected accessories, and rollback of policies, firmware ACLs and the real private approval daemon after a setup failure. A service-control bridge manages only the fixture's private processes. These tests simulate kernel devices and firmware. They do not demonstrate physical DMA isolation or real firmware enforcement during boot. Systemd packaging, the installed approval helper, and the actual desktop Polkit rules require installed-guest validation; source-only dev linking does not install system units or rules.
|
||||
@@ -1,17 +0,0 @@
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id === "org.omarchy.thunderbolt.approve" &&
|
||||
subject.active && subject.local && subject.isInGroup("wheel")) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
// Bolt's vendor rule grants wheel passwordless management, including enabling
|
||||
// automatic approval of every IOMMU device. Keep blanket policy changes behind
|
||||
// administrator authentication; the narrow Omarchy action approves one device.
|
||||
if (action.id === "org.freedesktop.bolt.manage" && subject.user !== "root") {
|
||||
try {
|
||||
polkit.spawn(["/usr/bin/test", "-f", "/etc/omarchy/thunderbolt-authorization.enabled"]);
|
||||
return polkit.Result.AUTH_ADMIN;
|
||||
} catch (error) {
|
||||
// Protection is disabled: retain the packaged Bolt permission rules.
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id === "org.omarchy.usb.approve" &&
|
||||
subject.active && subject.local && subject.isInGroup("wheel")) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
[Service]
|
||||
# Runs before Bolt enumerates hardware, including D-Bus/udev activation.
|
||||
ExecStartPre=/usr/bin/omarchy-thunderbolt-authorization-admin guard
|
||||
@@ -1,26 +0,0 @@
|
||||
[Unit]
|
||||
Description=Restore approved Thunderbolt devices and handle new approvals
|
||||
Wants=bolt.service
|
||||
After=bolt.service polkit.service
|
||||
ConditionPathExists=/etc/omarchy/thunderbolt-authorization.enabled
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/omarchy-thunderbolt-authorization-daemon
|
||||
RuntimeDirectory=omarchy-thunderbolt-authorization
|
||||
RuntimeDirectoryMode=0755
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/omarchy/thunderbolt-authorization /run/lock
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -8,7 +8,5 @@ run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/locale.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/thunderbolt-authorization.sh"
|
||||
@@ -1,10 +0,0 @@
|
||||
echo "Configure Thunderbolt device authorization"
|
||||
|
||||
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
|
||||
|
||||
# The builder's accessories must not become the new owner's trust policy.
|
||||
# Keep ordinary Bolt behavior until interactive owner setup has finished.
|
||||
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
|
||||
/usr/bin/omarchy-thunderbolt-authorization-admin prepare
|
||||
systemctl enable omarchy-thunderbolt-authorization.service
|
||||
fi
|
||||
@@ -1,26 +0,0 @@
|
||||
echo "Configure USB device authorization"
|
||||
|
||||
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
|
||||
|
||||
# A deferred install has only the builder's peripherals attached. The owner
|
||||
# needs working input before there is an account or a session for approvals.
|
||||
if [[ -z ${OMARCHY_INSTALL_USER:-} ]]; then
|
||||
systemctl disable usbguard.service
|
||||
return 0
|
||||
fi
|
||||
|
||||
rules_file="${OMARCHY_USB_AUTHORIZATION_RULES_FILE:-/etc/usbguard/rules.conf}"
|
||||
daemon_config="${OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG:-/etc/usbguard/usbguard-daemon.conf}"
|
||||
|
||||
source "$OMARCHY_INSTALL/helpers/usb-authorization.sh"
|
||||
usb_authorization_require_secure_settings "$daemon_config"
|
||||
|
||||
if [[ ! -s $rules_file ]]; then
|
||||
usb_authorization_install_policy "$rules_file"
|
||||
fi
|
||||
|
||||
usb_authorization_add_user "$OMARCHY_INSTALL_USER"
|
||||
|
||||
# Installs are followed by a reboot, so enable the daemon without starting it
|
||||
# in the live ISO's chroot.
|
||||
systemctl enable usbguard.service
|
||||
Loaded 100 of 196 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user