Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b01ace6b | ||
|
|
848e9610c0 | ||
|
|
95d4e57db9 | ||
|
|
6da63c8942 | ||
|
|
562684f9fa | ||
|
|
b914d694f2 | ||
|
|
199a7f23fc | ||
|
|
1824bc3066 | ||
|
|
371fd05f03 | ||
|
|
557ba1d270 | ||
|
|
fdd457c0fa | ||
|
|
8798461461 | ||
|
|
4521dac023 | ||
|
|
c7402fe653 | ||
|
|
40642c0d35 | ||
|
|
ee358c251b | ||
|
|
b333308b2d | ||
|
|
8415c8d2a2 | ||
|
|
f273eed840 | ||
|
|
d62dff95b6 | ||
|
|
036e501e58 | ||
|
|
35f8eef1bc | ||
|
|
9a57690e7d | ||
|
|
2841563e6c | ||
|
|
cd73dc6cfd | ||
|
|
d2e4a95aed | ||
|
|
1abe693d73 | ||
|
|
fcda55944a | ||
|
|
22514b59f2 | ||
|
|
0f14a84934 | ||
|
|
cd0025b62f | ||
|
|
d9b970dd62 | ||
|
|
982290fa42 | ||
|
|
b6f2c1cef7 | ||
|
|
7b68fee056 | ||
|
|
9fe6a04715 | ||
|
|
402128b6a2 | ||
|
|
2fa6d0ecc5 | ||
|
|
83957145a7 | ||
|
|
0f8af9be30 | ||
|
|
db21abdef0 | ||
|
|
499f50ffd6 | ||
|
|
5fc227b06c | ||
|
|
051952600d | ||
|
|
0d8232b9db | ||
|
|
619979a716 | ||
|
|
1454e04b7f | ||
|
|
d53b24cfc7 | ||
|
|
f63ecb6697 | ||
|
|
2b2302a279 | ||
|
|
e236713d9f | ||
|
|
e590f22de6 | ||
|
|
df44695ea7 | ||
|
|
7f0c94d480 | ||
|
|
f9c36b2f3b | ||
|
|
a77c246d38 | ||
|
|
a3e40cb8b0 | ||
|
|
09da70c303 | ||
|
|
df338c4003 | ||
|
|
1f5d29a3ac | ||
|
|
41c491ae4c | ||
|
|
b268838479 | ||
|
|
bf08fedf8e | ||
|
|
7f9711ed9c | ||
|
|
8956964ee5 | ||
|
|
ebb0273584 | ||
|
|
3c3f23222b | ||
|
|
4e323342ca | ||
|
|
dff33f16e3 | ||
|
|
6fe83bfa47 | ||
|
|
dae9edef4c | ||
|
|
19e5941075 | ||
|
|
033b5ecd3d | ||
|
|
77c8e3f413 | ||
|
|
ab6804f8cc | ||
|
|
2ad241159e | ||
|
|
4eafef5025 | ||
|
|
e1471efa42 | ||
|
|
b51f36a9fe | ||
|
|
07d5f49963 | ||
|
|
5f6c803b21 | ||
|
|
56c8344fd2 | ||
|
|
465d4da567 | ||
|
|
151a530d15 | ||
|
|
da6045be3f | ||
|
|
23072924c4 | ||
|
|
f4bb97018e | ||
|
|
343501011f | ||
|
|
600734790c | ||
|
|
ab028e80eb | ||
|
|
a62771e2b4 | ||
|
|
32ebf6b85b | ||
|
|
b6bf851af4 | ||
|
|
c86af5cea5 | ||
|
|
83d158ad07 | ||
|
|
0c0f4881cb | ||
|
|
81df05533c | ||
|
|
8f7dd71fda | ||
|
|
96af8d4c02 | ||
|
|
8be14d5ac6 | ||
|
|
89fcd6a1b7 | ||
|
|
45d2e807db |
No files matched your search
@@ -98,8 +98,13 @@ Exceptions are allowed for migration and package-helper scripts where the helper
|
|||||||
|
|
||||||
# Region
|
# Region
|
||||||
|
|
||||||
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
|
- `/etc/omarchy/region` holds `cn`; no file means Global. `omarchy-region` prints the region and switches it (`omarchy region cn|global`); the profile lives in `default/regions/cn/`, its contract in `docs/regions.md`, user-facing docs in `manual/52-china.md`.
|
||||||
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create.
|
- `install/config/region.sh` is the one system leaf for both the ISO install (the `/etc/omarchy/region` marker the installer derives from the timezone) and the runtime switch (`OMARCHY_REGION`): the region file and the profile drop-ins it symlinks. Input methods are not part of a region: the installer's keyboard choice and `omarchy-setup-input` own them (`docs/input-methods.md`).
|
||||||
|
- `omarchy-apply-pacman` renders pacman for every caller: the channel templates, the profile's `pacman/mirrorlist-<channel>.prepend` ahead of the mirrorlist, and its `pacman.conf.append`. Nothing in `omarchy update` or migrations may change the region or `/etc/locale.conf`.
|
||||||
|
- Language is separate from region: `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`, the installer sets it from its first screen, and nothing keyed by region touches it.
|
||||||
|
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard.
|
||||||
|
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
|
||||||
|
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
|
||||||
|
|
||||||
# Config Structure
|
# Config Structure
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ The manual lives in [`manual/`](manual/), which is its authoritative source.
|
|||||||
- [Omarchy on...](manual/49-omarchy-on.md)
|
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||||
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||||
- [Unattended Installs](manual/51-unattended-installs.md)
|
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||||
|
- [China](manual/52-china.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
|||||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||||
|
|
||||||
|
## Text
|
||||||
|
|
||||||
|
Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it.
|
||||||
|
|
||||||
## IPC
|
## IPC
|
||||||
|
|
||||||
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Version=1.0
|
|
||||||
Name=Disk Usage
|
|
||||||
Exec=xdg-terminal-exec --app-id=TUI.float -e bash -c "dua i /"
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
|
||||||
Icon=disk-usage
|
|
||||||
StartupNotify=true
|
|
||||||
@@ -58,6 +58,7 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
|
|||||||
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
|
||||||
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
GROUP_DESCRIPTIONS[install]="Optional software installers"
|
||||||
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
|
||||||
|
GROUP_DESCRIPTIONS[language]="System language"
|
||||||
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
GROUP_DESCRIPTIONS[launch]="Application launchers"
|
||||||
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
|
||||||
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
GROUP_DESCRIPTIONS[migrate]="Migration runner"
|
||||||
@@ -73,6 +74,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
|
|||||||
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
GROUP_DESCRIPTIONS[power]="Power supply detection"
|
||||||
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
|
||||||
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
|
||||||
|
GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup"
|
||||||
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
|
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
|
||||||
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
|
||||||
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
GROUP_DESCRIPTIONS[remove]="Removal workflows"
|
||||||
|
|||||||
@@ -78,10 +78,22 @@ claude)
|
|||||||
command=(claude --permission-mode auto)
|
command=(claude --permission-mode auto)
|
||||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
;;
|
;;
|
||||||
|
dim)
|
||||||
|
# `dim exec` is the one-shot form; bare dim is the TUI and has no prompts to skip.
|
||||||
|
if [[ -n ${prompt:-} ]]; then
|
||||||
|
command=(dim exec "$prompt")
|
||||||
|
else
|
||||||
|
command=(dim)
|
||||||
|
fi
|
||||||
|
;;
|
||||||
grok)
|
grok)
|
||||||
command=(grok --permission-mode bypassPermissions)
|
command=(grok --permission-mode bypassPermissions)
|
||||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
;;
|
;;
|
||||||
|
kimi)
|
||||||
|
command=(kimi --auto)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||||
|
;;
|
||||||
openclaw)
|
openclaw)
|
||||||
# The launcher owns onboarding and the gateway dance: OpenClaw's terminal UI
|
# The launcher owns onboarding and the gateway dance: OpenClaw's terminal UI
|
||||||
# must attach to the running gateway (the embedded `openclaw chat` refuses to
|
# must attach to the running gateway (the embedded `openclaw chat` refuses to
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ finish() {
|
|||||||
fail() {
|
fail() {
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
printf '@@omarchy error %s\n' "$1"
|
printf '@@omarchy error %s\n' "$1"
|
||||||
omarchy-notification-send -g -u critical "Couldn't add the $name account" "$1"
|
omarchy-notification-send -g -u critical "$(omarchy-text "Couldn't add the %1 account" "$name")" "$1"
|
||||||
else
|
else
|
||||||
echo "$1" >&2
|
echo "$1" >&2
|
||||||
fi
|
fi
|
||||||
@@ -113,9 +113,9 @@ interruptible() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if omarchy-cmd-missing "$provider"; then
|
if omarchy-cmd-missing "$provider"; then
|
||||||
say "Installing the $name CLI…"
|
say "$(omarchy-text "Installing the %1 CLI…" "$name")"
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
mise use -g "$package" >/dev/null 2>&1 || fail "Installing the $name CLI didn't finish."
|
mise use -g "$package" >/dev/null 2>&1 || fail "$(omarchy-text "Installing the %1 CLI didn't finish." "$name")"
|
||||||
else
|
else
|
||||||
mise use -g "$package" || exit 1
|
mise use -g "$package" || exit 1
|
||||||
fi
|
fi
|
||||||
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
|
|||||||
else
|
else
|
||||||
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
||||||
fi
|
fi
|
||||||
[[ -n $account_home ]] || fail "No $name account named $reauth."
|
[[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")"
|
||||||
|
|
||||||
if [[ $account_home == "$HOME/.$provider" ]]; then
|
if [[ $account_home == "$HOME/.$provider" ]]; then
|
||||||
say "Sign in to $name again in the browser window that opens."
|
say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
|
||||||
login || fail "The $name sign-in didn't finish."
|
login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
else
|
else
|
||||||
say "Sign in again as this account in the private window that opens."
|
say "$(omarchy-text "Sign in again as this account in the private window that opens.")"
|
||||||
private_browser
|
private_browser
|
||||||
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish."
|
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
finish "Signed in to $name again."
|
finish "$(omarchy-text "Signed in to %1 again." "$name")"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The first subscription signs in where the CLI always looks, through your
|
# The first subscription signs in where the CLI always looks, through your
|
||||||
# normal browser: nothing is signed in there yet that it could be mistaken for.
|
# normal browser: nothing is signed in there yet that it could be mistaken for.
|
||||||
if ! primary_signed_in "$provider"; then
|
if ! primary_signed_in "$provider"; then
|
||||||
say "Sign in to $name in the browser window that opens."
|
say "$(omarchy-text "Sign in to %1 in the browser window that opens." "$name")"
|
||||||
if login && primary_signed_in "$provider"; then
|
if login && primary_signed_in "$provider"; then
|
||||||
# The first agent signed in on a machine with none chosen becomes the
|
# The first agent signed in on a machine with none chosen becomes the
|
||||||
# default, so `omarchy agent` and the panel's starter prompts have one to
|
# default, so `omarchy agent` and the panel's starter prompts have one to
|
||||||
@@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then
|
|||||||
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
|
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
|
||||||
fi
|
fi
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
finish "Signed in to $name."
|
finish "$(omarchy-text "Signed in to %1." "$name")"
|
||||||
else
|
else
|
||||||
fail "The $name sign-in didn't finish."
|
fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
||||||
@@ -189,7 +189,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
say "Sign in as the account you're adding in the private window that opens."
|
say "$(omarchy-text "Sign in as the account you're adding in the private window that opens.")"
|
||||||
else
|
else
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ Don't log out of the $name CLI to do this: logging out revokes the saved sign-in
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account."
|
home=$(omarchy-agent-account-state begin "$provider") || fail "$(omarchy-text "Couldn't set up a home for the new account.")"
|
||||||
|
|
||||||
private_browser
|
private_browser
|
||||||
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
||||||
@@ -211,9 +211,9 @@ EOF
|
|||||||
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
|
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
|
||||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||||
if [[ $events == "true" ]]; then
|
if [[ $events == "true" ]]; then
|
||||||
finish "Added $added."
|
finish "$(omarchy-text "Added %1." "$added")"
|
||||||
else
|
else
|
||||||
finish "Added $added."
|
finish "$(omarchy-text "Added %1." "$added")"
|
||||||
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ class AccountError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def text(english: str, *args: object) -> str:
|
||||||
|
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||||
|
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------- paths
|
# ------------------------------------------------------------------- paths
|
||||||
|
|
||||||
|
|
||||||
@@ -335,12 +340,12 @@ def begin(provider: str) -> Path:
|
|||||||
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
||||||
pending = pending.resolve()
|
pending = pending.resolve()
|
||||||
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
if pending.parent != (accounts_root() / provider / ".pending").resolve():
|
||||||
raise AccountError(f"{pending} is not a pending {PROVIDERS[provider]['name']} login")
|
raise AccountError(text("%1 is not a pending %2 login", pending, PROVIDERS[provider]['name']))
|
||||||
|
|
||||||
found = identity(provider, pending)
|
found = identity(provider, pending)
|
||||||
if not found["accountId"]:
|
if not found["accountId"]:
|
||||||
shutil.rmtree(pending, ignore_errors=True)
|
shutil.rmtree(pending, ignore_errors=True)
|
||||||
raise AccountError("The login didn't finish, so no account was added.")
|
raise AccountError(text("The login didn't finish, so no account was added."))
|
||||||
|
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
# Each home is asked who it is now, not trusted from when it was added: a
|
# Each home is asked who it is now, not trusted from when it was added: a
|
||||||
@@ -350,10 +355,10 @@ def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
|
|||||||
for account in registry["accounts"]:
|
for account in registry["accounts"]:
|
||||||
if account.get("accountId") == found["accountId"]:
|
if account.get("accountId") == found["accountId"]:
|
||||||
shutil.rmtree(pending, ignore_errors=True)
|
shutil.rmtree(pending, ignore_errors=True)
|
||||||
raise AccountError(
|
raise AccountError(text(
|
||||||
f"That's {account['label']} ({found['email'] or 'already added'}). "
|
"That's %1 (%2). Try again, and sign in as the other account in the private window.",
|
||||||
"Try again, and sign in as the other account in the private window."
|
account["label"], found["email"] or text("already added"),
|
||||||
)
|
))
|
||||||
|
|
||||||
label = label.strip() or found["email"].split("@")[0] or "Account"
|
label = label.strip() or found["email"].split("@")[0] or "Account"
|
||||||
account_id = unique_id(provider, registry, label)
|
account_id = unique_id(provider, registry, label)
|
||||||
@@ -395,11 +400,11 @@ def unique_id(provider: str, registry: dict[str, Any], label: str, keep: dict[st
|
|||||||
def rename(provider: str, target: str, label: str) -> dict[str, Any]:
|
def rename(provider: str, target: str, label: str) -> dict[str, Any]:
|
||||||
label = label.strip()
|
label = label.strip()
|
||||||
if not label:
|
if not label:
|
||||||
raise AccountError("Give the account a name")
|
raise AccountError(text("Give the account a name"))
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
new_id = unique_id(provider, registry, label, keep=account)
|
new_id = unique_id(provider, registry, label, keep=account)
|
||||||
if not account.get("primary") and not account.get("home"):
|
if not account.get("primary") and not account.get("home"):
|
||||||
account["home"] = str(account_home(provider, account))
|
account["home"] = str(account_home(provider, account))
|
||||||
@@ -428,7 +433,7 @@ def use(provider: str, target: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|||||||
target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
|
target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
registry["active"] = account["id"]
|
registry["active"] = account["id"]
|
||||||
registry["alert"] = ""
|
registry["alert"] = ""
|
||||||
if not account.get("primary"):
|
if not account.get("primary"):
|
||||||
@@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
|||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
account = find(registry, target)
|
account = find(registry, target)
|
||||||
if not account:
|
if not account:
|
||||||
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}")
|
raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
|
||||||
if account.get("primary"):
|
if account.get("primary"):
|
||||||
raise AccountError(f"{account['label']} is the login in {primary_home(provider)}; it can't be removed here.")
|
raise AccountError(text("%1 is the login in %2; it can't be removed here.", account["label"], primary_home(provider)))
|
||||||
home = account_home(provider, account)
|
home = account_home(provider, account)
|
||||||
if home_in_use(provider, home):
|
if home_in_use(provider, home):
|
||||||
raise AccountError(f"A running {PROVIDERS[provider]['name']} session uses {account['label']}. Quit it first, then remove the account.")
|
raise AccountError(text("A running %1 session uses %2. Quit it first, then remove the account.", PROVIDERS[provider]['name'], account["label"]))
|
||||||
# Only the account's own directory goes. Its links point into the primary
|
# Only the account's own directory goes. Its links point into the primary
|
||||||
# home, and rmtree removes a link without following it.
|
# home, and rmtree removes a link without following it.
|
||||||
if home.resolve().parent == (accounts_root() / provider).resolve():
|
if home.resolve().parent == (accounts_root() / provider).resolve():
|
||||||
@@ -474,14 +479,14 @@ def remove(provider: str, target: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
|
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
|
||||||
if mode not in SWITCH_MODES:
|
if mode not in SWITCH_MODES:
|
||||||
raise AccountError("Switch mode is manual or auto")
|
raise AccountError(text("Switch mode is manual or auto"))
|
||||||
with Locked(provider) as registry:
|
with Locked(provider) as registry:
|
||||||
registry["switch"] = mode
|
registry["switch"] = mode
|
||||||
if threshold is not None:
|
if threshold is not None:
|
||||||
try:
|
try:
|
||||||
registry["threshold"] = max(50, min(100, int(threshold)))
|
registry["threshold"] = max(50, min(100, int(threshold)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise AccountError("The threshold is a percentage between 50 and 100")
|
raise AccountError(text("The threshold is a percentage between 50 and 100"))
|
||||||
registry["alert"] = ""
|
registry["alert"] = ""
|
||||||
return dict(registry)
|
return dict(registry)
|
||||||
|
|
||||||
@@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str:
|
|||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
days, hours = divmod(hours, 24)
|
days, hours = divmod(hours, 24)
|
||||||
if days:
|
if days:
|
||||||
return f"{days}d {hours}h"
|
return text("%1d %2h", days, hours)
|
||||||
if hours:
|
if hours:
|
||||||
return f"{hours}h {minutes}m"
|
return text("%1h %2m", hours, minutes)
|
||||||
return f"{minutes}m"
|
return text("%1m", minutes)
|
||||||
|
|
||||||
|
|
||||||
# The whole switching policy, with no I/O: given the registry and the per
|
# The whole switching policy, with no I/O: given the registry and the per
|
||||||
@@ -659,21 +664,23 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool:
|
|||||||
threshold = registry["threshold"]
|
threshold = registry["threshold"]
|
||||||
if decision["action"] == "exhausted":
|
if decision["action"] == "exhausted":
|
||||||
soonest = decision["soonest"]
|
soonest = decision["soonest"]
|
||||||
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else ""
|
staying = text("Staying on %1.", source["label"])
|
||||||
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}")
|
if soonest:
|
||||||
|
staying = text("Staying on %1. %2 resets in %3.", source["label"], soonest[1]["label"], duration(soonest[0] - now))
|
||||||
|
notify(text("All %1 accounts are over %2%", name, threshold), staying)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target = decision["to"]
|
target = decision["to"]
|
||||||
headline = f"{source['label']} is at {round(decision['fromPeak'])}% of a {name} limit"
|
headline = text("%1 is at %2% of a %3 limit", source["label"], round(decision["fromPeak"]), name)
|
||||||
if decision["action"] == "switch":
|
if decision["action"] == "switch":
|
||||||
registry["active"] = target["id"]
|
registry["active"] = target["id"]
|
||||||
if not target.get("primary"):
|
if not target.get("primary"):
|
||||||
link_shared(provider, account_home(provider, target))
|
link_shared(provider, account_home(provider, target))
|
||||||
notify(headline, f"New {name} sessions now use {target['label']} ({round(decision['toPeak'])}%). Running sessions stay on {source['label']}.")
|
notify(headline, text("New %1 sessions now use %2 (%3%). Running sessions stay on %4.", name, target["label"], round(decision["toPeak"]), source["label"]))
|
||||||
return True
|
return True
|
||||||
notify(
|
notify(
|
||||||
headline,
|
headline,
|
||||||
f"{target['label']} is at {round(decision['toPeak'])}%. Click to switch new sessions to it.",
|
text("%1 is at %2%. Click to switch new sessions to it.", target["label"], round(decision["toPeak"])),
|
||||||
["omarchy-agent-account-use", provider, target["id"]],
|
["omarchy-agent-account-use", provider, target["id"]],
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ active=$(jq -r '.active.label + (if (.active.plan // "") != "" then " (" + .acti
|
|||||||
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
|
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
|
||||||
echo "New $name sessions already use $active."
|
echo "New $name sessions already use $active."
|
||||||
else
|
else
|
||||||
message="Running sessions stay on $previous."
|
message=$(omarchy-text "Running sessions stay on %1." "$previous")
|
||||||
echo "New $name sessions now use $active. $message"
|
echo "New $name sessions now use $active. $message"
|
||||||
omarchy-notification-send -g "New $name sessions now use $active" "$message"
|
omarchy-notification-send -g "$(omarchy-text "New %1 sessions now use %2" "$name" "$active")" "$message"
|
||||||
# The usage record's own limits describe the active account.
|
# The usage record's own limits describe the active account.
|
||||||
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
|
||||||
fi
|
fi
|
||||||
@@ -22,6 +22,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -32,6 +33,11 @@ from typing import Any
|
|||||||
|
|
||||||
AGENT_ID = "claude"
|
AGENT_ID = "claude"
|
||||||
AGENT_NAME = "Claude Code"
|
AGENT_NAME = "Claude Code"
|
||||||
|
def text(english, *args):
|
||||||
|
"""English with %1.. filled, in the session's language, from the shell's table."""
|
||||||
|
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
|
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
|
||||||
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
|
||||||
PROBE_MIN_INTERVAL_SECONDS = 15
|
PROBE_MIN_INTERVAL_SECONDS = 15
|
||||||
@@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
|||||||
except urllib.error.HTTPError as error:
|
except urllib.error.HTTPError as error:
|
||||||
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
retry_after = error.headers.get("retry-after", "") if error.headers else ""
|
||||||
if error.code == 429:
|
if error.code == 429:
|
||||||
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
|
if retry_after:
|
||||||
f" (retry after {retry_after}s)" if retry_after else ""
|
help_text = text("Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", retry_after)
|
||||||
) + ". Local Claude Code stats are still shown."
|
|
||||||
else:
|
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 is rate limiting checks right now. Local Claude Code stats are still shown.")
|
||||||
|
else:
|
||||||
|
help_text = text("Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", error.code)
|
||||||
return {"ok": False, "helpText": help_text}
|
return {"ok": False, "helpText": help_text}
|
||||||
except Exception:
|
except Exception:
|
||||||
# A transport failure reached no server at all — no route, no DNS. Any
|
# A transport failure reached no server at all — no route, no DNS. Any
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
|
|||||||
|
|
||||||
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
|
||||||
omarchy-brightness-keyboard-mute on
|
omarchy-brightness-keyboard-mute on
|
||||||
omarchy-osd -i microphone-muted -m "Microphone muted"
|
omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
|
||||||
else
|
else
|
||||||
omarchy-brightness-keyboard-mute off
|
omarchy-brightness-keyboard-mute off
|
||||||
omarchy-osd -i microphone -m "Microphone on"
|
omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
|
||||||
fi
|
fi
|
||||||
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
|
|||||||
sinks_count=$(jq 'length' <<<"$sinks")
|
sinks_count=$(jq 'length' <<<"$sinks")
|
||||||
|
|
||||||
if (( sinks_count == 0 )); then
|
if (( sinks_count == 0 )); then
|
||||||
omarchy-osd -m "No audio devices found"
|
omarchy-osd -m "$(omarchy-text "No audio devices found")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ fi
|
|||||||
|
|
||||||
level=$1
|
level=$1
|
||||||
|
|
||||||
omarchy-notification-send -g -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000
|
omarchy-notification-send -g -u critical "$(omarchy-text "Time to recharge!")" "$(omarchy-text "Battery is down to %1%" "$level")" -i battery-caution -t 30000
|
||||||
omarchy-hook battery-low "$level"
|
omarchy-hook battery-low "$level"
|
||||||
@@ -129,9 +129,9 @@ if [[ $charge_holding == "true" ]]; then
|
|||||||
threshold_label="${threshold_end}%"
|
threshold_label="${threshold_end}%"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · Holding at %2 · %3W / %4Wh" "$percentage" "$threshold_label" "$power_rate" "$capacity"
|
||||||
elif [[ $state == "charging" ]]; then
|
elif [[ $state == "charging" ]]; then
|
||||||
echo "Battery ${percentage}% · ${time_remaining} to full · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · %2 to full · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||||
else
|
else
|
||||||
echo "Battery ${percentage}% · ${time_remaining} left · ${power_rate}W / ${capacity}Wh"
|
omarchy-text "Battery %1% · %2 left · %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
|
||||||
fi
|
fi
|
||||||
@@ -23,7 +23,7 @@ SELECTION=$(slurp 2>/dev/null)
|
|||||||
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
|
||||||
|
|
||||||
if [[ -z $RESULT ]]; then
|
if [[ -z $RESULT ]]; then
|
||||||
omarchy-notification-send -g -u critical "No QR code found" "Select a region containing a QR code"
|
omarchy-notification-send -g -u critical "$(omarchy-text "No QR code found")" "$(omarchy-text "Select a region containing a QR code")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -33,4 +33,4 @@ fi
|
|||||||
# the notification history, and an unmarked copy would be retained by clipboard
|
# the notification history, and an unmarked copy would be retained by clipboard
|
||||||
# history. Pasting still works; only the recorded copy is given up.
|
# history. Pasting still works; only the recorded copy is given up.
|
||||||
printf '%s' "$RESULT" | wl-copy --sensitive
|
printf '%s' "$RESULT" | wl-copy --sensitive
|
||||||
omarchy-notification-send -g "QR code copied to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "QR code copied to clipboard")"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
|
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
|
||||||
|
|
||||||
if [[ ! -d $OUTPUT_DIR ]]; then
|
if [[ ! -d $OUTPUT_DIR ]]; then
|
||||||
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR"
|
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ start_webcam_overlay() {
|
|||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -231,7 +231,7 @@ stop_screenrecording() {
|
|||||||
|
|
||||||
if pgrep -f "^gpu-screen-recorder" >/dev/null; then
|
if pgrep -f "^gpu-screen-recorder" >/dev/null; then
|
||||||
pkill -9 -f "^gpu-screen-recorder"
|
pkill -9 -f "^gpu-screen-recorder"
|
||||||
omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted."
|
omarchy-notification-send -u critical -t 5000 "$(omarchy-text "Screen recording error")" "$(omarchy-text "Recording process had to be force-killed. Video may be corrupted.")"
|
||||||
else
|
else
|
||||||
finalize_recording
|
finalize_recording
|
||||||
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
|
||||||
@@ -241,7 +241,7 @@ stop_screenrecording() {
|
|||||||
# Generate a preview thumbnail from the first frame
|
# Generate a preview thumbnail from the first frame
|
||||||
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
|
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
|
||||||
|
|
||||||
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
omarchy-notification-send "$(omarchy-text "Screen recording saved")" "$(omarchy-text "Open with Super + Alt + , (or click this)")" \
|
||||||
-t 10000 --image "${preview:-$filename}" \
|
-t 10000 --image "${preview:-$filename}" \
|
||||||
--exec mpv -- "$filename"
|
--exec mpv -- "$filename"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
mapfile -t devices < <(omarchy-capture-webcam-list)
|
mapfile -t devices < <(omarchy-capture-webcam-list)
|
||||||
if (( ${#devices[@]} == 0 )); then
|
if (( ${#devices[@]} == 0 )); then
|
||||||
omarchy-notification-send "No webcam devices found" -u critical -t 3000
|
omarchy-notification-send "$(omarchy-text "No webcam devices found")" -u critical -t 3000
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ TEXT=$(grim -g "$SELECTION" - | tesseract stdin stdout --oem 1 --psm 6 -l "${OMA
|
|||||||
[[ -z $TEXT ]] && exit 1
|
[[ -z $TEXT ]] && exit 1
|
||||||
|
|
||||||
printf "%s" "$TEXT" | wl-copy
|
printf "%s" "$TEXT" | wl-copy
|
||||||
omarchy-notification-send -g "Copied text from selection to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "Copied text from selection to clipboard")"
|
||||||
@@ -18,7 +18,7 @@ copy_url() {
|
|||||||
|
|
||||||
[[ -n $url ]] || return 1
|
[[ -n $url ]] || return 1
|
||||||
printf '%s' "$url" | wl-copy --type text/plain
|
printf '%s' "$url" | wl-copy --type text/plain
|
||||||
omarchy-notification-send -g "URL copied to clipboard"
|
omarchy-notification-send -g "$(omarchy-text "URL copied to clipboard")"
|
||||||
}
|
}
|
||||||
|
|
||||||
reply_copied() {
|
reply_copied() {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ download_url() {
|
|||||||
|
|
||||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
||||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
||||||
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
omarchy-notification-send -u critical -g "$(omarchy-text "No video found for download")" "$url"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ download_url() {
|
|||||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||||
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||||
# is one discrete argument, so it never reaches a shell.
|
# is one discrete argument, so it never reaches a shell.
|
||||||
omarchy-notification-send -g "Download complete" "$title" \
|
omarchy-notification-send -g "$(omarchy-text "Download complete")" "$title" \
|
||||||
-t 10000 --image "${preview:-$filepath}" \
|
-t 10000 --image "${preview:-$filepath}" \
|
||||||
--exec mpv -- "$filepath" || true
|
--exec mpv -- "$filepath" || true
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ download_url() {
|
|||||||
rm -f "$preview"
|
rm -f "$preview"
|
||||||
) &
|
) &
|
||||||
else
|
else
|
||||||
omarchy-notification-send -u critical -g "Download failed" "$url"
|
omarchy-notification-send -u critical -g "$(omarchy-text "Download failed")" "$url"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ announce() {
|
|||||||
omarchy-notification-send \
|
omarchy-notification-send \
|
||||||
--urgency critical \
|
--urgency critical \
|
||||||
--glyph "$CRASH_GLYPH" \
|
--glyph "$CRASH_GLYPH" \
|
||||||
"Process crashed: $comm" \
|
"$(omarchy-text "Process crashed: %1" "$comm")" \
|
||||||
"Click to diagnose with AI" \
|
"$(omarchy-text "Click to diagnose with AI")" \
|
||||||
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Set and launch the default coding agent
|
# omarchy:summary=Set and launch the default coding agent
|
||||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush|cursor-agent|muse]
|
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush|cursor-agent|muse|dim|kimi]
|
||||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
||||||
|
|
||||||
installing=false
|
installing=false
|
||||||
@@ -31,7 +31,9 @@ ori | openrouter) agent="ori"; name="Ori"; agent_package="github:OpenRouterLabs/
|
|||||||
claude | claude-code) agent="claude"; name="Claude Code" ;;
|
claude | claude-code) agent="claude"; name="Claude Code" ;;
|
||||||
codex) agent="codex"; name="Codex" ;;
|
codex) agent="codex"; name="Codex" ;;
|
||||||
crush) agent="crush"; name="Crush" ;;
|
crush) agent="crush"; name="Crush" ;;
|
||||||
|
dim | dimcode | dim-agent) agent="dim"; name="Dim Agent"; agent_package="npm:dimcode" ;;
|
||||||
grok) agent="grok"; name="Grok" ;;
|
grok) agent="grok"; name="Grok" ;;
|
||||||
|
kimi | kimi-code) agent="kimi"; name="Kimi Code"; agent_package="npm:@moonshot-ai/kimi-code" ;;
|
||||||
openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-cli" ;;
|
openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-cli" ;;
|
||||||
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
|
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
|
||||||
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
|
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
|
||||||
@@ -43,7 +45,7 @@ muse | muse-code | musecode)
|
|||||||
;;
|
;;
|
||||||
cursor | cursor-agent) agent="cursor-agent"; name="Cursor CLI" ;;
|
cursor | cursor-agent) agent="cursor-agent"; name="Cursor CLI" ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush|cursor-agent|muse>"
|
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush|cursor-agent|muse|dim|kimi>"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ fi
|
|||||||
|
|
||||||
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
|
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default browser"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default browser" "$name")"
|
||||||
@@ -59,4 +59,4 @@ fi
|
|||||||
mkdir -p "$(dirname "$editor_file")"
|
mkdir -p "$(dirname "$editor_file")"
|
||||||
printf '%s\n' "$editor" >"$editor_file"
|
printf '%s\n' "$editor" >"$editor_file"
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default editor"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default editor" "$name")"
|
||||||
@@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <<EOF
|
|||||||
$desktop_id
|
$desktop_id
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
omarchy-notification-send -g $glyph "$name is now the default terminal"
|
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default terminal" "$name")"
|
||||||
@@ -169,7 +169,7 @@ set_terminal_size() {
|
|||||||
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
|
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
|
||||||
local new_id
|
local new_id
|
||||||
new_id="$(omarchy-notification-send \
|
new_id="$(omarchy-notification-send \
|
||||||
"Restart Foot to apply the new terminal font size" \
|
"$(omarchy-text "Restart Foot to apply the new terminal font size")" \
|
||||||
"${replace[@]}" -p 2>/dev/null)" || true
|
"${replace[@]}" -p 2>/dev/null)" || true
|
||||||
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ XML
|
|||||||
omarchy-restart-shell
|
omarchy-restart-shell
|
||||||
|
|
||||||
if pgrep -x ghostty; then
|
if pgrep -x ghostty; then
|
||||||
omarchy-notification-send -g "You must restart Ghostty to see font change"
|
omarchy-notification-send -g "$(omarchy-text "You must restart Ghostty to see font change")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if pgrep -x foot; then
|
if pgrep -x foot; then
|
||||||
omarchy-notification-send -g "You must restart Foot to see font change"
|
omarchy-notification-send -g "$(omarchy-text "You must restart Foot to see font change")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-hook font-set "$font_name"
|
omarchy-hook font-set "$font_name"
|
||||||
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
|
|||||||
mapfile -t cores < <(omarchy-games-retro-cores)
|
mapfile -t cores < <(omarchy-games-retro-cores)
|
||||||
|
|
||||||
if (( ${#cores[@]} == 0 )); then
|
if (( ${#cores[@]} == 0 )); then
|
||||||
omarchy-notification-send -g "No RetroArch cores found" "/usr/lib/libretro"
|
omarchy-notification-send -g "$(omarchy-text "No RetroArch cores found")" "/usr/lib/libretro"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,4 +69,4 @@ EOF
|
|||||||
chmod +x "$desktop_file"
|
chmod +x "$desktop_file"
|
||||||
update-desktop-database "$desktop_dir" &>/dev/null || true
|
update-desktop-database "$desktop_dir" &>/dev/null || true
|
||||||
|
|
||||||
omarchy-notification-send -g "$game_name installed" "Start it with Super + Space"
|
omarchy-notification-send -g "$(omarchy-text "%1 installed" "$game_name")" "$(omarchy-text "Start it with Super + Space")"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Match the Dell XPS 13 DX13260 with an Intel Panther Lake GPU.
|
||||||
|
|
||||||
|
omarchy-hw-match "DX13260" && omarchy-hw-intel-ptl
|
||||||
@@ -16,7 +16,7 @@ wake() {
|
|||||||
on() {
|
on() {
|
||||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||||
omarchy-hyprland-toggle $TOGGLE off
|
omarchy-hyprland-toggle $TOGGLE off
|
||||||
omarchy-notification-send -g "Laptop display enabled"
|
omarchy-notification-send -g "$(omarchy-text "Laptop display enabled")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wake
|
wake
|
||||||
@@ -24,25 +24,25 @@ on() {
|
|||||||
|
|
||||||
off() {
|
off() {
|
||||||
if [[ -z $INTERNAL ]]; then
|
if [[ -z $INTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No laptop display found"
|
omarchy-notification-send -g "$(omarchy-text "No laptop display found")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The name is written into generated Lua below, so only a plain connector
|
# The name is written into generated Lua below, so only a plain connector
|
||||||
# name may pass; anything else could execute on the next reload.
|
# name may pass; anything else could execute on the next reload.
|
||||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! omarchy-hyprland-monitor-external-active; then
|
if ! omarchy-hyprland-monitor-external-active; then
|
||||||
omarchy-notification-send -g "Can't disable the only active display"
|
omarchy-notification-send -g "$(omarchy-text "Can't disable the only active display")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
|
||||||
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
|
||||||
omarchy-notification-send -g "Laptop display disabled"
|
omarchy-notification-send -g "$(omarchy-text "Laptop display disabled")"
|
||||||
hyprctl reload
|
hyprctl reload
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
|
|||||||
|
|
||||||
on() {
|
on() {
|
||||||
if [[ -z $EXTERNAL ]]; then
|
if [[ -z $EXTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No external monitors found for mirror"
|
omarchy-notification-send -g "$(omarchy-text "No external monitors found for mirror")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $INTERNAL ]]; then
|
if [[ -z $INTERNAL ]]; then
|
||||||
omarchy-notification-send -g "No laptop monitor found to mirror"
|
omarchy-notification-send -g "$(omarchy-text "No laptop monitor found to mirror")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ on() {
|
|||||||
# names may pass; a user-created headless output can carry any name.
|
# names may pass; a user-created headless output can carry any name.
|
||||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
omarchy-notification-send -g "$(omarchy-text "Refusing unsafe monitor name")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -35,7 +35,7 @@ on() {
|
|||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||||
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
|
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
|
||||||
omarchy-notification-send -g "Mirroring enabled ($EXTERNAL)"
|
omarchy-notification-send -g "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
|
||||||
hyprctl reload
|
hyprctl reload
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ on() {
|
|||||||
off() {
|
off() {
|
||||||
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
if omarchy-hyprland-toggle-enabled $TOGGLE; then
|
||||||
omarchy-hyprland-toggle $TOGGLE off
|
omarchy-hyprland-toggle $TOGGLE off
|
||||||
omarchy-notification-send -g "Extended mode restored"
|
omarchy-notification-send -g "$(omarchy-text "Extended mode restored")"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
# omarchy:summary=Toggle single-window square aspect ratio.
|
# omarchy:summary=Toggle single-window square aspect ratio.
|
||||||
|
|
||||||
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
|
||||||
on) omarchy-notification-send -g "Enable single-window square aspect ratio" ;;
|
on) omarchy-notification-send -g "$(omarchy-text "Enable single-window square aspect ratio")" ;;
|
||||||
off) omarchy-notification-send -g "Disable single-window square aspect ratio" ;;
|
off) omarchy-notification-send -g "$(omarchy-text "Disable single-window square aspect ratio")" ;;
|
||||||
esac
|
esac
|
||||||
@@ -40,14 +40,14 @@ notify_missing_width() {
|
|||||||
local key="$1"
|
local key="$1"
|
||||||
local workspace="$2"
|
local workspace="$2"
|
||||||
|
|
||||||
omarchy-notification-send -g "No saved width found for $key on workspace $workspace" "Use Super + Alt + Home to save one for this workspace."
|
omarchy-notification-send -g "$(omarchy-text "No saved width found for %1 on workspace %2" "$key" "$workspace")" "Use Super + Alt + Home to save one for this workspace."
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_saved_width() {
|
notify_saved_width() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
local workspace="$2"
|
local workspace="$2"
|
||||||
|
|
||||||
omarchy-notification-send -g "Saved width for $key on workspace $workspace" "Restore using Super + Home on this workspace."
|
omarchy-notification-send -g "$(omarchy-text "Saved width for %1 on workspace %2" "$key" "$workspace")" "Restore using Super + Home on this workspace."
|
||||||
}
|
}
|
||||||
|
|
||||||
hypr_dispatch() {
|
hypr_dispatch() {
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKS
|
|||||||
|
|
||||||
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
|
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
|
||||||
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
|
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
|
||||||
omarchy-notification-send -g "Workspace layout set to $NEW_LAYOUT"
|
omarchy-notification-send -g "$(omarchy-text "Workspace layout set to %1" "$NEW_LAYOUT")"
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Configure or inspect the input method selected at installation
|
||||||
|
# omarchy:args=<status|configure [--seed|--defaults] [none|mozc|hangul|pinyin|chewing]>
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
python "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Show or switch the system language between English and Simplified Chinese
|
||||||
|
# omarchy:args=[en|zh]
|
||||||
|
# omarchy:examples=omarchy language | omarchy language zh
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
locale_conf=/etc/locale.conf
|
||||||
|
|
||||||
|
if (($# == 0)); then
|
||||||
|
lang=$(sed -n 's/^LANG=//p' "$locale_conf")
|
||||||
|
# locale.conf may quote the value.
|
||||||
|
lang=${lang#[\"\']}
|
||||||
|
printf '%s\n' "${lang%[\"\']}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case ${1:-} in
|
||||||
|
en) lang=en_US.UTF-8 ;;
|
||||||
|
zh) lang=zh_CN.UTF-8 ;;
|
||||||
|
*)
|
||||||
|
echo "Usage: omarchy-language [en|zh]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sudo sed -i "s/^#\(${lang/./\\.} UTF-8\)/\1/" /etc/locale.gen
|
||||||
|
sudo locale-gen
|
||||||
|
if grep -q '^LANG=' "$locale_conf"; then
|
||||||
|
sudo sed -i "s/^LANG=.*/LANG=$lang/" "$locale_conf"
|
||||||
|
else
|
||||||
|
sudo sed -i '$a\' "$locale_conf"
|
||||||
|
echo "LANG=$lang" | sudo tee -a "$locale_conf" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The language reaches only a new session, so offer one in the new language.
|
||||||
|
prompt=$(LANG=$lang omarchy-text "Log out now to switch the language?")
|
||||||
|
log_out=$(LANG=$lang omarchy-text "Log out now")
|
||||||
|
later=$(LANG=$lang omarchy-text "Later")
|
||||||
|
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$log_out" --negative "$later" "$prompt" 2>&1; then
|
||||||
|
omarchy-system-logout
|
||||||
|
else
|
||||||
|
LANG=$lang omarchy-text "Log back in to switch the language."
|
||||||
|
fi
|
||||||
@@ -11,5 +11,5 @@ if [[ -z $path ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-notification-send -u low "Editing config file" "$path"
|
omarchy-notification-send -u low "$(omarchy-text "Editing config file")" "$path"
|
||||||
exec omarchy-launch-editor "$path"
|
exec omarchy-launch-editor "$path"
|
||||||
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
|
|||||||
case $terminal in
|
case $terminal in
|
||||||
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
|
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
|
||||||
*)
|
*)
|
||||||
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty"
|
omarchy-notification-send -g ✋ "$(omarchy-text "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty")"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ dynamic_bindings() {
|
|||||||
static_bindings() {
|
static_bindings() {
|
||||||
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
|
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
|
||||||
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
|
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
|
||||||
|
echo "CTRL SHIFT,SPACE,Switch input language,exec,fcitx5-remote -t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Actions Omarchy binds to a second chord meant as an alternative, named one at
|
# Actions Omarchy binds to a second chord meant as an alternative, named one at
|
||||||
@@ -533,7 +534,7 @@ output_binding_records_uncached() {
|
|||||||
|
|
||||||
keybindings_cache_key() {
|
keybindings_cache_key() {
|
||||||
{
|
{
|
||||||
printf 'v14\n'
|
printf 'v15\n'
|
||||||
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
|
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
|
||||||
hyprctl binds 2>/dev/null
|
hyprctl binds 2>/dev/null
|
||||||
} | sha256sum | awk '{ print $1 }'
|
} | sha256sum | awk '{ print $1 }'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
|
|||||||
". as \$plugins
|
". as \$plugins
|
||||||
| .[] | select($filter)
|
| .[] | select($filter)
|
||||||
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
|
||||||
[[ -n $rows ]] || { omarchy-notification-send "No plugin to ${1}"; exit 0; }
|
[[ -n $rows ]] || { omarchy-notification-send "$(omarchy-text "No plugin to $1")"; exit 0; }
|
||||||
|
|
||||||
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
|
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
|
||||||
[[ -n $selection ]] || exit 0
|
[[ -n $selection ]] || exit 0
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ else
|
|||||||
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
|
||||||
|
|
||||||
if ((${status:-0} > 1)); then
|
if ((${status:-0} > 1)); then
|
||||||
omarchy-notification-send -g "" -u critical "Could not share" "The file chooser did not open"
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not share")" "$(omarchy-text "The file chooser did not open")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ set -e
|
|||||||
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
|
||||||
sudo timedatectl set-timezone "$timezone"
|
sudo timedatectl set-timezone "$timezone"
|
||||||
omarchy-shell -q omarchy.clock refresh
|
omarchy-shell -q omarchy.clock refresh
|
||||||
omarchy-notification-send "Timezone is now set to $timezone"
|
omarchy-notification-send "$(omarchy-text "Timezone is now set to %1" "$timezone")"
|
||||||
@@ -21,9 +21,9 @@ pending_migrations=$(omarchy-migrate --pending 2>/dev/null) || exit 0
|
|||||||
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
|
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
|
||||||
|
|
||||||
if (( pending_count == 1 )); then
|
if (( pending_count == 1 )); then
|
||||||
message="Click to run 1 pending migration."
|
message=$(omarchy-text "Click to run 1 pending migration.")
|
||||||
else
|
else
|
||||||
message="Click to run $pending_count pending migrations."
|
message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# This runs from omarchy-migrate-notify.service after graphical-session.target,
|
# This runs from omarchy-migrate-notify.service after graphical-session.target,
|
||||||
@@ -40,7 +40,7 @@ fi
|
|||||||
|
|
||||||
# The shell keeps the click command with the toast, so this oneshot can hand the
|
# The shell keeps the click command with the toast, so this oneshot can hand the
|
||||||
# invitation over and exit instead of staying activated until it is answered.
|
# invitation over and exit instead of staying activated until it is answered.
|
||||||
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
omarchy-notification-send -u critical -g "$(omarchy-text "Pending Omarchy Migrations")" "$message" \
|
||||||
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
||||||
|
|
||||||
# Reached when the notification could not be handed off, so fall back to telling
|
# Reached when the notification could not be handed off, so fall back to telling
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
# omarchy:summary=Show the current time and date notification
|
# omarchy:summary=Show the current time and date notification
|
||||||
|
|
||||||
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y · Week %V")"
|
omarchy-notification-send -g -u low "$(date +"%A %H:%M · %d %B %Y") · $(omarchy-text "Week %1" "$(date +%V)")"
|
||||||
+9
-3
@@ -5,11 +5,17 @@
|
|||||||
# omarchy:examples=omarchy pkg add jq ripgrep
|
# omarchy:examples=omarchy pkg add jq ripgrep
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
if omarchy-pkg-missing "$@"; then
|
# Only the missing ones: an installed package from no repository is a "target not found" to pacman -S.
|
||||||
|
missing=()
|
||||||
|
for pkg in "$@"; do
|
||||||
|
omarchy-pkg-missing "$pkg" && missing+=("$pkg")
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#missing[@]} > 0 )); then
|
||||||
if (( EUID == 0 )); then
|
if (( EUID == 0 )); then
|
||||||
pacman -S --noconfirm --needed -- "$@" || exit 1
|
pacman -S --noconfirm --needed -- "${missing[@]}" || exit 1
|
||||||
else
|
else
|
||||||
sudo pacman -S --noconfirm --needed -- "$@" || exit 1
|
sudo pacman -S --noconfirm --needed -- "${missing[@]}" || exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ done
|
|||||||
omarchy-plugin-enable "$new_id" >/dev/null
|
omarchy-plugin-enable "$new_id" >/dev/null
|
||||||
clone_complete=1
|
clone_complete=1
|
||||||
omarchy-notification-send -g \
|
omarchy-notification-send -g \
|
||||||
"Editing Cloned Plugin" \
|
"$(omarchy-text "Editing Cloned Plugin")" \
|
||||||
"Original plugin has been replace by clone."
|
"$(omarchy-text "Original plugin has been replace by clone.")"
|
||||||
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
echo "Cloned $source_id to $target_dir and switched to $new_id"
|
||||||
if (( edit_clone )); then
|
if (( edit_clone )); then
|
||||||
# Word-splitting is deliberate: EDITOR may carry flags.
|
# Word-splitting is deliberate: EDITOR may carry flags.
|
||||||
|
|||||||
@@ -599,6 +599,7 @@ keyboard_form() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
apply_keyboard "$keyboard"
|
apply_keyboard "$keyboard"
|
||||||
|
omarchy_persist_input_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm_reboot() {
|
confirm_reboot() {
|
||||||
|
|||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Show or switch the region this machine is set up for
|
||||||
|
# omarchy:args=[global|cn]
|
||||||
|
# omarchy:examples=omarchy region | omarchy region cn
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
region_file=/etc/omarchy/region
|
||||||
|
|
||||||
|
if (($# == 0)); then
|
||||||
|
if [[ ! -e $region_file ]]; then
|
||||||
|
echo "Global"
|
||||||
|
elif [[ $(<"$region_file") == "cn" ]]; then
|
||||||
|
echo "China"
|
||||||
|
else
|
||||||
|
echo "Unknown region in $region_file: $(<"$region_file")" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $1 != "global" && $1 != "cn" ]]; then
|
||||||
|
echo "Usage: omarchy-region [global|cn]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
channel=$(omarchy-version-channel)
|
||||||
|
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||||
|
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $1 == "cn" ]]; then
|
||||||
|
# Upgrade first: the keyring install below syncs new databases.
|
||||||
|
omarchy-update-pacman -Syu --noconfirm
|
||||||
|
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
|
||||||
|
# Trust ArchLinuxCN before the refresh upgrades through it.
|
||||||
|
rendered=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$rendered"' EXIT
|
||||||
|
omarchy-apply-pacman --render "$channel" "$rendered"
|
||||||
|
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
|
||||||
|
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
|
||||||
|
sudo pacman-key --populate archlinuxcn
|
||||||
|
omarchy-refresh-pacman "$channel"
|
||||||
|
else
|
||||||
|
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
|
||||||
|
omarchy-refresh-pacman "$channel"
|
||||||
|
omarchy-setup-claude-api --remove
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The developer mirrors reach only a new session, so offer one.
|
||||||
|
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")" 2>&1; then
|
||||||
|
omarchy-system-logout
|
||||||
|
else
|
||||||
|
omarchy-text "Log back in to switch the developer mirrors."
|
||||||
|
fi
|
||||||
+15
-14
@@ -57,16 +57,16 @@ show_reminders() {
|
|||||||
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
|
||||||
|
|
||||||
if [[ -n $reminder_message ]]; then
|
if [[ -n $reminder_message ]]; then
|
||||||
body+="$reminder_message in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
body+="$(omarchy-text "%1 in %2 (%3)" "$reminder_message" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||||
else
|
else
|
||||||
body+="${reminder_minutes}-min reminder in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n'
|
body+="$(omarchy-text "%1-min reminder in %2 (%3)" "$reminder_minutes" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
|
||||||
fi
|
fi
|
||||||
done < <(active_reminder_timers "$now")
|
done < <(active_reminder_timers "$now")
|
||||||
|
|
||||||
if [[ -z $body ]]; then
|
if [[ -z $body ]]; then
|
||||||
omarchy-notification-send -g "Upcoming reminders" "No outstanding reminders"
|
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "$(omarchy-text "No outstanding reminders")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Upcoming reminders" "${body%$'\n'}"
|
omarchy-notification-send -g "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,8 @@ show_json() {
|
|||||||
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||||
local now=$(date +%s)
|
local now=$(date +%s)
|
||||||
local count=0
|
local count=0
|
||||||
local tooltip="Set Reminder"
|
local tooltip
|
||||||
|
tooltip=$(omarchy-text "Set Reminder")
|
||||||
|
|
||||||
while IFS=$'\t' read -r timer next; do
|
while IFS=$'\t' read -r timer next; do
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
@@ -91,7 +92,7 @@ show_json() {
|
|||||||
if [[ -n $reminder_message ]]; then
|
if [[ -n $reminder_message ]]; then
|
||||||
label=$reminder_message
|
label=$reminder_message
|
||||||
else
|
else
|
||||||
label="${reminder_minutes}-min reminder"
|
label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
item_json=$(jq -cn \
|
item_json=$(jq -cn \
|
||||||
@@ -109,9 +110,9 @@ show_json() {
|
|||||||
done < <(active_reminder_timers "$now")
|
done < <(active_reminder_timers "$now")
|
||||||
|
|
||||||
if ((count == 1)); then
|
if ((count == 1)); then
|
||||||
tooltip="1 reminder"
|
tooltip=$(omarchy-text "1 reminder")
|
||||||
elif ((count > 1)); then
|
elif ((count > 1)); then
|
||||||
tooltip="$count reminders"
|
tooltip=$(omarchy-text "%1 reminders" "$count")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
|
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
|
||||||
@@ -129,7 +130,7 @@ clear_reminders() {
|
|||||||
|
|
||||||
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
|
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
|
||||||
refresh_indicator
|
refresh_indicator
|
||||||
omarchy-notification-send -g "All reminders have been cleared"
|
omarchy-notification-send -g "$(omarchy-text "All reminders have been cleared")"
|
||||||
}
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -176,7 +177,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $message ]]; then
|
if [[ -z $message ]]; then
|
||||||
message="Your ${minutes} minutes are up"
|
message=$(omarchy-text "Your %1 minutes are up" "$minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_at=$(date +%s)
|
set_at=$(date +%s)
|
||||||
@@ -184,18 +185,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
|
|||||||
unit="omarchy-reminder-${minutes}m-$set_at"
|
unit="omarchy-reminder-${minutes}m-$set_at"
|
||||||
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
|
||||||
message_file="$reminder_dir/$unit.message"
|
message_file="$reminder_dir/$unit.message"
|
||||||
confirmation="You'll be reminded at $remind_at"
|
confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
|
||||||
confirmation_title="Reminder set for ${minutes} minutes"
|
confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
|
||||||
|
|
||||||
mkdir -p "$reminder_dir"
|
mkdir -p "$reminder_dir"
|
||||||
|
|
||||||
if [[ -n $custom_message ]]; then
|
if [[ -n $custom_message ]]; then
|
||||||
printf "%s" "$custom_message" >"$message_file"
|
printf "%s" "$custom_message" >"$message_file"
|
||||||
confirmation_title="$custom_message in ${minutes} minutes"
|
confirmation_title=$(omarchy-text "%1 in %2 minutes" "$custom_message" "$minutes")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
|
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
|
||||||
bash -c 'omarchy-notification-send -g "Reminder" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file"
|
bash -c 'omarchy-notification-send -g "$(omarchy-text "Reminder")" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file"
|
||||||
|
|
||||||
refresh_indicator
|
refresh_indicator
|
||||||
omarchy-notification-send -g "$confirmation_title" "$confirmation"
|
omarchy-notification-send -g "$confirmation_title" "$confirmation"
|
||||||
@@ -24,6 +24,15 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
|||||||
rm -f ~/.local/bin/cursor-agent
|
rm -f ~/.local/bin/cursor-agent
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Only Omarchy's mise wrappers for Dim and Kimi are preinstalls.
|
||||||
|
for wrapper in "dim npm:dimcode" "kimi npm:@moonshot-ai/kimi-code"; do
|
||||||
|
read -r command package <<<"$wrapper"
|
||||||
|
if [[ -f ~/.local/bin/$command && ! -L ~/.local/bin/$command ]] &&
|
||||||
|
grep -qF "mise use -g --quiet \"$package\"" ~/.local/bin/$command; then
|
||||||
|
rm -f ~/.local/bin/$command
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Preserve a user-managed Muse launcher at the same path.
|
# Preserve a user-managed Muse launcher at the same path.
|
||||||
if [[ -f ~/.local/bin/muse && ! -L ~/.local/bin/muse ]] &&
|
if [[ -f ~/.local/bin/muse && ! -L ~/.local/bin/muse ]] &&
|
||||||
grep -Eq '^mise use -g .*"http:muse\[' ~/.local/bin/muse; then
|
grep -Eq '^mise use -g .*"http:muse\[' ~/.local/bin/muse; then
|
||||||
|
|||||||
@@ -2,9 +2,30 @@
|
|||||||
|
|
||||||
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
|
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
|
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
|
||||||
# instance would exit on arrival and the stale process would keep serving the
|
# instance would exit on arrival and the stale process would keep serving the
|
||||||
# old ~/.XCompose -- a restart that reports success and changes nothing.
|
# old ~/.XCompose -- a restart that reports success and changes nothing.
|
||||||
systemctl --user stop omarchy-fcitx5.service
|
systemctl --user stop omarchy-fcitx5.service
|
||||||
pkill -x fcitx5 >/dev/null 2>&1 || true
|
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
|
||||||
|
for attempt in {1..50}; do
|
||||||
|
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
|
||||||
|
# An activation concurrent with stop may have restarted the unit.
|
||||||
|
systemctl --user stop omarchy-fcitx5.service
|
||||||
|
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
|
||||||
|
for attempt in {1..50}; do
|
||||||
|
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then break; fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
|
||||||
|
echo "Fcitx did not stop; input service was not restarted." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
systemctl --user start omarchy-fcitx5.service
|
systemctl --user start omarchy-fcitx5.service
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Point Claude Code at a third-party API key and model, or remove that setup
|
||||||
|
# omarchy:args=[--remove]
|
||||||
|
# omarchy:examples=omarchy setup claude-api | omarchy setup claude-api --remove
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
settings="$HOME/.claude/settings.json"
|
||||||
|
keys=(ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN ANTHROPIC_MODEL ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL ANTHROPIC_DEFAULT_HAIKU_MODEL CLAUDE_CODE_SUBAGENT_MODEL)
|
||||||
|
|
||||||
|
keys_json=$(printf '%s\n' "${keys[@]}" | jq -R . | jq -s .)
|
||||||
|
|
||||||
|
# rewrite <file> <jq filter> [jq args...] replaces the file with a fresh 0600 copy.
|
||||||
|
rewrite() {
|
||||||
|
local file=$1 filter=$2 tmp
|
||||||
|
shift 2
|
||||||
|
mkdir -p "${file%/*}"
|
||||||
|
[[ -f $file ]] || echo '{}' >"$file"
|
||||||
|
tmp=$(mktemp "$file.XXXXXX")
|
||||||
|
jq --argjson keys "$keys_json" "$@" "$filter" "$file" >"$tmp" || { rm -f "$tmp"; exit 1; }
|
||||||
|
mv "$tmp" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ${1:-} == "--remove" ]]; then
|
||||||
|
if [[ -f $settings ]]; then
|
||||||
|
rewrite "$settings" 'if .env then .env |= delpaths($keys | map([.])) | if .env == {} then del(.env) end end'
|
||||||
|
fi
|
||||||
|
echo "Claude Code third-party API removed from $settings"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
provider=$(gum choose --header "Claude Code API provider" DeepSeek "Kimi Code" "GLM (Zhipu)" MiniMax Custom)
|
||||||
|
case "$provider" in
|
||||||
|
DeepSeek) base_url="https://api.deepseek.com/anthropic" model="deepseek-flash[1m]" key_url="https://platform.deepseek.com/api_keys" ;;
|
||||||
|
"Kimi Code") base_url="https://api.kimi.com/coding/" model="kimi-for-coding" key_url="https://www.kimi.com/code" ;;
|
||||||
|
"GLM (Zhipu)") base_url="https://open.bigmodel.cn/api/anthropic" model="glm-5.3[1m]" key_url="https://open.bigmodel.cn/usercenter/apikeys" ;;
|
||||||
|
MiniMax) base_url="https://api.minimaxi.com/anthropic" model="MiniMax-M3[1m]" key_url="https://platform.minimaxi.com/user-center/basic-information/interface-key" ;;
|
||||||
|
Custom)
|
||||||
|
base_url=$(gum input --header "Anthropic-compatible base URL" --placeholder "https://example.com/anthropic")
|
||||||
|
model=""
|
||||||
|
key_url=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
key=$(gum input --password --header "$provider API key${key_url:+ ($key_url)}")
|
||||||
|
model=$(gum input --header "Model for every Claude Code tier" --value "$model")
|
||||||
|
|
||||||
|
if [[ -z $base_url || -z $key || -z $model ]]; then
|
||||||
|
echo "Base URL, API key and model are all required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rewrite "$settings" '.env = (.env // {}) + ([$keys[] | {key: ., value: $model}] | from_entries) + {ANTHROPIC_BASE_URL: $url, ANTHROPIC_AUTH_TOKEN: $token}' \
|
||||||
|
--arg url "$base_url" --arg token "$key" --arg model "$model"
|
||||||
|
|
||||||
|
# Third-party keys skip Anthropic's sign-in screen.
|
||||||
|
rewrite "$HOME/.claude.json" '.hasCompletedOnboarding = true'
|
||||||
|
|
||||||
|
echo "Claude Code now uses $model at $base_url"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Set up Japanese, Korean, or Chinese typing
|
||||||
|
# omarchy:args=[mozc|hangul|pinyin|chewing]
|
||||||
|
# omarchy:examples=omarchy setup input | omarchy setup input mozc
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if (( $# > 1 )); then
|
||||||
|
echo "Usage: omarchy setup input [mozc|hangul|pinyin|chewing]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
method=${1:-}
|
||||||
|
if [[ -z $method ]]; then
|
||||||
|
choice=$(jq -r 'to_entries[] | select(.key != "none") | .value.label' "$OMARCHY_PATH/default/input-methods/presets.json" |
|
||||||
|
gum choose --header "Select input language")
|
||||||
|
method=$(jq -r --arg label "$choice" 'to_entries[] | select(.value.label == $label) | .key' "$OMARCHY_PATH/default/input-methods/presets.json")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$method" in
|
||||||
|
mozc | hangul | pinyin | chewing) ;;
|
||||||
|
*) echo "Unknown input method: $method" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
|
||||||
|
omarchy-pkg-add "${packages[@]}"
|
||||||
|
omarchy-input-method configure "$method"
|
||||||
|
echo "Input method added. The default switch is Ctrl+Shift+Space or the Japanese/Korean input key; existing custom keys are preserved."
|
||||||
+179
-32
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash -p
|
#!/bin/bash -p
|
||||||
|
|
||||||
# omarchy:summary=Toggle passwordless sudo for the current user.
|
# omarchy:summary=Toggle passwordless sudo for the current user.
|
||||||
# omarchy:args=[MINUTES]
|
# omarchy:args=[MINUTES|permanent|--active|--disable]
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
|
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||||
@@ -22,7 +22,6 @@ fi
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
readonly DEFAULT_MINUTES=15
|
|
||||||
readonly MAX_MINUTES=1440
|
readonly MAX_MINUTES=1440
|
||||||
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
|
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
|
||||||
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
|
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
|
||||||
@@ -34,7 +33,7 @@ readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
|
|||||||
readonly STATUS_INACTIVE=3
|
readonly STATUS_INACTIVE=3
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
|
echo "Usage: omarchy-sudo-passwordless [MINUTES|permanent|--active|--disable]" >&2
|
||||||
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
|
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -43,6 +42,10 @@ valid_minutes() {
|
|||||||
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
|
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valid_duration() {
|
||||||
|
[[ $1 == "permanent" ]] || valid_minutes "$1"
|
||||||
|
}
|
||||||
|
|
||||||
valid_uid() {
|
valid_uid() {
|
||||||
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
|
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
|
||||||
}
|
}
|
||||||
@@ -96,14 +99,39 @@ with_root_lock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rule_file() {
|
rule_file() {
|
||||||
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
|
local uid=$((10#$1))
|
||||||
|
if [[ ${2:-} == "permanent" ]]; then
|
||||||
|
printf '/etc/sudoers.d/99-omarchy-permanent-nopasswd-%s' "$uid"
|
||||||
|
else
|
||||||
|
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$uid"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# The sudoers rule is the only grant record. A missing file is distinct from
|
# The sudoers rule is the only grant record. A missing file is distinct from
|
||||||
# an unreadable, unsafe, or administrator-modified file.
|
# an unreadable, unsafe, or administrator-modified file.
|
||||||
read_grant() {
|
read_grant() {
|
||||||
local file contents
|
local file permanent contents
|
||||||
file=$(rule_file "$1")
|
file=$(rule_file "$1")
|
||||||
|
permanent=$(rule_file "$1" permanent)
|
||||||
|
if [[ -e $permanent || -L $permanent ]]; then
|
||||||
|
verify_root_path "$permanent" && [[ -f $permanent ]] || return 2
|
||||||
|
contents=$(/usr/bin/cat -- "$permanent") || return 2
|
||||||
|
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOPASSWD:\ ALL$ ]] || return 2
|
||||||
|
GRANT_NAME=${BASH_REMATCH[1]}
|
||||||
|
GRANT_DEADLINE=""
|
||||||
|
valid_account_name "$GRANT_NAME" || return 2
|
||||||
|
if [[ -e $file || -L $file ]]; then
|
||||||
|
# A SIGKILL between publication and removal can leave both policies.
|
||||||
|
# Only accept a matching generated timed rule. Permanent access already
|
||||||
|
# exists, so old expiry callbacks must not revoke it; disable removes
|
||||||
|
# both files, and the next duration change finishes their replacement.
|
||||||
|
verify_root_path "$file" && [[ -f $file ]] || return 2
|
||||||
|
contents=$(/usr/bin/cat -- "$file") || return 2
|
||||||
|
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOTAFTER=([0-9]{14}Z)\ NOPASSWD:\ ALL$ ]] || return 2
|
||||||
|
[[ ${BASH_REMATCH[1]} == "$GRANT_NAME" ]] || return 2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
[[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
|
[[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
|
||||||
verify_root_path "$file" && [[ -f $file ]] || return 2
|
verify_root_path "$file" && [[ -f $file ]] || return 2
|
||||||
contents=$(/usr/bin/cat -- "$file") || return 2
|
contents=$(/usr/bin/cat -- "$file") || return 2
|
||||||
@@ -121,6 +149,9 @@ classify_generated_rule() {
|
|||||||
[[ -f $file && ! -L $file ]] || return 1
|
[[ -f $file && ! -L $file ]] || return 1
|
||||||
contents=$(/usr/bin/cat -- "$file") || return 2
|
contents=$(/usr/bin/cat -- "$file") || return 2
|
||||||
suffix=${file##*/99-omarchy-nopasswd-}
|
suffix=${file##*/99-omarchy-nopasswd-}
|
||||||
|
if [[ $file == */99-omarchy-permanent-nopasswd-* ]]; then
|
||||||
|
suffix=${file##*/99-omarchy-permanent-nopasswd-}
|
||||||
|
fi
|
||||||
|
|
||||||
# The legacy command wrote the caller's unvalidated name into both the
|
# The legacy command wrote the caller's unvalidated name into both the
|
||||||
# filename and the rule. That exact relationship is its fingerprint, so an
|
# filename and the rule. That exact relationship is its fingerprint, so an
|
||||||
@@ -138,14 +169,21 @@ classify_generated_rule() {
|
|||||||
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
|
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_uid_locked() {
|
cleanup_rule_file() {
|
||||||
local file
|
local file=$1
|
||||||
file=$(rule_file "$1")
|
|
||||||
[[ -e $file || -L $file ]] || return 0
|
[[ -e $file || -L $file ]] || return 0
|
||||||
verify_root_path "$file" && classify_generated_rule "$file" || return 1
|
verify_root_path "$file" && classify_generated_rule "$file" || return 1
|
||||||
/usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
|
/usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup_uid_locked() {
|
||||||
|
local file duration
|
||||||
|
for duration in temporary permanent; do
|
||||||
|
file=$(rule_file "$1" "$duration")
|
||||||
|
cleanup_rule_file "$file" || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# The generated prefix is reserved: boot cleanup and the package hook already
|
# The generated prefix is reserved: boot cleanup and the package hook already
|
||||||
# remove everything in it, and the legacy writer could produce a rule whose
|
# remove everything in it, and the legacy writer could produce a rule whose
|
||||||
# body differs from its filename. Nothing unrecognized may stay live there, but
|
# body differs from its filename. Nothing unrecognized may stay live there, but
|
||||||
@@ -263,6 +301,7 @@ migrate_locked() {
|
|||||||
expire_locked() {
|
expire_locked() {
|
||||||
local status now
|
local status now
|
||||||
if read_grant "$1"; then
|
if read_grant "$1"; then
|
||||||
|
[[ -n $GRANT_DEADLINE ]] || return 0
|
||||||
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
|
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
|
||||||
[[ $now < $GRANT_DEADLINE ]] && return 0
|
[[ $now < $GRANT_DEADLINE ]] && return 0
|
||||||
cleanup_uid_locked "$1"
|
cleanup_uid_locked "$1"
|
||||||
@@ -281,6 +320,7 @@ status_locked() {
|
|||||||
resolve_account "$1" || return 2
|
resolve_account "$1" || return 2
|
||||||
if read_grant "$1"; then
|
if read_grant "$1"; then
|
||||||
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
|
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
|
||||||
|
[[ -n $GRANT_DEADLINE ]] || return 0
|
||||||
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
|
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
|
||||||
if [[ $now < $GRANT_DEADLINE ]]; then
|
if [[ $now < $GRANT_DEADLINE ]]; then
|
||||||
return 0
|
return 0
|
||||||
@@ -300,7 +340,11 @@ finish_enable() {
|
|||||||
if cleanup_uid_locked "$uid"; then
|
if cleanup_uid_locked "$uid"; then
|
||||||
[[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
|
[[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
|
||||||
else
|
else
|
||||||
|
if [[ -n $timer ]]; then
|
||||||
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
|
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
|
||||||
|
else
|
||||||
|
echo "Could not revoke passwordless sudo. Administrator cleanup is required." >&2
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
[[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
|
[[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
|
||||||
@@ -309,9 +353,15 @@ finish_enable() {
|
|||||||
|
|
||||||
enable_locked() (
|
enable_locked() (
|
||||||
local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
|
local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
|
||||||
resolve_account "$uid" && valid_minutes "$minutes" || return 1
|
resolve_account "$uid" && valid_duration "$minutes" || return 1
|
||||||
verify_boot_cleanup && verify_root_path /etc/sudoers.d || return 1
|
verify_root_path /etc/sudoers.d || return 1
|
||||||
file=$(rule_file "$uid")
|
if [[ $minutes != "permanent" ]]; then
|
||||||
|
verify_boot_cleanup || {
|
||||||
|
echo "Temporary sudo cleanup is unavailable or a settings package transaction is incomplete. Check the omarchy-settings installation." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
file=$(rule_file "$uid" "$minutes")
|
||||||
if read_grant "$uid"; then
|
if read_grant "$uid"; then
|
||||||
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
|
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
|
||||||
else
|
else
|
||||||
@@ -320,13 +370,18 @@ enable_locked() (
|
|||||||
fi
|
fi
|
||||||
trap finish_enable EXIT
|
trap finish_enable EXIT
|
||||||
omarchy_security_install_signal_exit_traps
|
omarchy_security_install_signal_exit_traps
|
||||||
|
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
|
||||||
|
if [[ $minutes == "permanent" ]]; then
|
||||||
|
/usr/bin/printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$ACCOUNT_NAME" >"$pending" || return 1
|
||||||
|
else
|
||||||
now=$(/usr/bin/date +%s) || return 1
|
now=$(/usr/bin/date +%s) || return 1
|
||||||
expires=$((now + 10#$minutes * 60))
|
expires=$((now + 10#$minutes * 60))
|
||||||
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
|
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
|
||||||
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
|
|
||||||
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
|
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
|
||||||
|
fi
|
||||||
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
|
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
|
||||||
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1
|
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1
|
||||||
|
if [[ $minutes != "permanent" ]]; then
|
||||||
token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
|
token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
|
||||||
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
|
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
|
||||||
timer="omarchy-nopasswd-expire-$uid-$token"
|
timer="omarchy-nopasswd-expire-$uid-$token"
|
||||||
@@ -334,25 +389,50 @@ enable_locked() (
|
|||||||
--timer-property=AccuracySec=1s --unit="$timer" \
|
--timer-property=AccuracySec=1s --unit="$timer" \
|
||||||
-- "$INSTALLED_SELF" __expire "$uid" || return 1
|
-- "$INSTALLED_SELF" __expire "$uid" || return 1
|
||||||
/usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
|
/usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
|
||||||
|
fi
|
||||||
# The temporary filename contains a dot, so sudo ignores it. Rename within
|
# The temporary filename contains a dot, so sudo ignores it. Rename within
|
||||||
# sudoers.d publishes the complete validated policy in one operation.
|
# sudoers.d publishes the complete validated policy in one operation.
|
||||||
/usr/bin/mv -fT -- "$pending" "$file" || return 1
|
/usr/bin/mv -fT -- "$pending" "$file" || return 1
|
||||||
pending=""
|
pending=""
|
||||||
|
# Keep access uninterrupted during renewal. When changing duration type,
|
||||||
|
# remove the other rule only after its replacement is published. The lock
|
||||||
|
# keeps status and expiry callbacks from observing both rules together.
|
||||||
|
if [[ $minutes == "permanent" ]]; then
|
||||||
|
cleanup_rule_file "$(rule_file "$uid")" || return 1
|
||||||
|
else
|
||||||
|
cleanup_rule_file "$(rule_file "$uid" permanent)" || return 1
|
||||||
|
fi
|
||||||
|
if [[ $minutes != "permanent" ]]; then
|
||||||
now=$(/usr/bin/date +%s) || return 1
|
now=$(/usr/bin/date +%s) || return 1
|
||||||
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
|
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
|
||||||
|
fi
|
||||||
)
|
)
|
||||||
|
|
||||||
root_dispatch() {
|
root_dispatch() {
|
||||||
local action="$1"
|
local action="$1" status
|
||||||
shift
|
shift
|
||||||
case "$action" in
|
case "$action" in
|
||||||
__status)
|
__status)
|
||||||
(($# == 1)) && verify_sudo_caller "$1" || return 2
|
(($# == 1)) && verify_sudo_caller "$1" || return 2
|
||||||
with_root_lock status_locked "$1"
|
if with_root_lock status_locked "$1"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
# Reserve status 1 for sudo refusing noninteractive authentication.
|
||||||
|
# Every internal inspection failure, including lock failures, is 2.
|
||||||
|
(( status == STATUS_INACTIVE )) || status=2
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
__enable)
|
__enable)
|
||||||
(($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
|
(($# == 2)) && verify_sudo_caller "$1" && valid_duration "$2" || return 1
|
||||||
with_root_lock enable_locked "$1" "$2"
|
if with_root_lock enable_locked "$1" "$2"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
echo "Could not enable passwordless sudo. Check existing sudoers policy and cleanup support." >&2
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
__disable)
|
__disable)
|
||||||
(($# == 1)) && verify_sudo_caller "$1" || return 1
|
(($# == 1)) && verify_sudo_caller "$1" || return 1
|
||||||
@@ -382,6 +462,14 @@ root_dispatch() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_enabled() {
|
||||||
|
if [[ $minutes == "permanent" ]]; then
|
||||||
|
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
|
||||||
|
else
|
||||||
|
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
|
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
|
||||||
action=$1
|
action=$1
|
||||||
@@ -391,9 +479,30 @@ case "${1:-}" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Prefer matched policy tags to executing a privileged command, avoiding
|
||||||
|
# authentication/session logs under the default listing policy. Restrictive
|
||||||
|
# listpw settings require the root-status fallback. Neither path prompts or
|
||||||
|
# trusts cached credentials as evidence of passwordless access.
|
||||||
|
if [[ ${1:-} == "--active" ]]; then
|
||||||
|
(($# == 1)) || usage
|
||||||
|
uid=$(/usr/bin/id -u)
|
||||||
|
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
|
||||||
|
/usr/bin/grep -q '!authenticate' <<<"$policy"
|
||||||
|
else
|
||||||
|
# listpw=always can require authentication to list a passwordless grant.
|
||||||
|
# With a command, -k ignores cached credentials without invalidating them.
|
||||||
|
status=0
|
||||||
|
/usr/bin/sudo -kn -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
|
||||||
|
# An internal inspection error still proves that sudo ran the helper
|
||||||
|
# without authentication. Keep the warning visible for unsafe policy.
|
||||||
|
(( status == 0 || status == 2 )) || exit 1
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
(($# <= 1)) || usage
|
(($# <= 1)) || usage
|
||||||
minutes=${1:-$DEFAULT_MINUTES}
|
minutes=${1:-}
|
||||||
valid_minutes "$minutes" || usage
|
(($# == 0)) || [[ $minutes == "--disable" ]] || valid_duration "$minutes" || usage
|
||||||
uid=$(/usr/bin/id -u)
|
uid=$(/usr/bin/id -u)
|
||||||
valid_uid "$uid" || {
|
valid_uid "$uid" || {
|
||||||
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
|
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
|
||||||
@@ -411,38 +520,76 @@ omarchy_security_install_sudo_cleanup_traps
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Toggle passwordless sudo..."
|
# Inspect policy without authenticating. Only inspect the root-owned grant
|
||||||
if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
|
# when policy already permits it without a password; otherwise the eventual
|
||||||
if (($# == 0)); then
|
# enable action is the sole authentication boundary. enable_locked validates
|
||||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
|
# any existing policy before publishing, including expired or modified rules.
|
||||||
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
|
status=$STATUS_INACTIVE
|
||||||
else
|
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
|
||||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
if /usr/bin/grep -q '!authenticate' <<<"$policy"; then
|
||||||
echo "Passwordless sudo expiry updated. It will automatically disable in ${minutes} minutes."
|
status=0
|
||||||
|
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" || status=$?
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
status=$?
|
# A listing password is independent of command authorization. An active
|
||||||
if (( status != STATUS_INACTIVE )); then
|
# grant can still be inspected/revoked without prompting under listpw=always.
|
||||||
|
status=0
|
||||||
|
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
|
||||||
|
if (( status == 1 )) && [[ $minutes != "--disable" ]]; then
|
||||||
|
# No noninteractive inspection is available. Offer the confirmed enable
|
||||||
|
# flow; its single authenticated action validates existing policy itself.
|
||||||
|
status=$STATUS_INACTIVE
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
|
||||||
|
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
|
||||||
|
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
|
||||||
|
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
|
||||||
|
echo "Passwordless sudo is already DISABLED."
|
||||||
|
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
|
||||||
|
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||||
|
show_enabled
|
||||||
|
else
|
||||||
|
if (( status != 0 && status != STATUS_INACTIVE )); then
|
||||||
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
|
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ -z $minutes ]]; then
|
||||||
|
choice=$(/usr/bin/gum choose --header "How long should passwordless sudo stay enabled?" "15 minutes" "1 Hour" "1 Day" "Permanently") || exit 130
|
||||||
|
case "$choice" in
|
||||||
|
"15 minutes") minutes=15 ;;
|
||||||
|
"1 Hour") minutes=60 ;;
|
||||||
|
"1 Day") minutes=1440 ;;
|
||||||
|
"Permanently") minutes=permanent ;;
|
||||||
|
*) exit 130 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [[ $minutes == "permanent" ]]; then
|
||||||
|
duration="permanently"
|
||||||
|
else
|
||||||
|
duration="for ${minutes} minutes"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ WARNING: This will allow ANY process running as your user to"
|
echo "⚠️ WARNING: This will allow ANY process running as your user to"
|
||||||
echo "execute ANY command as root WITHOUT a password for ${minutes} minutes."
|
echo "execute ANY command as root WITHOUT a password $duration."
|
||||||
echo ""
|
echo ""
|
||||||
echo "This is useful for AI agents that need to run sudo commands,"
|
echo "This is useful for AI agents that need to run sudo commands,"
|
||||||
echo "but it significantly weakens the security of your system."
|
echo "but it significantly weakens the security of your system."
|
||||||
echo "Anyone or anything with access to your user account gets full root."
|
echo "Anyone or anything with access to your user account gets full root."
|
||||||
echo ""
|
echo ""
|
||||||
|
if [[ $minutes == "permanent" ]]; then
|
||||||
|
echo "Passwordless sudo will remain enabled across reboots until you disable it."
|
||||||
|
else
|
||||||
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
|
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
|
||||||
echo "including if the machine reboots before the deadline."
|
echo "including if the machine reboots before the deadline."
|
||||||
echo "Run this command again to disable it early."
|
fi
|
||||||
|
echo "Run this command again to disable it."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if /usr/bin/gum confirm "Enable passwordless sudo for ${minutes} minutes? This is a significant security risk!"; then
|
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
|
||||||
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
|
show_enabled
|
||||||
else
|
else
|
||||||
echo "Aborted. No changes made."
|
echo "Aborted. No changes made."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
|
||||||
|
|
||||||
omarchy-osd -i logout -m "Logging out" -d 5000
|
omarchy-osd -i logout -m "$(omarchy-text "Logging out")" -d 5000
|
||||||
|
|
||||||
# Now close all windows
|
# Now close all windows
|
||||||
omarchy-hyprland-window-close-all
|
omarchy-hyprland-window-close-all
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# scope cannot terminate it before it runs.
|
# scope cannot terminate it before it runs.
|
||||||
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
|
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
|
||||||
|
|
||||||
omarchy-osd -i reboot -m "Rebooting" -d 5000
|
omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
|
||||||
|
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
if [[ ${1:-} == "--inhibited" ]]; then
|
if [[ ${1:-} == "--inhibited" ]]; then
|
||||||
# Close windows in the background so they cannot delay power-off.
|
# Close windows in the background so they cannot delay power-off.
|
||||||
(
|
(
|
||||||
omarchy-osd -i shutdown -m "Shutting down" -d 5000
|
omarchy-osd -i shutdown -m "$(omarchy-text "Shutting down")" -d 5000
|
||||||
omarchy-state clear re*-required
|
omarchy-state clear re*-required
|
||||||
omarchy-hyprland-window-close-all
|
omarchy-hyprland-window-close-all
|
||||||
) &
|
) &
|
||||||
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
|
|||||||
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
--who=Omarchy --why="Shutdown in progress" --mode=block \
|
||||||
"$0" --inhibited || {
|
"$0" --inhibited || {
|
||||||
status=$?
|
status=$?
|
||||||
omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again."
|
omarchy-notification-send -u critical "$(omarchy-text "Shutdown failed")" "$(omarchy-text "Could not complete shutdown. Please try again.")"
|
||||||
exit "$status"
|
exit "$status"
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ report_unsecured() {
|
|||||||
"$1" >&2
|
"$1" >&2
|
||||||
|
|
||||||
omarchy-notification-send -u critical -g \
|
omarchy-notification-send -u critical -g \
|
||||||
"Screen did not lock before suspend" \
|
"$(omarchy-text "Screen did not lock before suspend")" \
|
||||||
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
|
"$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ claim_path() {
|
|||||||
announce() {
|
announce() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
local name="${path##*/}"
|
local name="${path##*/}"
|
||||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical)
|
local args=("$(omarchy-text "Received %1" "$name")" "$(omarchy-text "Saved to %1" "${dir/#$HOME/~}")" -u critical)
|
||||||
|
|
||||||
case "${name,,}" in
|
case "${name,,}" in
|
||||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ if ((${#files[@]} == 0)); then
|
|||||||
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
||||||
|
|
||||||
if ((${status:-0} > 1)); then
|
if ((${status:-0} > 1)); then
|
||||||
omarchy-notification-send -g "" -u critical "Could not send to $name" \
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
|
||||||
"The file chooser did not open"
|
"$(omarchy-text "The file chooser did not open")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
||||||
omarchy-notification-send -g "" "Sent to $name" "$what"
|
omarchy-notification-send -g "" "$(omarchy-text "Sent to %1" "$name")" "$what"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}"
|
omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not send to %1" "$name")" "${error:-$(omarchy-text "Taildrop transfer failed")}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Print an Omarchy message in the session's language, with %1.. filled from the arguments
|
||||||
|
# omarchy:args=<english> [args...]
|
||||||
|
# omarchy:examples=omarchy-text "Sent to %1" phone
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if (($# < 1)); then
|
||||||
|
echo "Usage: omarchy-text <english> [args...]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
text=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
# The shell's table is the one source: one JSON-encoded "english": "chinese" pair per line.
|
||||||
|
if [[ ${LANG:-} == zh_CN* ]]; then
|
||||||
|
translated=$(grep -E '^ ".*": ".*",?$' "$OMARCHY_PATH/shell/Commons/I18nZh.js" | sed 's/,$//' | paste -sd, |
|
||||||
|
sed 's/^/{/; s/$/}/' | jq -r --arg key "$text" '.[$key] // empty')
|
||||||
|
[[ -z $translated ]] || text=$translated
|
||||||
|
fi
|
||||||
|
|
||||||
|
# One pass over the template, so an argument that holds %2 is printed as given.
|
||||||
|
out=""
|
||||||
|
rest=$text
|
||||||
|
while [[ $rest =~ ^([^%]*)%(.)(.*)$ ]]; do
|
||||||
|
out+=${BASH_REMATCH[1]}
|
||||||
|
mark=${BASH_REMATCH[2]}
|
||||||
|
rest=${BASH_REMATCH[3]}
|
||||||
|
if [[ $mark == [1-9] ]] && ((mark <= $#)); then
|
||||||
|
out+=${!mark}
|
||||||
|
else
|
||||||
|
out+="%$mark"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s\n' "$out$rest"
|
||||||
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
|
|||||||
TOTAL=${#BACKGROUNDS[@]}
|
TOTAL=${#BACKGROUNDS[@]}
|
||||||
|
|
||||||
if (( TOTAL == 0 )); then
|
if (( TOTAL == 0 )); then
|
||||||
omarchy-notification-send "No background was found for theme" -t 2000
|
omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
|
||||||
else
|
else
|
||||||
# Get current background from symlink
|
# Get current background from symlink
|
||||||
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ fi
|
|||||||
|
|
||||||
# Now remove the theme directory for THEME_NAME
|
# Now remove the theme directory for THEME_NAME
|
||||||
rm -rf "$THEME_PATH"
|
rm -rf "$THEME_PATH"
|
||||||
omarchy-notification-send "Theme removed" "$THEME_NAME" || true
|
omarchy-notification-send "$(omarchy-text "Theme removed")" "$THEME_NAME" || true
|
||||||
echo "Removed $THEME_NAME"
|
echo "Removed $THEME_NAME"
|
||||||
@@ -175,7 +175,7 @@ set_theme_background() {
|
|||||||
|
|
||||||
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
|
||||||
if ! choose_theme_background; then
|
if ! choose_theme_background; then
|
||||||
omarchy-notification-send "No background was found for theme" -t 2000
|
omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
|
||||||
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
|
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-enabled no-animations; then
|
if omarchy-hyprland-toggle-enabled no-animations; then
|
||||||
omarchy-notification-send -g "Animations disabled"
|
omarchy-notification-send -g "$(omarchy-text "Animations disabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Animations enabled"
|
omarchy-notification-send -g "$(omarchy-text "Animations enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -11,8 +11,8 @@ omarchy-toggle crash-capture-off
|
|||||||
# starting only settles this session; the next login reads the flag itself.
|
# starting only settles this session; the next login reads the flag itself.
|
||||||
if omarchy-toggle-enabled crash-capture-off; then
|
if omarchy-toggle-enabled crash-capture-off; then
|
||||||
systemctl --user stop omarchy-crash-watch.service
|
systemctl --user stop omarchy-crash-watch.service
|
||||||
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture disabled"
|
omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture disabled")"
|
||||||
else
|
else
|
||||||
systemctl --user start omarchy-crash-watch.service
|
systemctl --user start omarchy-crash-watch.service
|
||||||
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture enabled"
|
omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle screensaver-off
|
omarchy-toggle screensaver-off
|
||||||
|
|
||||||
if omarchy-toggle-enabled screensaver-off; then
|
if omarchy-toggle-enabled screensaver-off; then
|
||||||
omarchy-notification-send -g "Screensaver disabled"
|
omarchy-notification-send -g "$(omarchy-text "Screensaver disabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Screensaver enabled"
|
omarchy-notification-send -g "$(omarchy-text "Screensaver enabled")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle suspend-off
|
omarchy-toggle suspend-off
|
||||||
|
|
||||||
if omarchy-toggle-enabled suspend-off; then
|
if omarchy-toggle-enabled suspend-off; then
|
||||||
omarchy-notification-send -g "Suspend removed from system menu"
|
omarchy-notification-send -g "$(omarchy-text "Suspend removed from system menu")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Suspend now available in system menu"
|
omarchy-notification-send -g "$(omarchy-text "Suspend now available in system menu")"
|
||||||
fi
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
omarchy-toggle herdr-theme-sync
|
omarchy-toggle herdr-theme-sync
|
||||||
|
|
||||||
if omarchy-toggle-enabled herdr-theme-sync; then
|
if omarchy-toggle-enabled herdr-theme-sync; then
|
||||||
omarchy-notification-send -g "Herdr theme sync enabled"
|
omarchy-notification-send -g "$(omarchy-text "Herdr theme sync enabled")"
|
||||||
else
|
else
|
||||||
omarchy-notification-send -g "Herdr theme sync disabled"
|
omarchy-notification-send -g "$(omarchy-text "Herdr theme sync disabled")"
|
||||||
fi
|
fi
|
||||||
@@ -193,14 +193,14 @@ main() {
|
|||||||
output=$(output_path "$input" "$format" "$resolution")
|
output=$(output_path "$input" "$format" "$resolution")
|
||||||
|
|
||||||
if [[ $type == "video" ]]; then
|
if [[ $type == "video" ]]; then
|
||||||
omarchy-notification-send -g "Transcoding video…" "$(basename -- "$input") to $format ($resolution)"
|
omarchy-notification-send -g "$(omarchy-text "Transcoding video…")" "$(omarchy-text "%1 to %2 (%3)" "$(basename -- "$input")" "$format" "$resolution")"
|
||||||
transcode_video "$input" "$format" "$resolution" "$output"
|
transcode_video "$input" "$format" "$resolution" "$output"
|
||||||
copy_to_clipboard "$output"
|
copy_to_clipboard "$output"
|
||||||
omarchy-notification-send -g "Transcoded to $resolution $format" "Saved and copied to clipboard."
|
omarchy-notification-send -g "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
|
||||||
else
|
else
|
||||||
transcode_picture "$input" "$format" "$resolution" "$output"
|
transcode_picture "$input" "$format" "$resolution" "$output"
|
||||||
copy_to_clipboard "$output"
|
copy_to_clipboard "$output"
|
||||||
omarchy-notification-send -g "Transcoded to $resolution $format" "Saved and copied to clipboard."
|
omarchy-notification-send -g "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:
|
|||||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
||||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||||
omarchy-notification-send -g "TUI removed" "$APP_NAME"
|
omarchy-notification-send -g "$(omarchy-text "TUI removed")" "$APP_NAME"
|
||||||
fi
|
fi
|
||||||
+1
-1
@@ -127,7 +127,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
omarchy-update-system-pkgs
|
omarchy-update-system-pkgs
|
||||||
|
|
||||||
omarchy-migrate
|
omarchy-migrate
|
||||||
omarchy-update-orphan-pkgs
|
omarchy-update-orphan-pkgs -y
|
||||||
|
|
||||||
omarchy-update-analyze-logs
|
omarchy-update-analyze-logs
|
||||||
omarchy-update-status
|
omarchy-update-status
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Review and optionally remove orphaned system packages after updates
|
# omarchy:summary=Review and optionally remove orphaned system packages after updates
|
||||||
|
# omarchy:args=[-y]
|
||||||
|
# omarchy:examples=omarchy update orphan-pkgs | omarchy update orphan-pkgs -y
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -12,18 +14,18 @@ echo -e "\e[32m\nOrphan system packages\e[0m"
|
|||||||
printf ' %s\n' "${orphans[@]}"
|
printf ' %s\n' "${orphans[@]}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
if [[ ! -t 0 || ! -t 1 ]]; then
|
if [[ ${1:-} != "-y" && ( ! -t 0 || ! -t 1 ) ]]; then
|
||||||
echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them."
|
echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them."
|
||||||
echo
|
echo
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then
|
if [[ ${1:-} != "-y" ]] && ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then
|
||||||
echo "Keeping orphaned packages."
|
echo "Keeping orphaned packages."
|
||||||
echo
|
echo
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\e[32m\nRemoving orphan system packages\e[0m"
|
echo -e "\e[32m\nRemoving orphan system packages\e[0m"
|
||||||
sudo pacman -Rns "${orphans[@]}"
|
omarchy-pkg-drop "${orphans[@]}"
|
||||||
echo
|
echo
|
||||||
@@ -1802,6 +1802,8 @@ copy_missing_config_defaults() {
|
|||||||
while IFS= read -r -d '' source_path; do
|
while IFS= read -r -d '' source_path; do
|
||||||
rel="${source_path#$source_dir/}"
|
rel="${source_path#$source_dir/}"
|
||||||
is_retired_config_file "$rel" && continue
|
is_retired_config_file "$rel" && continue
|
||||||
|
# The input migration preserves working shortcuts in existing IME profiles.
|
||||||
|
[[ $rel == "fcitx5/config" ]] && continue
|
||||||
|
|
||||||
target_path="$target_dir/$rel"
|
target_path="$target_dir/$rel"
|
||||||
[[ -e $target_path || -L $target_path ]] && continue
|
[[ -e $target_path || -L $target_path ]] && continue
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
|
|||||||
|
|
||||||
hyprctl reload >/dev/null
|
hyprctl reload >/dev/null
|
||||||
omarchy-restart-shell
|
omarchy-restart-shell
|
||||||
omarchy-notification-send -g "Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
omarchy-notification-send -g "$(omarchy-text "Voxtype Dictation Ready")" "$(omarchy-text "Hold F9 to dictate (or toggle with Super + Ctrl + X).")" -t 10000
|
||||||
fi
|
fi
|
||||||
@@ -56,6 +56,6 @@ rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
|||||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||||
|
|
||||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||||
omarchy-notification-send -g "Web app removed" "$APP_NAME"
|
omarchy-notification-send -g "$(omarchy-text "Web app removed")" "$APP_NAME"
|
||||||
fi
|
fi
|
||||||
update-desktop-database "$DESKTOP_DIR" &>/dev/null
|
update-desktop-database "$DESKTOP_DIR" &>/dev/null
|
||||||
@@ -1414,7 +1414,7 @@ launch_windows() {
|
|||||||
if ! priv up_wait; then
|
if ! priv up_wait; then
|
||||||
echo "❌ Failed to start Windows VM!"
|
echo "❌ Failed to start Windows VM!"
|
||||||
echo " Try checking: omarchy-windows-vm status"
|
echo " Try checking: omarchy-windows-vm status"
|
||||||
omarchy-notification-send -u critical "Windows VM" "Failed to start Windows VM"
|
omarchy-notification-send -u critical "$(omarchy-text "Windows VM")" "$(omarchy-text "Failed to start Windows VM")"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Keep Pinyin offline without prompting to enable cloud prediction.
|
||||||
|
FirstRun=False
|
||||||
|
CloudPinyinEnabled=False
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[TriggerKey]
|
||||||
|
0=Super+semicolon
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Allow Overriding System XKB Settings=False
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Leave Ctrl+Space to tmux/Herdr and the Super shortcuts to the desktop.
|
||||||
|
[Hotkey/TriggerKeys]
|
||||||
|
0=Control+Shift+space
|
||||||
|
1=Zenkaku_Hankaku
|
||||||
|
2=Hangul
|
||||||
|
|
||||||
|
[Hotkey/ActivateKeys]
|
||||||
|
0=Henkan
|
||||||
|
|
||||||
|
[Hotkey/DeactivateKeys]
|
||||||
|
0=Muhenkan
|
||||||
|
|
||||||
|
[Hotkey/AltTriggerKeys]
|
||||||
|
|
||||||
|
[Hotkey/EnumerateGroupForwardKeys]
|
||||||
|
|
||||||
|
[Hotkey/EnumerateGroupBackwardKeys]
|
||||||
|
|
||||||
|
[Behavior]
|
||||||
|
ActiveByDefault=False
|
||||||
@@ -1,6 +1,298 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||||
<fontconfig>
|
<fontconfig>
|
||||||
|
<!-- CJK: fold the language tag onto one tag per Noto CJK variant -->
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="eq">
|
||||||
|
<string>zh</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="eq">
|
||||||
|
<string>zh-hant</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-tw</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hant-hk</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hant-mo</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hant</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-tw</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hans</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-sg</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-mo</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="eq">
|
||||||
|
<string>yue</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>yue-hans</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>yue-cn</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>yue</string>
|
||||||
|
</test>
|
||||||
|
<edit name="lang" mode="assign">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<!-- Then append that variant after the generic family it falls back from -->
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>sans-serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans CJK SC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Serif CJK SC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-cn</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>monospace</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans Mono CJK SC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>sans-serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans CJK HK</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Serif CJK HK</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-hk</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>monospace</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans Mono CJK HK</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-tw</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>sans-serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans CJK TC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-tw</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Serif CJK TC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<string>zh-tw</string>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>monospace</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans Mono CJK TC</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ja</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>sans-serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans CJK JP</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ja</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Serif CJK JP</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ja</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>monospace</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans Mono CJK JP</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ko</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>sans-serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans CJK KR</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ko</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>serif</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Serif CJK KR</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" qual="first" compare="contains">
|
||||||
|
<langset><string>ko</string></langset>
|
||||||
|
</test>
|
||||||
|
<test name="family">
|
||||||
|
<string>monospace</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Sans Mono CJK KR</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
<match target="pattern">
|
<match target="pattern">
|
||||||
<test name="family" qual="any">
|
<test name="family" qual="any">
|
||||||
<string>sans-serif</string>
|
<string>sans-serif</string>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
local paths = require("default.hypr.paths")
|
||||||
|
|
||||||
|
-- Same terminal tag test as clipboard.lua.
|
||||||
|
local function active_window_is_terminal()
|
||||||
|
local window = hl.get_active_window()
|
||||||
|
if not window then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, tag in ipairs(window.tags or {}) do
|
||||||
|
if tag:gsub("%*$", "") == "terminal" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read on demand so adding an engine in a live desktop needs no reload.
|
||||||
|
local function has_input_method()
|
||||||
|
local file = io.open(paths.config_home .. "/fcitx5/profile", "r")
|
||||||
|
if not file then return false end
|
||||||
|
local in_item, found = false, false
|
||||||
|
for line in file:lines() do
|
||||||
|
if line:match("^%[") then
|
||||||
|
in_item = line:match("^%[Groups/%d+/Items/%d+%]$") ~= nil
|
||||||
|
elseif in_item then
|
||||||
|
local name = line:match("^Name=(.+)$")
|
||||||
|
if name and not name:match("^keyboard%-") then found = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
file:close()
|
||||||
|
return found
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Preserve custom Ctrl + Space triggers. Without a
|
||||||
|
-- [Hotkey/TriggerKeys] list in the config, fcitx5 uses its default, which has
|
||||||
|
-- Ctrl + Space.
|
||||||
|
local function fcitx5_toggles_on_ctrl_space()
|
||||||
|
local file = io.open(paths.config_home .. "/fcitx5/config", "r")
|
||||||
|
if not file then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local in_triggers, has_triggers, toggles = false, false, false
|
||||||
|
for line in file:lines() do
|
||||||
|
if line:match("^%[") then
|
||||||
|
in_triggers = line == "[Hotkey/TriggerKeys]"
|
||||||
|
has_triggers = has_triggers or in_triggers
|
||||||
|
elseif in_triggers and line:match("^%d+=Control%+space$") then
|
||||||
|
toggles = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
file:close()
|
||||||
|
return toggles or not has_triggers
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pass the tmux/Herdr prefix through and return to direct input, so the next
|
||||||
|
-- key reaches the multiplexer instead of the engine's composition.
|
||||||
|
o.bind("CTRL + SPACE", "Direct input for the terminal prefix", function()
|
||||||
|
if active_window_is_terminal() and has_input_method() and not fcitx5_toggles_on_ctrl_space() then
|
||||||
|
-- Wait for the local controller reply before dispatching the next key.
|
||||||
|
o.shell_succeeds("timeout 0.3 fcitx5-remote --check -c")
|
||||||
|
end
|
||||||
|
end, { non_consuming = true })
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- Defaults for Japanese (JIS) keyboards, which only apply when the installed
|
||||||
|
-- layout is jp. See default/hypr/keyboard.lua.
|
||||||
|
|
||||||
|
local keyboard = require("default.hypr.keyboard")
|
||||||
|
|
||||||
|
if not keyboard.jis() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Same down/up split as the universal clipboard shortcuts in clipboard.lua.
|
||||||
|
local function send_shortcut_once(mods, key)
|
||||||
|
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
|
||||||
|
|
||||||
|
hl.timer(function()
|
||||||
|
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
|
||||||
|
end, { timeout = 50, type = "oneshot" })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function active_window_class()
|
||||||
|
local window = hl.get_active_window()
|
||||||
|
return ((window and window.class) or ""):lower()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- On JIS, + is Shift + ;, so apps see Ctrl + Shift + semicolon and never zoom
|
||||||
|
-- in. Hand them Ctrl + keypad plus instead, which Chromium, Electron apps,
|
||||||
|
-- VS Code, and Firefox read as zoom in. Obsidian only listens for Ctrl + ^,
|
||||||
|
-- which JIS types without Shift.
|
||||||
|
local zoom_in_overrides = {
|
||||||
|
obsidian = { "CTRL", "asciicircum" },
|
||||||
|
}
|
||||||
|
|
||||||
|
o.bind("CTRL + SHIFT + semicolon", "Zoom in (JIS + key)", function()
|
||||||
|
local class = active_window_class()
|
||||||
|
|
||||||
|
for pattern, chord in pairs(zoom_in_overrides) do
|
||||||
|
if class:find(pattern, 1, true) then
|
||||||
|
send_shortcut_once(chord[1], chord[2])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
send_shortcut_once("CTRL", "KP_Add")
|
||||||
|
end)
|
||||||
+2
-21
@@ -1,32 +1,13 @@
|
|||||||
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input
|
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input
|
||||||
|
|
||||||
local function read_vconsole()
|
local keyboard = require("default.hypr.keyboard")
|
||||||
local values = {}
|
|
||||||
local file = io.open("/etc/vconsole.conf", "r")
|
|
||||||
if not file then
|
|
||||||
return values
|
|
||||||
end
|
|
||||||
|
|
||||||
for line in file:lines() do
|
|
||||||
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
|
|
||||||
if key and value then
|
|
||||||
value = value:gsub("%s+#.*$", "")
|
|
||||||
value = value:gsub('^"(.*)"$', "%1")
|
|
||||||
value = value:gsub("^'(.*)'$", "%1")
|
|
||||||
values[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
file:close()
|
|
||||||
return values
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Layouts that can't type Latin letters. Keep in sync with the list in
|
-- Layouts that can't type Latin letters. Keep in sync with the list in
|
||||||
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
|
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
|
||||||
local non_latin_layouts =
|
local non_latin_layouts =
|
||||||
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
|
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
|
||||||
|
|
||||||
local vconsole = read_vconsole()
|
local vconsole = keyboard.installed()
|
||||||
|
|
||||||
local kb_layout = vconsole.XKBLAYOUT or "us"
|
local kb_layout = vconsole.XKBLAYOUT or "us"
|
||||||
local kb_variant = vconsole.XKBVARIANT or ""
|
local kb_variant = vconsole.XKBVARIANT or ""
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- The keyboard layout picked at install time. The installer and first-boot
|
||||||
|
-- setup persist it to /etc/vconsole.conf, so Hyprland reads it from there
|
||||||
|
-- instead of rewriting every user's config.
|
||||||
|
|
||||||
|
local keyboard = {}
|
||||||
|
|
||||||
|
local function read_values(path)
|
||||||
|
local values = {}
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
|
||||||
|
for line in file:lines() do
|
||||||
|
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
|
||||||
|
if key and value then
|
||||||
|
value = value:gsub("%s+#.*$", "")
|
||||||
|
value = value:gsub('^"(.*)"$', "%1")
|
||||||
|
value = value:gsub("^'(.*)'$", "%1")
|
||||||
|
values[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
file:close()
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
|
||||||
|
function keyboard.vconsole()
|
||||||
|
return read_values("/etc/vconsole.conf")
|
||||||
|
end
|
||||||
|
|
||||||
|
function keyboard.installed()
|
||||||
|
local values = keyboard.vconsole()
|
||||||
|
if read_values("/etc/omarchy/input-method").XKB_LAYOUT == "kr"
|
||||||
|
and (values.XKBLAYOUT or "us") == "us"
|
||||||
|
and (values.XKBVARIANT or "") == "" then
|
||||||
|
values.XKBLAYOUT, values.XKBVARIANT = "kr", ""
|
||||||
|
end
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Japanese (JIS) keyboards put +, ^, and the Henkan/Muhenkan keys where US
|
||||||
|
-- keyboards don't, so a few defaults only apply when that layout leads.
|
||||||
|
function keyboard.jis()
|
||||||
|
local layout = keyboard.installed().XKBLAYOUT or "us"
|
||||||
|
return layout:match("^[^,]*") == "jp"
|
||||||
|
end
|
||||||
|
|
||||||
|
return keyboard
|
||||||
@@ -8,6 +8,8 @@ require("default.hypr.autostart")
|
|||||||
if _G.omarchy_default_bindings ~= false then
|
if _G.omarchy_default_bindings ~= false then
|
||||||
require("default.hypr.bindings.media")
|
require("default.hypr.bindings.media")
|
||||||
require("default.hypr.bindings.clipboard")
|
require("default.hypr.bindings.clipboard")
|
||||||
|
require("default.hypr.bindings.input-method")
|
||||||
|
require("default.hypr.bindings.jis")
|
||||||
require("default.hypr.bindings.tiling")
|
require("default.hypr.bindings.tiling")
|
||||||
require("default.hypr.bindings.utilities")
|
require("default.hypr.bindings.utilities")
|
||||||
require("default.hypr.bindings.voxtype")
|
require("default.hypr.bindings.voxtype")
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
"""One Fcitx setup path for offline finalization and a running desktop."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(os.environ["OMARCHY_PATH"])
|
||||||
|
PRESETS = json.loads((ROOT / "default/input-methods/presets.json").read_text())
|
||||||
|
NON_LATIN = set("af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua".split())
|
||||||
|
CONTROLLER = ["busctl", "--user", "call", "org.fcitx.Fcitx5", "/controller", "org.fcitx.Fcitx.Controller1"]
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, **kwargs):
|
||||||
|
return subprocess.run(args, check=True, text=True, capture_output=True, **kwargs).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
return path.read_text() if path.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def values(text):
|
||||||
|
result = {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
match = re.match(r"^\s*([\w ]+)\s*=\s*(.*?)\s*(?:\s+#.*)?$", line)
|
||||||
|
if match:
|
||||||
|
result[match[1].strip()] = match[2].strip("\"'")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sections(text):
|
||||||
|
result = {}
|
||||||
|
current = ""
|
||||||
|
for line in text.splitlines():
|
||||||
|
match = re.match(r"^\s*\[([^]]+)\]\s*$", line)
|
||||||
|
if match:
|
||||||
|
current = match[1]
|
||||||
|
result.setdefault(current, {})
|
||||||
|
elif current:
|
||||||
|
result[current].update(values(line))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_write(path, text):
|
||||||
|
# Follow a user's config symlink rather than replacing the link itself.
|
||||||
|
path = path.resolve()
|
||||||
|
if read(path) == text:
|
||||||
|
return False
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
mode = path.stat().st_mode & 0o777 if path.exists() else 0o644
|
||||||
|
fd, temporary = tempfile.mkstemp(prefix=".omarchy-input-", dir=path.parent)
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w") as file:
|
||||||
|
file.write(text)
|
||||||
|
os.chmod(temporary, mode)
|
||||||
|
os.replace(temporary, path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temporary):
|
||||||
|
os.unlink(temporary)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def key_list(entries):
|
||||||
|
# Fcitx canonicalizes modifiers (e.g. Shift+Super instead of Super+Shift).
|
||||||
|
return {frozenset(value.split("+")) for value in entries.values()}
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config_home, fresh=False):
|
||||||
|
changed = False
|
||||||
|
files = ["config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]
|
||||||
|
for name in files:
|
||||||
|
target = config_home / "fcitx5" / name
|
||||||
|
original = read(target)
|
||||||
|
shipped = read(ROOT / "config/fcitx5" / name)
|
||||||
|
if name == "config" or name == "conf/quickphrase.conf":
|
||||||
|
existing = sections(original)
|
||||||
|
additions = []
|
||||||
|
for section, entries in sections(shipped).items():
|
||||||
|
# Missing hotkey sections also represent a choice: Fcitx's defaults.
|
||||||
|
if not fresh and section.startswith("Hotkey/"):
|
||||||
|
continue
|
||||||
|
# Existing lists (including explicitly empty lists) are user choices.
|
||||||
|
# A fresh profile has no working IME shortcut to preserve.
|
||||||
|
stock_lists = {
|
||||||
|
"Hotkey/TriggerKeys": {"0": "Control+space", "1": "Zenkaku_Hankaku", "2": "Hangul"},
|
||||||
|
"Hotkey/ActivateKeys": {"0": "Hangul_Hanja"},
|
||||||
|
"Hotkey/DeactivateKeys": {"0": "Hangul_Romaja"},
|
||||||
|
"Hotkey/AltTriggerKeys": {"0": "Shift_L"},
|
||||||
|
"Hotkey/EnumerateGroupForwardKeys": {"0": "Super+space"},
|
||||||
|
"Hotkey/EnumerateGroupBackwardKeys": {"0": "Super+Shift+space"},
|
||||||
|
"TriggerKey": {"0": "Super+grave", "1": "Super+semicolon"},
|
||||||
|
}
|
||||||
|
if fresh and section in existing and section in stock_lists and (key_list(existing[section]) == key_list(stock_lists[section]) or (not existing[section] and section in {"Hotkey/ActivateKeys", "Hotkey/DeactivateKeys"})):
|
||||||
|
original = re.sub(r"(?ms)^\[" + re.escape(section) + r"\]\s*\n.*?(?=^\[|\Z)", "", original)
|
||||||
|
existing.pop(section)
|
||||||
|
if section not in existing:
|
||||||
|
additions.append(f"[{section}]\n" + "".join(f"{key}={value}\n" for key, value in entries.items()))
|
||||||
|
if additions:
|
||||||
|
changed |= atomic_write(target, original.rstrip() + ("\n\n" if original.strip() else "") + "\n".join(additions))
|
||||||
|
elif name == "conf/pinyin.conf":
|
||||||
|
# Suppress the first-use cloud prompt while preserving any explicit
|
||||||
|
# cloud-prediction preference and all other existing engine settings.
|
||||||
|
if not original:
|
||||||
|
updated = shipped
|
||||||
|
elif "FirstRun" in values(original):
|
||||||
|
updated = re.sub(r"(?m)^\s*FirstRun\s*=.*$", "FirstRun=False", original)
|
||||||
|
else:
|
||||||
|
updated = "FirstRun=False\n" + original
|
||||||
|
changed |= atomic_write(target, updated)
|
||||||
|
elif not target.exists() and not target.is_symlink():
|
||||||
|
changed |= atomic_write(target, shipped)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def activation_default():
|
||||||
|
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
|
||||||
|
path = data_home / "dbus-1/services/org.fcitx.Fcitx5.service"
|
||||||
|
original = read(path)
|
||||||
|
# Repair the package's plain activation entry, but retain custom launchers.
|
||||||
|
packaged = values(original).get("Exec") in ("/usr/bin/fcitx5", "/usr/bin/fcitx5 -d")
|
||||||
|
if not original or (packaged and "SystemdService" not in values(original)):
|
||||||
|
return atomic_write(path, read(ROOT / "default/input-methods/org.fcitx.Fcitx5.service"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(config_home, live=False):
|
||||||
|
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
|
||||||
|
paths = [config_home / "fcitx5" / name for name in ["profile", "config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]]
|
||||||
|
paths += [config_home / "fontconfig/conf.d/50-omarchy-input-method.conf", data_home / "dbus-1/services/org.fcitx.Fcitx5.service"]
|
||||||
|
before = {path.resolve(): path.read_text() if path.exists() else None for path in paths}
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except BaseException as setup_error:
|
||||||
|
if live:
|
||||||
|
# Stop before restoring disk: a graceful stop saves the in-memory profile.
|
||||||
|
try:
|
||||||
|
run(["systemctl", "--user", "stop", "omarchy-fcitx5.service"])
|
||||||
|
subprocess.run(["pkill", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
for attempt in range(50):
|
||||||
|
if subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Fcitx did not stop for rollback; previous settings could not safely be restored")
|
||||||
|
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
|
||||||
|
raise RuntimeError(f"Could not stop Fcitx for input rollback: {error} (setup failed: {setup_error})") from error
|
||||||
|
for path, original in before.items():
|
||||||
|
if original is None:
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
atomic_write(path, original)
|
||||||
|
if live:
|
||||||
|
# Restore memory as well as disk; otherwise Fcitx saves the failed change
|
||||||
|
# again on exit. Bus activation caches also need to forget the new entry.
|
||||||
|
reload_bus()
|
||||||
|
try:
|
||||||
|
run(["omarchy-restart-xcompose"])
|
||||||
|
run(CONTROLLER + ["ReloadConfig"])
|
||||||
|
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
|
||||||
|
print(f"Could not reload restored input settings: {error}", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def selection():
|
||||||
|
vconsole = values(read(Path(os.environ.get("OMARCHY_VCONSOLE", "/etc/vconsole.conf"))))
|
||||||
|
preference = values(read(Path(os.environ.get("OMARCHY_INPUT_SELECTION", "/etc/omarchy/input-method"))))
|
||||||
|
layout = vconsole.get("XKBLAYOUT", "us").split(",")[0] or "us"
|
||||||
|
variant = vconsole.get("XKBVARIANT", "").split(",")[0]
|
||||||
|
if preference.get("XKB_LAYOUT") == "kr" and vconsole.get("XKBLAYOUT", "us") == "us" and not variant:
|
||||||
|
layout, variant = "kr", ""
|
||||||
|
method = preference.get("INPUT_METHOD", "mozc" if layout == "jp" else "none")
|
||||||
|
if method not in PRESETS:
|
||||||
|
raise ValueError(f"Unknown installed input method: {method}")
|
||||||
|
if layout in NON_LATIN:
|
||||||
|
layout, variant = "us", ""
|
||||||
|
group_layout = layout + (f"-{variant}" if variant else "")
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9_-]+", group_layout):
|
||||||
|
raise ValueError("Invalid installed keyboard layout")
|
||||||
|
return method, group_layout
|
||||||
|
|
||||||
|
|
||||||
|
def stock_profile(text):
|
||||||
|
if not text.strip():
|
||||||
|
return True
|
||||||
|
groups = sections(text)
|
||||||
|
items = [data.get("Name", "") for key, data in groups.items() if re.fullmatch(r"Groups/\d+/Items/\d+", key)]
|
||||||
|
group = groups.get("Groups/0", {})
|
||||||
|
return (items == ["keyboard-us"]
|
||||||
|
and list(key for key in groups if re.fullmatch(r"Groups/\d+", key)) == ["Groups/0"]
|
||||||
|
and group.get("Name", "Default") == "Default"
|
||||||
|
and group.get("Default Layout", "us") == "us"
|
||||||
|
and not groups.get("Groups/0/Items/0", {}).get("Layout"))
|
||||||
|
|
||||||
|
|
||||||
|
def profile_text(layout, method):
|
||||||
|
default_im = method if method != "none" else f"keyboard-{layout}"
|
||||||
|
text = f"[Groups/0]\nName=Default\nDefault Layout={layout}\nDefaultIM={default_im}\n\n[Groups/0/Items/0]\nName=keyboard-{layout}\nLayout=\n"
|
||||||
|
if method != "none":
|
||||||
|
text += f"\n[Groups/0/Items/1]\nName={method}\nLayout=\n"
|
||||||
|
return text + "\n[GroupOrder]\n0=Default\n"
|
||||||
|
|
||||||
|
|
||||||
|
def font_default(config_home, method):
|
||||||
|
variant = PRESETS[method]["font"]
|
||||||
|
path = config_home / "fontconfig/conf.d/50-omarchy-input-method.conf"
|
||||||
|
if not variant or path.exists() or path.is_symlink():
|
||||||
|
return
|
||||||
|
# Only CJK fallback changes; retain the interface and terminal's Latin font.
|
||||||
|
rules = []
|
||||||
|
for families, face in [("sans-serif|Liberation Sans", "Sans"), ("serif|Liberation Serif", "Serif"), ("monospace|JetBrainsMono Nerd Font", "Sans Mono")]:
|
||||||
|
for family in families.split("|"):
|
||||||
|
rules.append(f'<alias><family>{family}</family><accept><family>Noto {face} CJK {variant}</family></accept></alias>')
|
||||||
|
atomic_write(path, '<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n<fontconfig>\n' + "\n".join(rules) + "\n</fontconfig>\n")
|
||||||
|
|
||||||
|
|
||||||
|
def live_group():
|
||||||
|
group = run(["fcitx5-remote", "-q"])
|
||||||
|
info = json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["InputMethodGroupInfo", "s", group]))["data"]
|
||||||
|
layout, items = info[:2]
|
||||||
|
return group, layout, items
|
||||||
|
|
||||||
|
|
||||||
|
def live_set(group, layout, items):
|
||||||
|
args = [group, layout, str(len(items))]
|
||||||
|
for name, item_layout in items:
|
||||||
|
args.extend([name, item_layout])
|
||||||
|
run(CONTROLLER + ["SetInputMethodGroupInfo", "ssa(ss)"] + args)
|
||||||
|
run(CONTROLLER + ["Save"])
|
||||||
|
|
||||||
|
|
||||||
|
def reload_bus():
|
||||||
|
# Offline finalization and updates over SSH may have no session bus.
|
||||||
|
try:
|
||||||
|
run(["busctl", "--user", "call", "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ReloadConfig"])
|
||||||
|
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
|
||||||
|
print(f"Input activation reload deferred until login: {error}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def available_methods():
|
||||||
|
return json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["AvailableInputMethods"]))["data"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def wait_ready(method):
|
||||||
|
for attempt in range(100):
|
||||||
|
try:
|
||||||
|
available = available_methods()
|
||||||
|
if any(item[0] == method for item in available) and live_group()[2]:
|
||||||
|
return
|
||||||
|
except (OSError, ValueError, KeyError, IndexError, subprocess.CalledProcessError):
|
||||||
|
pass
|
||||||
|
time.sleep(0.1)
|
||||||
|
raise RuntimeError(f"Fcitx did not load the {method} engine after restart")
|
||||||
|
|
||||||
|
|
||||||
|
def live_append(method):
|
||||||
|
group, layout, items = live_group()
|
||||||
|
if any(item[0] == method for item in items):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
live_set(group, layout, items + [[method, ""]])
|
||||||
|
if not any(item[0] == method for item in live_group()[2]):
|
||||||
|
raise RuntimeError(f"Fcitx did not retain the {method} engine")
|
||||||
|
except BaseException:
|
||||||
|
live_set(group, layout, items)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def select_live_method(method, profile):
|
||||||
|
# Clients reconnect asynchronously after restart. SetCurrentIM silently does
|
||||||
|
# nothing without a focused input context, so verify the saved group default.
|
||||||
|
for attempt in range(30):
|
||||||
|
run(["fcitx5-remote", "-s", method])
|
||||||
|
run(["fcitx5-remote", "-c"])
|
||||||
|
run(CONTROLLER + ["Save"])
|
||||||
|
group = run(["fcitx5-remote", "-q"])
|
||||||
|
for section, entries in sections(read(profile)).items():
|
||||||
|
if re.fullmatch(r"Groups/\d+", section) and entries.get("Name") == group and entries.get("DefaultIM") == method:
|
||||||
|
return
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(f"Input method added; select {method} in Fcitx settings to switch to it.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def configure(args):
|
||||||
|
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
|
||||||
|
running = subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||||
|
# Finalization can be retried after login. A live daemon owns its profile.
|
||||||
|
if args.seed and running:
|
||||||
|
return
|
||||||
|
with transaction(config_home, live=running and not args.defaults):
|
||||||
|
configure_inner(args, config_home, running)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_inner(args, config_home, running):
|
||||||
|
if args.seed or args.defaults:
|
||||||
|
method, layout = selection()
|
||||||
|
else:
|
||||||
|
method, layout = args.method or selection()[0], None
|
||||||
|
if args.method:
|
||||||
|
method = args.method
|
||||||
|
profile = config_home / "fcitx5/profile"
|
||||||
|
original = read(profile)
|
||||||
|
if args.seed and not stock_profile(original):
|
||||||
|
return
|
||||||
|
if not args.seed and not args.defaults and not running:
|
||||||
|
raise RuntimeError("Log in to an Omarchy desktop before changing input methods")
|
||||||
|
if not args.seed and not args.defaults and method == "none":
|
||||||
|
raise ValueError("Choose an input engine to add")
|
||||||
|
|
||||||
|
if args.seed:
|
||||||
|
atomic_write(profile, profile_text(layout, method))
|
||||||
|
defaults(config_home, fresh=True)
|
||||||
|
activation_default()
|
||||||
|
font_default(config_home, method)
|
||||||
|
elif args.defaults:
|
||||||
|
fresh = stock_profile(original)
|
||||||
|
changed = defaults(config_home, fresh=fresh)
|
||||||
|
if activation_default():
|
||||||
|
reload_bus()
|
||||||
|
# Upgrades can run through sudo/SSH without access to the live user's bus.
|
||||||
|
# Never overwrite its profile or restart input in that context.
|
||||||
|
if fresh:
|
||||||
|
if running:
|
||||||
|
try:
|
||||||
|
group, old_layout, items = live_group()
|
||||||
|
if old_layout == "us" and items == [["keyboard-us", ""]]:
|
||||||
|
new_items = [[f"keyboard-{layout}", ""]]
|
||||||
|
if method != "none":
|
||||||
|
if any(item[0] == method for item in available_methods()):
|
||||||
|
new_items.append([method, ""])
|
||||||
|
else:
|
||||||
|
print(f"Finish input setup after login: omarchy setup input {method}", file=sys.stderr)
|
||||||
|
live_set(group, layout, new_items)
|
||||||
|
if live_group()[1:] != (layout, new_items):
|
||||||
|
live_set(group, old_layout, items)
|
||||||
|
raise RuntimeError("Fcitx did not retain the installed keyboard settings")
|
||||||
|
except (OSError, ValueError, KeyError, IndexError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||||
|
print(f"Input profile alignment deferred until login: {error}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
atomic_write(profile, profile_text(layout, method))
|
||||||
|
if running and changed:
|
||||||
|
try:
|
||||||
|
run(CONTROLLER + ["ReloadConfig"])
|
||||||
|
except (OSError, subprocess.CalledProcessError) as error:
|
||||||
|
print(f"Input defaults reload deferred until login: {error}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
# New addons are discovered only at startup. Supervision remains with the
|
||||||
|
# existing unit; never spawn a second fcitx instance ourselves.
|
||||||
|
activation_default()
|
||||||
|
defaults(config_home, fresh=stock_profile(original))
|
||||||
|
reload_bus()
|
||||||
|
run(["omarchy-restart-xcompose"])
|
||||||
|
wait_ready(method)
|
||||||
|
live_append(method)
|
||||||
|
# The selected language becomes the next active engine; leave Latin input
|
||||||
|
# active until the user deliberately switches. Keep all other group items.
|
||||||
|
select_live_method(method, profile)
|
||||||
|
font_default(config_home, method)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
commands = parser.add_subparsers(dest="command", required=True)
|
||||||
|
commands.add_parser("status")
|
||||||
|
configure_parser = commands.add_parser("configure")
|
||||||
|
mode = configure_parser.add_mutually_exclusive_group()
|
||||||
|
mode.add_argument("--seed", action="store_true")
|
||||||
|
mode.add_argument("--defaults", action="store_true")
|
||||||
|
configure_parser.add_argument("method", nargs="?", choices=PRESETS)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.command == "status":
|
||||||
|
method, layout = selection()
|
||||||
|
print(json.dumps({"installed_method": method, "layout": layout}))
|
||||||
|
else:
|
||||||
|
configure(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||||
|
raise SystemExit(f"Input method setup failed: {error}")
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Live Fcitx state for the existing keyboard layout widget."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
|
||||||
|
BUS = "org.fcitx.Fcitx5"
|
||||||
|
INTERFACE = "org.fcitx.Fcitx.Controller1"
|
||||||
|
|
||||||
|
|
||||||
|
def call(bus, method, args=None):
|
||||||
|
# Reading the bar must neither activate a stopped service nor wait indefinitely.
|
||||||
|
return bus.call_sync(BUS, "/controller", INTERFACE, method, args, None,
|
||||||
|
Gio.DBusCallFlags.NO_AUTO_START, 1000, None).unpack()
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(bus):
|
||||||
|
group = call(bus, "CurrentInputMethodGroup")[0]
|
||||||
|
methods = list(dict.fromkeys(item[0] for item in
|
||||||
|
call(bus, "InputMethodGroupInfo", GLib.Variant("(s)", (group,)))[1]))
|
||||||
|
info = call(bus, "CurrentInputMethodInfo")
|
||||||
|
return {"methods": methods, "current": info[0], "name": info[1], "label": info[4], "language": info[5]}
|
||||||
|
|
||||||
|
|
||||||
|
def cycle(bus):
|
||||||
|
state = snapshot(bus)
|
||||||
|
methods = state["methods"]
|
||||||
|
if len(methods) > 1 and state["current"] in methods:
|
||||||
|
following = methods[(methods.index(state["current"]) + 1) % len(methods)]
|
||||||
|
call(bus, "SetCurrentIM", GLib.Variant("(s)", (following,)))
|
||||||
|
|
||||||
|
|
||||||
|
class Indicator:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
self.pending = ""
|
||||||
|
self.last = ""
|
||||||
|
self.entries = {}
|
||||||
|
|
||||||
|
def select_next(self):
|
||||||
|
state = snapshot(self.bus)
|
||||||
|
methods = state["methods"]
|
||||||
|
if len(methods) < 2:
|
||||||
|
return
|
||||||
|
current = self.pending or state["current"] or self.last or methods[0]
|
||||||
|
if current not in methods:
|
||||||
|
current = methods[0]
|
||||||
|
self.pending = methods[(methods.index(current) + 1) % len(methods)]
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
state = snapshot(self.bus)
|
||||||
|
methods = state["methods"]
|
||||||
|
if self.pending not in methods:
|
||||||
|
self.pending = ""
|
||||||
|
if self.pending and state["current"]:
|
||||||
|
# Before the first application gains focus Fcitx has no input context.
|
||||||
|
# Retain the click until it has one, without changing startup defaults.
|
||||||
|
call(self.bus, "SetCurrentIM", GLib.Variant("(s)", (self.pending,)))
|
||||||
|
state = snapshot(self.bus)
|
||||||
|
if state["current"] == self.pending:
|
||||||
|
self.pending = ""
|
||||||
|
if state["current"]:
|
||||||
|
self.last = state["current"]
|
||||||
|
else:
|
||||||
|
chosen = self.pending or (self.last if self.last in methods else "") or next(iter(methods), "")
|
||||||
|
if chosen not in self.entries:
|
||||||
|
self.entries = {entry[0]: entry for entry in call(self.bus, "AvailableInputMethods")[0]}
|
||||||
|
entry = self.entries.get(chosen)
|
||||||
|
if entry:
|
||||||
|
state.update(current=entry[0], name=entry[1], label=entry[4], language=entry[5])
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def watch(bus):
|
||||||
|
previous = None
|
||||||
|
indicator = Indicator(bus)
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
buffer = ""
|
||||||
|
timer = 0
|
||||||
|
|
||||||
|
def refresh():
|
||||||
|
nonlocal previous
|
||||||
|
try:
|
||||||
|
state = indicator.refresh()
|
||||||
|
except GLib.Error:
|
||||||
|
indicator.pending = ""
|
||||||
|
indicator.last = ""
|
||||||
|
state = {"methods": [], "current": "", "name": "", "language": ""}
|
||||||
|
line = json.dumps(state, ensure_ascii=False)
|
||||||
|
if line != previous:
|
||||||
|
print(line, flush=True)
|
||||||
|
previous = line
|
||||||
|
|
||||||
|
def poll():
|
||||||
|
nonlocal timer
|
||||||
|
refresh()
|
||||||
|
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
def command(source, condition):
|
||||||
|
nonlocal buffer, timer
|
||||||
|
data = os.read(sys.stdin.fileno(), 4096)
|
||||||
|
if not data:
|
||||||
|
loop.quit()
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
buffer += data.decode()
|
||||||
|
while "\n" in buffer:
|
||||||
|
line, buffer = buffer.split("\n", 1)
|
||||||
|
if line == "cycle":
|
||||||
|
try:
|
||||||
|
indicator.select_next()
|
||||||
|
except GLib.Error:
|
||||||
|
indicator.pending = ""
|
||||||
|
refresh()
|
||||||
|
GLib.source_remove(timer)
|
||||||
|
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
|
||||||
|
return GLib.SOURCE_CONTINUE
|
||||||
|
|
||||||
|
# Fcitx's controller has no current-method-changed signal. Keep one bus
|
||||||
|
# connection open instead of spawning a command on every poll. Polling also
|
||||||
|
# follows per-application input contexts and recovers after service restarts.
|
||||||
|
GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_HUP, command)
|
||||||
|
poll()
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
|
if sys.argv[1:] == ["cycle"]:
|
||||||
|
cycle(bus)
|
||||||
|
else:
|
||||||
|
watch(bus)
|
||||||
|
except GLib.Error as error:
|
||||||
|
raise SystemExit(str(error))
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Installed per-user so D-Bus activation uses the supervised Omarchy unit.
|
||||||
|
[D-BUS Service]
|
||||||
|
Name=org.fcitx.Fcitx5
|
||||||
|
Exec=/usr/bin/fcitx5
|
||||||
|
SystemdService=omarchy-fcitx5.service
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"none": {"label": "Keyboard only", "packages": [], "font": ""},
|
||||||
|
"mozc": {"label": "Japanese (Mozc)", "packages": ["fcitx5-mozc"], "font": "JP"},
|
||||||
|
"hangul": {"label": "Korean (Hangul)", "packages": ["fcitx5-hangul"], "font": "KR"},
|
||||||
|
"pinyin": {"label": "Chinese (Simplified, Pinyin)", "packages": ["fcitx5-chinese-addons"], "font": "SC"},
|
||||||
|
"chewing": {"label": "Chinese (Traditional, Zhuyin)", "packages": ["fcitx5-chewing"], "font": "TC"}
|
||||||
|
}
|
||||||
@@ -128,6 +128,7 @@
|
|||||||
"setup.monitors": {"icon":"","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
|
"setup.monitors": {"icon":"","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
|
||||||
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
|
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
|
||||||
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
|
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
|
||||||
|
"setup.input-method": {"icon":"","label":"Input Language","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-input'"},
|
||||||
"setup.network": {"icon":"","label":"Network","aliases":["network"]},
|
"setup.network": {"icon":"","label":"Network","aliases":["network"]},
|
||||||
"setup.network.dns": {"icon":"","label":"DNS","aliases":["dns"]},
|
"setup.network.dns": {"icon":"","label":"DNS","aliases":["dns"]},
|
||||||
"setup.network.dns.dhcp": {"icon":"","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"},
|
"setup.network.dns.dhcp": {"icon":"","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"},
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
"setup.network.dns.google": {"icon":"","label":"Google","checked":"[[ \"$(omarchy-dns)\" == \"Google\" ]]","action":"omarchy-dns Google"},
|
"setup.network.dns.google": {"icon":"","label":"Google","checked":"[[ \"$(omarchy-dns)\" == \"Google\" ]]","action":"omarchy-dns Google"},
|
||||||
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
||||||
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell shell summon omarchy.wifiqr"},
|
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell shell summon omarchy.wifiqr"},
|
||||||
|
"setup.claude-api": {"icon":"","label":"Claude Code API","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-claude-api"},
|
||||||
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
||||||
"setup.default.agent": {"icon":"","label":"Agent","title":"Default Agent"},
|
"setup.default.agent": {"icon":"","label":"Agent","title":"Default Agent"},
|
||||||
"setup.default.agent.agy": {"icon":"","label":"Antigravity","checked":"[[ \"$(omarchy-default-agent)\" == \"agy\" ]]","action":"omarchy-default-agent agy"},
|
"setup.default.agent.agy": {"icon":"","label":"Antigravity","checked":"[[ \"$(omarchy-default-agent)\" == \"agy\" ]]","action":"omarchy-default-agent agy"},
|
||||||
@@ -143,8 +145,10 @@
|
|||||||
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
||||||
"setup.default.agent.crush": {"icon":"","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
|
"setup.default.agent.crush": {"icon":"","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
|
||||||
"setup.default.agent.cursor-agent": {"icon":"","iconFont":"omarchy","label":"Cursor CLI","checked":"[[ \"$(omarchy-default-agent)\" == \"cursor-agent\" ]]","action":"omarchy-default-agent cursor-agent"},
|
"setup.default.agent.cursor-agent": {"icon":"","iconFont":"omarchy","label":"Cursor CLI","checked":"[[ \"$(omarchy-default-agent)\" == \"cursor-agent\" ]]","action":"omarchy-default-agent cursor-agent"},
|
||||||
|
"setup.default.agent.dim": {"icon":"","label":"Dim Agent","checked":"[[ \"$(omarchy-default-agent)\" == \"dim\" ]]","action":"omarchy-default-agent dim"},
|
||||||
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
|
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
|
||||||
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
|
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
|
||||||
|
"setup.default.agent.kimi": {"icon":"","label":"Kimi Code","checked":"[[ \"$(omarchy-default-agent)\" == \"kimi\" ]]","action":"omarchy-default-agent kimi"},
|
||||||
"setup.default.agent.muse": {"icon":"","label":"Muse Code","checked":"[[ \"$(omarchy-default-agent)\" == \"muse\" ]]","action":"omarchy-default-agent muse"},
|
"setup.default.agent.muse": {"icon":"","label":"Muse Code","checked":"[[ \"$(omarchy-default-agent)\" == \"muse\" ]]","action":"omarchy-default-agent muse"},
|
||||||
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
||||||
"setup.default.agent.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","checked":"[[ \"$(omarchy-default-agent)\" == \"openclaw\" ]]","action":"omarchy-default-agent openclaw"},
|
"setup.default.agent.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","checked":"[[ \"$(omarchy-default-agent)\" == \"openclaw\" ]]","action":"omarchy-default-agent openclaw"},
|
||||||
@@ -173,6 +177,12 @@
|
|||||||
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
|
||||||
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
|
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
|
||||||
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
|
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
|
||||||
|
"setup.language": {"icon":"","label":"Language"},
|
||||||
|
"setup.language.en": {"icon":"","label":"English","checked":"[[ \"$(omarchy-language)\" == \"en_US.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language en'"},
|
||||||
|
"setup.language.zh": {"icon":"","label":"简体中文","checked":"[[ \"$(omarchy-language)\" == \"zh_CN.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language zh'"},
|
||||||
|
"setup.region": {"icon":"","label":"Region"},
|
||||||
|
"setup.region.global": {"icon":"","label":"Global","checked":"[[ \"$(omarchy-region)\" == \"Global\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region global'"},
|
||||||
|
"setup.region.china": {"icon":"","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region cn'"},
|
||||||
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
||||||
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
||||||
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
||||||
@@ -185,8 +195,8 @@
|
|||||||
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
||||||
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
||||||
"setup.security.ssh-agent": {"icon":"","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
|
"setup.security.ssh-agent": {"icon":"","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
|
||||||
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
|
||||||
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
||||||
|
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
||||||
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
|
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
|
||||||
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
|
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
|
||||||
"setup.config.xcompose": {"icon":"","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
|
"setup.config.xcompose": {"icon":"","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
|
||||||
@@ -216,6 +226,16 @@
|
|||||||
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
|
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
|
||||||
"install.windows": {"icon":"","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
"install.windows": {"icon":"","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
||||||
"install.aur": {"icon":"","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
|
"install.aur": {"icon":"","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
|
||||||
|
"install.china": {"icon":"","label":"China Apps","when":"[[ \"$(omarchy-region)\" == \"China\" ]]"},
|
||||||
|
"install.china.wechat": {"icon":"","label":"WeChat","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wechat-universal-bwrap","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wechat-universal-bwrap'"},
|
||||||
|
"install.china.qq": {"icon":"","label":"QQ","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present linuxqq","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add linuxqq'"},
|
||||||
|
"install.china.feishu": {"icon":"","label":"Feishu","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present feishu-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add feishu-bin'"},
|
||||||
|
"install.china.dingtalk": {"icon":"","label":"DingTalk","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present dingtalk-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add dingtalk-bin'"},
|
||||||
|
"install.china.wemeet": {"icon":"","label":"Tencent Meeting","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wemeet-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wemeet-bin'"},
|
||||||
|
"install.china.wps": {"icon":"","label":"WPS Office","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wps-office-cn","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wps-office-cn'"},
|
||||||
|
"install.china.tencent-docs": {"icon":"","label":"Tencent Docs","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/腾讯文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 腾讯文档 https://docs.qq.com \"\"'"},
|
||||||
|
"install.china.yuque": {"icon":"","label":"Yuque","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/语雀.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 语雀 https://www.yuque.com/dashboard \"\"'"},
|
||||||
|
"install.china.shimo": {"icon":"","label":"Shimo","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/石墨文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 石墨文档 https://shimo.im/desktop \"\"'"},
|
||||||
"install.preinstalls": {"icon":"","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
|
"install.preinstalls": {"icon":"","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
|
||||||
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
||||||
"install.browser.edge": {"icon":"","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
"install.browser.edge": {"icon":"","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
||||||
@@ -227,6 +247,7 @@
|
|||||||
"install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
|
"install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
|
||||||
"install.service.spotify": {"icon":"","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
|
"install.service.spotify": {"icon":"","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
|
||||||
"install.service.signal": {"icon":"","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
|
"install.service.signal": {"icon":"","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
|
||||||
|
"install.service.slack": {"icon":"","label":"Slack","disabled":"omarchy-pkg-present slack-desktop","action":"omarchy-install-and-launch Slack slack-desktop slack"},
|
||||||
"install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
|
"install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
|
||||||
"install.service.nordvpn": {"icon":"","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
|
"install.service.nordvpn": {"icon":"","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
|
||||||
"install.service.once": {"icon":"","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
|
"install.service.once": {"icon":"","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// 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.toggle.top-bar": {"label":"菜单栏"},
|
||||||
|
"trigger.toggle.battery-percentage": {"label":"电量百分比"},
|
||||||
|
"trigger.toggle.workspace-layout": {"label":"工作区布局"},
|
||||||
|
"trigger.toggle.window-gaps": {"label":"窗口间距"},
|
||||||
|
"trigger.toggle.one-window-ratio": {"label":"单窗口比例"},
|
||||||
|
"trigger.tests.network-speedtest": {"label":"网络测速"},
|
||||||
|
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
|
||||||
|
"style.theme": {"label":"主题"},
|
||||||
|
"style.background": {"label":"壁纸"},
|
||||||
|
"style.unlock": {"label":"解锁画面"},
|
||||||
|
"style.font": {"label":"字体"},
|
||||||
|
"style.bar": {"label":"菜单栏"},
|
||||||
|
"style.bar.position": {"label":"位置"},
|
||||||
|
"style.bar.transparency": {"label":"透明"},
|
||||||
|
"style.screensaver": {"label":"屏保"},
|
||||||
|
"style.about": {"label":"关于"},
|
||||||
|
"style.bar.position.top": {"label":"顶部"},
|
||||||
|
"style.bar.position.bottom": {"label":"底部"},
|
||||||
|
"style.bar.position.left": {"label":"左侧"},
|
||||||
|
"style.bar.position.right": {"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.input-method": {"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.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.ai.dictation": {"label":"语音听写"},
|
||||||
|
"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.ai.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":"用户"},
|
||||||
|
"trigger.toggle.animations": {"label":"动画"},
|
||||||
|
"trigger.toggle.theme-sync": {"label":"Herdr 主题同步"},
|
||||||
|
"setup.region": {"label":"地区"},
|
||||||
|
"setup.region.global": {"label":"全球"},
|
||||||
|
"setup.region.china": {"label":"中国"},
|
||||||
|
"install.china": {"label":"国内应用"},
|
||||||
|
"install.china.wechat": {"label":"微信"},
|
||||||
|
"install.china.feishu": {"label":"飞书"},
|
||||||
|
"install.china.dingtalk": {"label":"钉钉"},
|
||||||
|
"install.china.wemeet": {"label":"腾讯会议"},
|
||||||
|
"install.china.tencent-docs": {"label":"腾讯文档"},
|
||||||
|
"install.china.yuque": {"label":"语雀"},
|
||||||
|
"install.china.shimo": {"label":"石墨文档"},
|
||||||
|
"update.channel.stable": {"label":"稳定版"},
|
||||||
|
"update.channel.rc": {"label":"候选版"},
|
||||||
|
"update.channel.edge": {"label":"尝鲜版"},
|
||||||
|
"update.channel.dev": {"label":"开发版"},
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Fcitx5 input method (XCompose sequences)
|
Description=Fcitx5 input methods and XCompose sequences
|
||||||
# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for
|
# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for
|
||||||
# Wayland clients.
|
# Wayland clients.
|
||||||
#
|
#
|
||||||
@@ -19,7 +19,8 @@ PartOf=graphical-session.target
|
|||||||
ConditionEnvironment=WAYLAND_DISPLAY
|
ConditionEnvironment=WAYLAND_DISPLAY
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=dbus
|
||||||
|
BusName=org.fcitx.Fcitx5
|
||||||
# notificationitem duplicates the tray entry omarchy already renders itself.
|
# notificationitem duplicates the tray entry omarchy already renders itself.
|
||||||
ExecStart=/usr/bin/fcitx5 --disable notificationitem
|
ExecStart=/usr/bin/fcitx5 --disable notificationitem
|
||||||
# always, not on-failure: fcitx5 exits 0 when it detects another instance owning
|
# always, not on-failure: fcitx5 exits 0 when it detects another instance owning
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Input methods
|
||||||
|
|
||||||
|
The install keyboard picker selects a console keymap plus an optional Fcitx engine. `install/provisioning/setup-form.sh` is shared by the ISO configurator and deferred first-boot owner setup. Its rows are `label|console keymap|input method|desktop XKB override`; the last two fields are optional, so ordinary keyboard choices keep their existing meaning.
|
||||||
|
|
||||||
|
The ISO carries the extra fields in `omarchy_install.input_method` and `omarchy_install.input_xkb_layout`. Both installation paths persist them in `/etc/omarchy/input-method` as `INPUT_METHOD` and `XKB_LAYOUT`. This file is parsed as data, never sourced. Korean has a real US console keymap and a `kr` desktop override; Japanese uses `jp106`, or US when the US-keyboard choice is selected. Console and disk-unlock input remain ordinary keymaps.
|
||||||
|
|
||||||
|
`install/user/input-method.sh` calls `omarchy-input-method configure --seed` during user finalization, before graphical login. The standard engines are in the base package set so deferred provisioning and factory resets do not need network access to configure typing. `default/input-methods/presets.json` is the engine catalog used by the post-install selector.
|
||||||
|
|
||||||
|
`default/input-methods/configure.py` owns the shared configuration behavior. Offline seeding replaces only a missing profile or Fcitx's untouched single `keyboard-us` guess. The group layout matches Hyprland's effective leading layout and variant; non-Latin layouts retain the US-leading behavior used for desktop bindings. The selected composition engine is the group's `DefaultIM`, with `ActiveByDefault=False` so initial typing remains Latin. Existing multilingual profiles and explicitly chosen keyboard layouts are preserved.
|
||||||
|
|
||||||
|
`omarchy setup input` installs the selected engine, restarts the supervised Fcitx unit so it discovers new addons, and appends the engine through `InputMethodGroupInfo`, `SetInputMethodGroupInfo`, and `Save`. It keeps the group name, layout, existing methods, and their layout overrides. There is no parallel unsupervised daemon. A per-user D-Bus activation file delegates `org.fcitx.Fcitx5` to `omarchy-fcitx5.service`; it is loaded before live restart.
|
||||||
|
|
||||||
|
Shortcut defaults reserve Ctrl+Space for Tmux/Herdr, Super+Space for the menu, Super+Shift+Space for the bar, and Super+grave for the console. Input toggles on Ctrl+Shift+Space, Zenkaku/Hankaku, or Hangul; Henkan activates and Muhenkan deactivates. An existing configured list, including an empty list, is preserved. Stock generated lists are repaired only while the user still has the single-keyboard default profile. A terminal Ctrl+Space returns to direct input before the following key, with a 300 ms timeout and no D-Bus activation; a keyboard-only profile skips this step. Wayland and XCB addons do not override the compositor's XKB settings.
|
||||||
|
|
||||||
|
The migration installs the standard engines and applies conservative defaults. It aligns an untouched Fcitx keyboard profile with the install layout, using the live controller when Fcitx is running. It leaves existing multilingual profiles and working trigger keys alone. Atomic file writes follow configuration symlinks, preserve file modes, and roll back on a failed seed or live setup so a retry can complete. Live rollback stops Fcitx before restoring disk, then reloads activation and restarts the unit. Migration bus errors defer profile alignment to the next login and do not restart input. If a pre-update daemon has not discovered a new engine, the migration aligns only its keyboard and prints the setup command.
|
||||||
|
|
||||||
|
Input selection does not change the system locale, repository region, desktop language, or Latin interface font. A new language setup seeds a CJK fallback preference once; additional languages do not overwrite it.
|
||||||
|
|
||||||
|
Focused checks live in `test/shell.d/input-method-test.sh`, `setup-form-test.sh`, `hyprland-keyboard-layout-test.sh`, and `hyprland-jis-bindings-test.sh`. `test/acceptance.d/input-methods-test.sh` exercises real composition in a disposable installed VM and restores the original user configuration. The ISO repository's keyboard tests cover transport and target persistence. Browser input must be verified against the shipped browser versions; the language selector is not a place to guess flags from older guides.
|
||||||
|
|
||||||
|
Default key values are checked against [Fcitx global configuration](https://github.com/fcitx/fcitx5/blob/master/src/lib/fcitx/globalconfig.cpp).
|
||||||
@@ -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
|
keeps its original position in the list. New ids append. A `root` entry is
|
||||||
injected if neither file declares one.
|
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`
|
The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc`
|
||||||
(refreshed into `~/.config/`) documents the format in its header and ships
|
(refreshed into `~/.config/`) documents the format in its header and ships
|
||||||
only comments, so the default state adds nothing.
|
only comments, so the default state adds nothing.
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
# Temporary passwordless sudo
|
# Passwordless sudo
|
||||||
|
|
||||||
`omarchy-sudo-passwordless` publishes a bounded grant for the numeric UID authenticated by sudo. Its user interface runs without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
|
`omarchy-sudo-passwordless` publishes a timed or permanent grant for the numeric UID authenticated by sudo. Its user interface inspects sudo policy without prompting and authenticates only the enable action when access is off, without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
|
||||||
|
|
||||||
|
The menu bar's `PasswordlessSudo` indicator polls `--active` every five seconds and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow.
|
||||||
|
|
||||||
## Grant lifecycle
|
## Grant lifecycle
|
||||||
|
|
||||||
The sudoers rule is the only grant record: it contains the resolved account name and a UTC `NOTAFTER` deadline enforced by sudo itself, including after suspend. Publication validates a dot-prefixed temporary file with `visudo`, arms a calendar cleanup timer, then atomically renames the complete rule into place. There is no separate per-user state file to publish, parse, or reconcile. Failure after renewal starts removes the old grant; failed revocation remains an error and leaves the cleanup timer armed.
|
The sudoers rule is the only grant record. A timed grant contains the resolved account name and a UTC `NOTAFTER` deadline enforced by sudo itself, including after suspend. Publication validates a dot-prefixed temporary file with `visudo`, arms a calendar cleanup timer, then atomically renames the complete rule into place. There is no separate per-user state file to publish, parse, or reconcile. Failure after renewal starts removes the old grant; failed revocation remains an error and leaves the cleanup timer armed.
|
||||||
|
|
||||||
An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface only offers a new grant after result `3`. It must not turn an inspection failure into a claim that no grant exists.
|
An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface offers a duration picker after result `3`. Result `0` toggles access off when no argument is given, or changes its duration when an argument is given. Permanent access always requires confirmation, including when replacing an active timed grant. It must not turn an inspection failure into a claim that no grant exists.
|
||||||
|
|
||||||
Calendar timers clean up expired files; their liveness does not define authorization. Callbacks read the current rule and remove it only when expired. Earlier callbacks cannot shorten a renewed grant, so no timer identity needs to be persisted. Old UID-only and token-bearing callbacks remain accepted. Pending callbacks after renewal or manual disable are harmless and expire within the maximum 24-hour grant window. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; routine non-boot tmpfiles maintenance leaves live grants alone.
|
Calendar timers clean up expired files; their liveness does not define authorization. Callbacks read the current rule and remove it only when expired. Earlier callbacks cannot shorten a renewed grant, so no timer identity needs to be persisted. Old UID-only and token-bearing callbacks remain accepted. Pending callbacks after renewal or manual disable are harmless and expire within the maximum 24-hour grant window. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; routine non-boot tmpfiles maintenance leaves live grants alone.
|
||||||
|
|
||||||
Legacy cleanup uses a root-owned machine marker under `/var/lib/omarchy/migrations/`, written only after successful cleanup under the grant lock. Later accounts can finish their migration queues without sudo and without revoking grants created after the repair. Old grant state files are no longer consulted. A legacy grant is recognized by its exact filename and rule relationship, since the old command wrote the caller's unvalidated name into both, so accounts outside the current name policy are still cleaned up. The generated filename prefix is reserved: boot cleanup and the package hook already remove everything under it, and the old writer could emit a rule whose body differs from its filename, so the migration moves any other file found there into a fresh root-only directory under `/var/lib/omarchy/sudoers-quarantine/`, as `policy` with the original name stored beside it, rather than leaving it live or deleting its content.
|
Legacy cleanup uses a root-owned machine marker under `/var/lib/omarchy/migrations/`, written only after successful cleanup under the grant lock. Later accounts can finish their migration queues without sudo and without revoking grants created after the repair. Old grant state files are no longer consulted. A legacy grant is recognized by its exact filename and rule relationship, since the old command wrote the caller's unvalidated name into both, so accounts outside the current name policy are still cleaned up. The generated filename prefix is reserved: boot cleanup and the package hook already remove everything under it, and the old writer could emit a rule whose body differs from its filename, so the migration moves any other file found there into a fresh root-only directory under `/var/lib/omarchy/sudoers-quarantine/`, as `policy` with the original name stored beside it, rather than leaving it live or deleting its content.
|
||||||
|
|
||||||
|
Permanent grants live under `/etc/sudoers.d/99-omarchy-permanent-nopasswd-<uid>`, outside the temporary cleanup namespace. They have no `NOTAFTER` deadline or timer. Status and disable handle both forms; switching duration publishes the replacement before removing the prior policy under the same lock. Old expiry callbacks leave permanent grants intact. If an untrappable interruption leaves both rules, a pair of trusted generated rules for the same account remains inspectable and revocable. Permanent policy continues to govern until an explicit disable or completed duration change; callbacks never remove it merely because the timed companion has expired. Mismatched accounts, symlinks, and administrator-modified companion rules remain errors. Permanent policy survives reboot and package upgrade or removal; disable it before uninstalling the command or downgrading to a version without permanent-grant support, or remove its sudoers file as an administrator afterward.
|
||||||
|
|
||||||
## Package ownership
|
## Package ownership
|
||||||
|
|
||||||
The packaging companion must put the publication/expiry command, `omarchy-security-functions`, `omarchy-nopasswd-sudo.conf`, and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
|
The packaging companion must put the publication/expiry command, `omarchy-security-functions`, `omarchy-nopasswd-sudo.conf`, and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
|
||||||
|
|
||||||
Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing policy. The marker prevents a waiting publisher from creating a new grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another grant.
|
Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing temporary policy. The marker prevents a waiting publisher from creating a new timed grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New timed grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another timed grant.
|
||||||
|
|
||||||
The runtime marker need not survive reboot: pre-removal revokes the old grants before package files disappear, and a new invocation independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch.
|
The runtime marker need not survive reboot: pre-removal revokes the old timed grants before package files disappear, and a new timed grant independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -34,11 +34,7 @@ For China, the builder installs `archlinuxcn-keyring` using the existing Arch tr
|
|||||||
|
|
||||||
## Scope of the China profile
|
## Scope of the China profile
|
||||||
|
|
||||||
The China profile provides repository defaults and developer mirrors. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones.
|
The China profile provides repository defaults only. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones.
|
||||||
|
|
||||||
## Developer mirrors
|
|
||||||
|
|
||||||
The `cn` profile also points the common package managers at mirrors inside China: `environment.conf` (npm, PyPI and uv, Go, rustup, Bundler, and Maven through `MAVEN_ARGS`), `cargo.toml` for crates.io, `gemrc` for RubyGems, and `maven-settings.xml`, which Maven reads in place. During installation `install/config/region.sh` links the first three to `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml` and `/etc/gemrc` when the marker says `cn`. It refuses to replace a regular file it did not create. The environment reaches sessions started after login.
|
|
||||||
|
|
||||||
## Remaining release validation
|
## Remaining release validation
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ omarchy-update
|
|||||||
├─ omarchy-update-stay-awake start
|
├─ omarchy-update-stay-awake start
|
||||||
├─ run system-package updates
|
├─ run system-package updates
|
||||||
├─ run migrations
|
├─ run migrations
|
||||||
├─ run orphan review and log analysis
|
├─ automatically remove orphan packages and run log analysis
|
||||||
├─ omarchy-update-status
|
├─ omarchy-update-status
|
||||||
│ └─ refresh or clear the shell update indicator
|
│ └─ refresh or clear the shell update indicator
|
||||||
├─ restart marked services and the shell
|
├─ restart marked services and the shell
|
||||||
@@ -158,7 +158,7 @@ Important behavior:
|
|||||||
- This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow.
|
- This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow.
|
||||||
- Sleep inhibition authenticates before detaching, drops the held command back to the caller, and closes both update lock descriptors before the persistent process starts. Cleanup accepts only caller-owned, mode-0600, single-link state and revalidates the recorded PID, process start time, owner, and random token immediately before every signal.
|
- Sleep inhibition authenticates before detaching, drops the held command back to the caller, and closes both update lock descriptors before the persistent process starts. Cleanup accepts only caller-owned, mode-0600, single-link state and revalidates the recorded PID, process start time, owner, and random token immediately before every signal.
|
||||||
- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit.
|
- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit.
|
||||||
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Interactive review steps (orphan removal, conflict handoff) report and skip instead of blocking. Privileged commands still require the one sudo authorization, and AUR installs can prompt separately.
|
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Conflict handoff reports and skips instead of blocking. Orphan packages are automatically removed during every update. Privileged commands still require the one sudo authorization, and AUR installs can prompt separately.
|
||||||
- The free-space requirement uses a 10 GiB threshold and stops the update before
|
- The free-space requirement uses a 10 GiB threshold and stops the update before
|
||||||
confirmation when it is not met. If free space cannot be determined, the
|
confirmation when it is not met. If free space cannot be determined, the
|
||||||
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
|
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
|
||||||
@@ -306,7 +306,7 @@ scripts.
|
|||||||
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
|
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
|
||||||
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
|
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
|
||||||
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
|
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
|
||||||
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
|
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal unless passed `-y`, as the update pipeline does; standalone noninteractive mode only reports. | **Keep for now.** Automates cleanup during updates and supports standalone review. |
|
||||||
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
|
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
|
||||||
| `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
|
| `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
|
||||||
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
|
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
|
||||||
|
|||||||
@@ -4,14 +4,23 @@ cn_links=(
|
|||||||
"gemrc /etc/gemrc"
|
"gemrc /etc/gemrc"
|
||||||
)
|
)
|
||||||
|
|
||||||
# The installer writes the marker from the timezone.
|
# The installer writes the marker; omarchy-region passes the region it switches to.
|
||||||
region=global
|
region=global
|
||||||
if [[ -e /etc/omarchy/region ]]; then
|
if [[ -e /etc/omarchy/region ]]; then
|
||||||
region=$(</etc/omarchy/region)
|
region=$(</etc/omarchy/region)
|
||||||
fi
|
fi
|
||||||
|
region=${OMARCHY_REGION:-$region}
|
||||||
|
|
||||||
case $region in
|
case $region in
|
||||||
global) ;;
|
global)
|
||||||
|
rm -f /etc/omarchy/region
|
||||||
|
for link in "${cn_links[@]}"; do
|
||||||
|
read -r _ target <<<"$link"
|
||||||
|
if [[ -L $target && $(readlink "$target") == */default/regions/cn/* ]]; then
|
||||||
|
rm "$target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
cn)
|
cn)
|
||||||
for link in "${cn_links[@]}"; do
|
for link in "${cn_links[@]}"; do
|
||||||
read -r _ target <<<"$link"
|
read -r _ target <<<"$link"
|
||||||
@@ -20,6 +29,8 @@ cn)
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
mkdir -p /etc/omarchy
|
||||||
|
echo cn >/etc/omarchy/region
|
||||||
for link in "${cn_links[@]}"; do
|
for link in "${cn_links[@]}"; do
|
||||||
read -r source target <<<"$link"
|
read -r source target <<<"$link"
|
||||||
mkdir -p "${target%/*}"
|
mkdir -p "${target%/*}"
|
||||||
|
|||||||
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user