Compare commits

...
28 Commits
Author SHA1 Message Date
Ryan Hughes 346e69e1ce Merge pull request #9267 from omacom/fix/close-password-only-sshd
Disable sshd entirely when no usable key is authorized

(cherry picked from commit b686ed892d)
2026-08-30 18:47:46 -04:00
Ryan Hughes 0e1ce609d8 Merge pull request #9255 from omacom/security/migrate-existing-sshd-hardening-v4-0-2
Harden existing key-based SSH setups

(cherry picked from commit 3c2a24b248)
2026-08-30 18:40:46 -04:00
Ryan Hughes 3cb3b2d20b Merge pull request #9263 from omacom/fix/shell-test-host-assumptions
Fix test failures caused by host assumptions

(cherry picked from commit a93ee6a433)
2026-08-30 18:28:00 -04:00
Omarchybot f8e35ec654 Merge pull request #7984 from Chessing234/fix/webapp-name-slashes
Keep a web app name out of the launcher's directory structure

(cherry picked from commit 83881e979b)
2026-08-30 18:03:24 -04:00
Ryan Hughes f802ae107d Merge pull request #9249 from omacom/fix/acceptance-installed-tree-default
Default the acceptance suite's OMARCHY_PATH to the installed tree alone

(cherry picked from commit 981274b20a)
2026-08-30 16:59:38 -04:00
Ryan Hughes 6f8b62f04c Merge pull request #9240 from omacom/fix/acceptance-security-coverage
Fix the bar visibility toggle and make the acceptance suite cover 4.0.2 security hardening

(cherry picked from commit 432b5e3e24)
2026-08-30 16:45:30 -04:00
Ryan Hughes 9aa0545c2a Merge pull request #9232 from omacom/fix/menu-acceptance-style-navigation
Fix Style submenu navigation in the menu acceptance test

(cherry picked from commit e68994680a)
2026-08-30 15:33:18 -04:00
Ryan Hughes ebd6480387 Merge pull request #9225 from omacom/fix/sshd-hardening-verification-case
Match sshd -T keywords case-insensitively when verifying SSH hardening

(cherry picked from commit a24064c720)
2026-08-30 15:14:32 -04:00
Ryan Hughes 07579f5e94 Merge pull request #9214 from omacom/rc-channel-pacman
Point the rc channel at the rc package repository

(cherry picked from commit 4271b880c3)
2026-08-30 13:54:21 -04:00
Ryan Hughes ff4cf8afbc Merge pull request #9200 from omacom/security/v4-0-2-input-asdcontrol-sshd
[4.0.2] Close unprivileged input and SSH escalation paths

(cherry picked from commit 21b27c5aed)
2026-08-30 13:05:45 -04:00
Ryan Hughes cf3d69c38d Merge pull request #9002 from acrogenesis/remove-legacy-installer-privileged-files
Repair legacy paths and privileged files left by retired installers

(cherry picked from commit 943d2fcbe9)
2026-08-30 12:08:00 -04:00
Adolanium 47ce81ebca Quote install-app and install-font names like install-and-launch (#7843)
* Quote install-app and install-font names like install-and-launch

* Quote the package list too, not just the display name

The display name was quoted but omarchy-pkg-add's own arguments were still interpolated into the bash -c string raw, so `omarchy install app Vim 'vim; id'` ran id. The list has to reach the helper as several words, so it cannot be quoted whole: it is split the way the unquoted expansion split it and each word is quoted on its own. Reading with -d '' keeps a newline-separated list intact instead of dropping every package after the first, which plain read -a would. install-font's package is singular and is quoted whole, and install-and-launch carried the same flaw.

Reported by acrogenesis in review of #7843.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Test that install-font skips font-set when pkg-add fails

The hostile-package case was asserting the family still got set, which only held because the mock always exits 0. pacman would reject that name and the && chain would skip font-set.

* Keep the installers working when errexit is inherited

read -d '' always ends at EOF rather than on its delimiter, so it reports failure on every input. Under an inherited errexit the installers exited there and built no command at all.

Reported by Codex XHigh in review of #7843.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
(cherry picked from commit 625c4a1603)
2026-08-30 12:08:00 -04:00
Ryan Hughes 2421e76fb8 Merge pull request #8419 from AFOliveira/security/windows-vm-mount-boundary
[codex] Secure Windows VM host mounts

(cherry picked from commit 158e8cfb3a)
2026-08-30 12:08:00 -04:00
Ryan Hughes bffc6a2a5d Merge pull request #8934 from ErikMelton/security/plymouth-publication-race
Secure Plymouth and SDDM asset publication

(cherry picked from commit e229927671)
2026-08-29 18:46:11 -04:00
Ryan Hughes 7678e5b0d8 Merge pull request #8496 from Chessing234/security/webapp-http-only
(cherry picked from commit 0b3f1b7ead)
2026-08-29 15:40:44 -04:00
Ryan Hughes be730884f6 Merge pull request #8473 from bastidotnet/fix/webapp-desktop-value-escaping
(cherry picked from commit f20bf0a21b)
2026-08-29 15:40:44 -04:00
Ryan Hughes a73a312e1b Merge pull request #8951 from omacom/cups-browsed-temporarily-removed
Temporarily remove automatic printer discovery

(cherry picked from commit c720f0b981)
2026-08-29 15:39:54 -04:00
David Heinemeier Hansson 159163bb16 Merge pull request #8203 from hjanuschka/fix-chromium-first-run-eula
Skip Chromium's new first-run EULA

(cherry picked from commit 5236f4426c)
2026-08-29 15:39:54 -04:00
Ryan Hughes 521779b114 Merge pull request #8416 from mdisec/theme-name-shell-syntax
Refuse a theme name that is shell syntax, and quote the one the unlock picker returns

(cherry picked from commit 9da8824098)
2026-08-29 03:20:17 -04:00
Ryan Hughes 51af7bf794 Merge pull request #8627 from mdisec/security/harden-cups-browsed
Harden CUPS printer discovery

(cherry picked from commit 169ad00a84)
2026-08-29 02:24:37 -04:00
Ryan Hughes 92c8f87730 Merge pull request #8268 from Skeptomenos/fix/copy-url-python-shim-recursion
Avoid mise shim recursion in Copy URL migration test

(cherry picked from commit f0672772d2)
2026-08-28 22:56:02 -04:00
Ryan Hughes 3bb9867245 Merge pull request #8835 from basecamp/fix-browser-policy-exit-trap
Fix migration 1787515927 failing on Bash 5.3

(cherry picked from commit 62eb5182d0)
2026-08-28 22:55:52 -04:00
Ryan Hughes da0fe6d89f Harden browser policy directories (#7972)
Cherry-picked from quattro (7d58bb9a).

Stop world-writable Chromium and Firefox policy directories: create them
root-owned at 0755, purge non-root entries, refuse planted symlinks, and
write the browser theme colour through a passwordless helper instead of
a world-writable policy file.

Conflict resolution for v4-0-2:
- bin/omarchy-install-browser: dropped the `chromium)` case, which does
  not exist on this branch.
- test/shell.d/default-apps-test.sh: dropped; the file does not exist on
  this branch.
2026-08-28 18:25:24 -04:00
Ryan Hughes 2645b82bcd Merge pull request #8397 from ErikMelton/unauthorized-http-get-requests-from-notifications
Require textFormat declaration for all Text elements

(cherry picked from commit 468b511249)
2026-08-28 15:50:37 -04:00
David Heinemeier Hansson 9c1ec524cd Merge pull request #8198 from bastidotnet/harden-apple-brightness-device-cache
Validate the cached Apple-display device path before use

(cherry picked from commit 06e32d243d)
2026-08-28 15:32:34 -04:00
orienw 880605f22b Fix Codex usage collection on 0.149 (#7649)
* Fix Codex usage collector approval policy

* Capture codex argv with boundaries in the scanner test

The stub joined its arguments with "$*", so the assertion compared one
flattened string and could not tell five arguments from fewer containing
spaces. Passing "-s read-only" and "-a on-request" as single arguments --
which codex rejects as an unexpected argument -- passed the test. NUL
separation and an array comparison keep the boundaries the assertion is
about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
(cherry picked from commit 4cd8a081cb)
2026-08-28 15:32:34 -04:00
Ryan Hughes 294c38a2f0 Merge pull request #8072 from basecamp/require-omarchy-repo-signatures
Require signed packages from the Omarchy repository

(cherry picked from commit c5a5e14e99)
2026-08-27 11:29:59 -04:00
Omarchybot 1082b7864d Constrain the tzupdate sudoers rule to a single timezone argument (#8194)
The wildcard granted passwordless root for timedatectl set-timezone plus any trailing arguments, so -H/--host and -M/--machine reached the SSH and machine transports as root. Systemd 261 guards argv injection into ssh, but -H still drives root's SSH client at an attacker-chosen host, and the transport resolves its helper through PATH; only Defaults secure_path stands between that and a planted ssh running as root. Match the argument with an anchored POSIX ERE that admits exactly one timezone token (no whitespace, no leading-dash segment, no traversal component), so no second argument and no option can ever match. The sole caller, omarchy-menu-timezone, passes one list-timezones value and is unaffected.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
(cherry picked from commit 0ae1694830)
2026-08-26 17:56:33 -04:00
167 changed files with 10497 additions and 508 deletions

No files matched your search

+1 -1
View File
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
[codex, "-s", "read-only", "-a", "untrusted", "app-server"], [codex, "-s", "read-only", "-a", "on-request", "app-server"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+17 -4
View File
@@ -4,7 +4,13 @@
# omarchy:args=[--no-osd] [+N%|N%-|N%] # omarchy:args=[--no-osd] [+N%|N%-|N%]
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50% # omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device" # Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
# caching (detect every run) rather than fall back to a predictable, world-writable
# /tmp path another user could pre-create.
device_cache=""
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
fi
no_osd=0 no_osd=0
if [[ ${1:-} == "--no-osd" ]]; then if [[ ${1:-} == "--no-osd" ]]; then
no_osd=1 no_osd=1
@@ -28,9 +34,14 @@ find_apple_display_device() {
local cached="" local cached=""
local device="" local device=""
if [[ -r $device_cache ]]; then if [[ -n $device_cache && -r $device_cache ]]; then
read -r cached <"$device_cache" || true read -r cached <"$device_cache" || true
if [[ -n $cached && -e $cached ]]; then # Trust a cached value only if it still names a hiddev character device. A
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
# and quoting them would turn the match into a literal string comparison.
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
printf '%s\n' "$cached" printf '%s\n' "$cached"
return 0 return 0
fi fi
@@ -39,7 +50,9 @@ find_apple_display_device() {
device="$(detect_apple_display_device)" || return 1 device="$(detect_apple_display_device)" || return 1
[[ -n $device ]] || return 1 [[ -n $device ]] || return 1
printf '%s\n' "$device" >"$device_cache" if [[ -n $device_cache ]]; then
printf '%s\n' "$device" >"$device_cache"
fi
printf '%s\n' "$device" printf '%s\n' "$device"
} }
+6 -2
View File
@@ -34,8 +34,12 @@ systemd, shell, or app-launcher environment; reboot to make every layer agree.
Affects only \$OMARCHY_PATH-resolved trees: bin/, default/, shell/, Affects only \$OMARCHY_PATH-resolved trees: bin/, default/, shell/,
themes/, applications/, config/. Files installed at fixed system paths themes/, applications/, config/. Files installed at fixed system paths
(/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user (/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user
creation, /usr/share/plymouth) are NOT covered — for those, use creation) are NOT covered — for those, use omarchy-dev-pkg-test to build
omarchy-dev-pkg-test to build and install the package from the checkout. and install the package from the checkout.
The Plymouth and SDDM themes under /usr/share are the exception: omarchy
plymouth set and omarchy refresh plymouth republish them from the checkout,
reading this link's authorization out of the root-owned /etc/omarchy.conf.
Also writes $sudoers_file so sudo resolves omarchy-* Also writes $sudoers_file so sudo resolves omarchy-*
from the checkout instead of the packaged copies. That part takes effect from the checkout instead of the packaged copies. That part takes effect
+6
View File
@@ -149,6 +149,9 @@ write_networkmanager_dns() {
local servers="$1" local servers="$1"
install -d -m 0755 "$(dirname "$NM_DNS_CONF")" install -d -m 0755 "$(dirname "$NM_DNS_CONF")"
# omarchy:heredoc-expands paths=none -- $servers is a normalized, single-line
# DNS server list written as data, not a path or command; nothing user-writable
# is resolved or executed from the root-owned drop-in.
cat >"$NM_DNS_CONF" <<EOF cat >"$NM_DNS_CONF" <<EOF
# Managed by omarchy-dns. Remove this file or run omarchy dns DHCP to use DHCP DNS again. # Managed by omarchy-dns. Remove this file or run omarchy dns DHCP to use DHCP DNS again.
[global-dns] [global-dns]
@@ -302,6 +305,9 @@ Custom)
split_dns_servers "$dns_servers" split_dns_servers "$dns_servers"
write_networkmanager_dns "$dns_servers" write_networkmanager_dns "$dns_servers"
set_connection_dns "$ipv4_dns" "$ipv6_dns" set_connection_dns "$ipv4_dns" "$ipv6_dns"
# omarchy:heredoc-expands paths=none -- $dns_servers is a normalized,
# single-line DNS server list; the //,/ turns its comma separators into the
# spaces resolved.conf wants. It is written as data, not a path or command.
tee /etc/systemd/resolved.conf >/dev/null <<EOF tee /etc/systemd/resolved.conf >/dev/null <<EOF
[Resolve] [Resolve]
DNS=${dns_servers//,/ } DNS=${dns_servers//,/ }
+7 -1
View File
@@ -16,6 +16,12 @@ fi
printf -v install_message '%q' "Installing ${name}..." printf -v install_message '%q' "Installing ${name}..."
printf -v desktop_id_arg '%q' "$desktop_id" printf -v desktop_id_arg '%q' "$desktop_id"
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
read -r -d '' -a package_list <<<"$packages" || true
printf -v packages_arg '%q ' "${package_list[@]}"
packages_arg="${packages_arg% }"
# The subshell keeps & from backgrounding the package installation too. # The subshell keeps & from backgrounding the package installation too.
exec omarchy-launch-floating-terminal-with-presentation \ exec omarchy-launch-floating-terminal-with-presentation \
"echo ${install_message}; omarchy-pkg-add ${packages} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)" "echo ${install_message}; omarchy-pkg-add ${packages_arg} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
+10 -1
View File
@@ -12,4 +12,13 @@ if [[ -z $name || -z $packages ]]; then
exit 1 exit 1
fi fi
exec omarchy-launch-floating-terminal-with-presentation "echo 'Installing ${name}...'; omarchy-pkg-add ${packages}" printf -v install_message '%q' "Installing ${name}..."
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
read -r -d '' -a package_list <<<"$packages" || true
printf -v packages_arg '%q ' "${package_list[@]}"
packages_arg="${packages_arg% }"
exec omarchy-launch-floating-terminal-with-presentation \
"echo ${install_message}; omarchy-pkg-add ${packages_arg}"
+10 -16
View File
@@ -4,9 +4,10 @@
# omarchy:args=<chrome|brave|brave-origin|edge|firefox|zen> # omarchy:args=<chrome|brave|brave-origin|edge|firefox|zen>
# omarchy:examples=omarchy install browser firefox | omarchy install browser brave # omarchy:examples=omarchy install browser firefox | omarchy install browser brave
setup_policy_directory() { source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
sudo mkdir -p "$1"
sudo chmod a+rw "$1" setup_chromium_policy_directory() {
browser_policy_setup_dir "$1"
} }
announce_browser_installed() { announce_browser_installed() {
@@ -21,13 +22,6 @@ copy_chromium_flags() {
omarchy-install-chromium-ytdlp omarchy-install-chromium-ytdlp
} }
setup_firefox_preferences() {
local distribution_dir="$1"
setup_policy_directory "$distribution_dir"
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
}
setup_firefox_wayland() { setup_firefox_wayland() {
mkdir -p ~/.config/environment.d mkdir -p ~/.config/environment.d
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
@@ -38,7 +32,7 @@ chrome)
echo "Installing Chrome..." echo "Installing Chrome..."
omarchy-pkg-aur-add google-chrome || exit 1 omarchy-pkg-aur-add google-chrome || exit 1
setup_policy_directory /etc/opt/chrome/policies/managed setup_chromium_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf copy_chromium_flags ~/.config/chrome-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
announce_browser_installed "Chrome" announce_browser_installed "Chrome"
@@ -47,7 +41,7 @@ edge)
echo "Installing Edge..." echo "Installing Edge..."
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1 omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
setup_policy_directory /etc/opt/edge/policies/managed setup_chromium_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
announce_browser_installed "Edge" announce_browser_installed "Edge"
@@ -56,7 +50,7 @@ brave)
echo "Installing Brave..." echo "Installing Brave..."
omarchy-pkg-aur-add brave-bin || exit 1 omarchy-pkg-aur-add brave-bin || exit 1
setup_policy_directory /etc/brave/policies/managed setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf copy_chromium_flags ~/.config/brave-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
announce_browser_installed "Brave" announce_browser_installed "Brave"
@@ -65,7 +59,7 @@ brave-origin)
echo "Installing Brave Origin..." echo "Installing Brave Origin..."
omarchy-pkg-aur-add brave-origin-bin || exit 1 omarchy-pkg-aur-add brave-origin-bin || exit 1
setup_policy_directory /etc/brave/policies/managed setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf copy_chromium_flags ~/.config/brave-origin-flags.conf
omarchy-theme-set-browser omarchy-theme-set-browser
announce_browser_installed "Brave Origin" announce_browser_installed "Brave Origin"
@@ -74,7 +68,7 @@ firefox)
echo "Installing Firefox..." echo "Installing Firefox..."
omarchy-pkg-add firefox || exit 1 omarchy-pkg-add firefox || exit 1
setup_firefox_preferences /usr/lib/firefox/distribution browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_wayland setup_firefox_wayland
announce_browser_installed "Firefox" announce_browser_installed "Firefox"
;; ;;
@@ -82,7 +76,7 @@ zen)
echo "Installing Zen..." echo "Installing Zen..."
omarchy-pkg-aur-add zen-browser-bin || exit 1 omarchy-pkg-aur-add zen-browser-bin || exit 1
setup_firefox_preferences /opt/zen-browser/distribution browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_wayland setup_firefox_wayland
announce_browser_installed "Zen" announce_browser_installed "Zen"
;; ;;
+5 -1
View File
@@ -13,5 +13,9 @@ if [[ -z $name || -z $package || -z $family ]]; then
exit 1 exit 1
fi fi
printf -v install_message '%q' "Installing ${name}..."
printf -v package_arg '%q' "$package"
printf -v family_arg '%q' "$family"
exec omarchy-launch-floating-terminal-with-presentation \ exec omarchy-launch-floating-terminal-with-presentation \
"echo 'Installing ${name}...'; omarchy-pkg-add ${package} && sleep 2 && omarchy-font-set '${family}'" "echo ${install_message}; omarchy-pkg-add ${package_arg} && sleep 2 && omarchy-font-set ${family_arg}"
+3 -3
View File
@@ -85,16 +85,16 @@ wait_for_pacman_transaction
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
[[ -d $MIGRATIONS_DIR ]] || exit 0 [[ -d $MIGRATIONS_DIR ]] || exit 0
while IFS=$'\t' read -r name file marker; do while IFS=$'\t' read -r name file marker <&3; do
[[ -n $name ]] || continue [[ -n $name ]] || continue
if [[ ! -f $marker ]]; then if [[ ! -f $marker ]]; then
echo -e "\e[32m\nRunning migration (${name%.sh})\e[0m" echo -e "\e[32m\nRunning migration (${name%.sh})\e[0m"
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" 3<&-
mkdir -p "$(dirname "$marker")" mkdir -p "$(dirname "$marker")"
touch "$marker" touch "$marker"
fi fi
done < <(migration_entries) done 3< <(migration_entries)
# Clear a login-time notification the user left sitting there and then resolved # Clear a login-time notification the user left sitting there and then resolved
# by running migrations some other way. The substring matches both the current # by running migrations some other way. The substring matches both the current
+4 -2
View File
@@ -3,5 +3,7 @@
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen # omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
omarchy-refresh-plymouth set -euo pipefail
omarchy-refresh-sddm
"$OMARCHY_PATH/bin/omarchy-refresh-plymouth"
"$OMARCHY_PATH/bin/omarchy-refresh-sddm"
+365 -76
View File
@@ -5,88 +5,377 @@
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png # omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
# Configure the Plymouth boot theme with a custom background color, text color, and logo. set -euo pipefail
# Stages the change in a temp dir, then commits the staged files to /usr/share and
# rebuilds the initramfs. Also syncs the SDDM login screen (the post-logout
# screen) with the same colors and logo so boot/login stay visually unified.
if (( $# != 3 )); then # Build the authoritative theme in a root-owned directory, then publish each
# fixed destination atomically. The caller opens the selected logo before sudo,
# so the privileged process never resolves a user-controlled input path.
usage() {
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2 echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
exit 1 exit 1
fi }
bg_hex="${1#\#}" if (( $# == 3 )); then
text_hex="${2#\#}" mode=set
logo_path="$3" elif (( $# == 1 )); then
case "$1" in
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then --refresh-default)
echo "Invalid background color: $1 (expected #RRGGBB)" >&2 mode=refresh-plymouth
exit 1 ;;
fi --refresh-sddm-default)
mode=refresh-sddm
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then ;;
echo "Invalid text color: $2 (expected #RRGGBB)" >&2 *)
exit 1 usage
fi ;;
esac
if [[ ! -f $logo_path ]]; then
echo "Logo file not found: $logo_path" >&2
exit 1
fi
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
# anything. The copies below land in world-readable /usr/share, so following one
# would republish whatever it points at.
if [[ -L $logo_path ]]; then
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
exit 1
fi
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
theme_dir="/usr/share/plymouth/themes/omarchy"
staging_dir=$(mktemp -d)
trap 'rm -rf "$staging_dir"' EXIT
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
cp "$logo_path" "$staging_dir/logo.png"
sed -i \
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
"$staging_dir/omarchy.script"
for asset in bullet.png entry.png lock.png progress_bar.png; do
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
done
sudo cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else else
sudo mkinitcpio -P usage
fi fi
# Sync the SDDM login screen with the same colors and logo. if (( EUID == 0 )); then
sddm_dir="/usr/share/sddm/themes/omarchy" echo "Error: run omarchy-plymouth-set as your user, not under sudo." >&2
sddm_template="$OMARCHY_PATH/default/sddm/omarchy/Main.qml" exit 1
fi
sed \ logo_fd=
-e "s/#1a1b26/#$bg_hex/g" \ if [[ $mode != "set" ]]; then
-e "s/#ffffff/#$text_hex/g" \ bg_hex=
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null text_hex=
else
bg_hex="${1#\#}"
text_hex="${2#\#}"
logo_path="$3"
sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png" if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
for asset in bullet.png entry.png lock.png; do echo "Invalid background color: $1 (expected #RRGGBB)" >&2
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset" exit 1
done fi
for asset in entry lock; do
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png" if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
sudo cp "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png" echo "Invalid text color: $2 (expected #RRGGBB)" >&2
done exit 1
sudo rm -f "$sddm_dir/logo.svg" fi
if [[ ! -f $logo_path ]]; then
echo "Logo file not found: $logo_path" >&2
exit 1
fi
if [[ -L $logo_path ]]; then
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
exit 1
fi
# Open the logo while still unprivileged. A replacement symlink to a root-only
# file therefore fails here instead of being followed after sudo starts.
if ! exec {logo_fd}<"$logo_path"; then
echo "Unable to open logo file as the current user: $logo_path" >&2
exit 1
fi
if [[ ! -f /proc/$$/fd/$logo_fd ]]; then
echo "Logo input is no longer a regular file: $logo_path" >&2
exit 1
fi
fi
run_root_transaction() {
sudo /bin/bash -c '
set -eEuo pipefail
PATH=/usr/bin:/bin
export PATH
umask 077
# Every check below is a bare assertion that aborts under set -e. Name the
# subject of each one so a refusal reaches the user instead of exiting mute.
failure_context="the privileged Plymouth transaction"
failure_reported=
report_failure() {
[[ -z $failure_reported ]] || return 0
failure_reported=1
printf "omarchy-plymouth-set: refusing to publish: %s failed validation\n" "$failure_context" >&2
if [[ -n ${failure_hint:-} ]]; then
printf "omarchy-plymouth-set: %s\n" "$failure_hint" >&2
fi
}
trap report_failure ERR
mode=$1
source_root=$2
bg_hex=$3
text_hex=$4
max_asset_size=$5
failure_context="the arguments of the privileged transaction"
[[ $mode == "set" || $mode == "refresh-plymouth" || $mode == "refresh-sddm" ]]
[[ $source_root == /* ]]
[[ $max_asset_size =~ ^[0-9]+$ ]]
(( max_asset_size > 0 ))
failure_context="the Omarchy source tree $source_root"
canonical_source_root=$(realpath -e -- "$source_root")
[[ $canonical_source_root == "$source_root" ]]
validate_trusted_directory() {
local directory=$1 canonical uid directory_mode
failure_context="directory $directory"
canonical=$(realpath -e -- "$directory")
[[ $canonical == "$directory" && -d $directory && ! -L $directory ]]
while :; do
failure_context="directory $directory (must be root-owned and not group- or world-writable)"
uid=$(stat -c %u -- "$directory")
directory_mode=$(stat -c %a -- "$directory")
(( uid == 0 ))
(( (8#$directory_mode & 0022) == 0 ))
[[ $directory == "/" ]] && break
directory=${directory%/*}
[[ -n $directory ]] || directory=/
done
}
validate_trusted_configuration_file() {
local configuration=$1 canonical uid configuration_mode size
failure_context="root configuration $configuration"
[[ -f $configuration && ! -L $configuration ]]
canonical=$(realpath -e -- "$configuration")
[[ $canonical == "$configuration" ]]
validate_trusted_directory "${configuration%/*}"
uid=$(stat -c %u -- "$configuration")
configuration_mode=$(stat -c %a -- "$configuration")
size=$(stat -c %s -- "$configuration")
(( uid == 0 ))
(( (8#$configuration_mode & 0022) == 0 ))
(( size > 0 && size <= 4096 ))
}
# A packaged tree must be root-owned. A development checkout is the one
# deliberate exception: omarchy dev link records its canonical path in a
# root-owned /etc/omarchy.conf. That is already an explicit decision to run
# privileged Omarchy commands from user-editable code in the checkout, so
# reading its packaged assets does not widen the development trust boundary.
development_source=false
source_root_uid=$(stat -c %u -- "$source_root")
if (( source_root_uid != 0 )); then
omarchy_conf=/etc/omarchy.conf
failure_context="$source_root is user-owned and $omarchy_conf must contain its trusted dev-link authorization; run omarchy dev link to authorize it"
failure_hint="$source_root is user-owned; run omarchy dev link to authorize this development checkout, or omarchy dev unlink to use the packaged tree"
validate_trusted_configuration_file "$omarchy_conf"
# validate_trusted_configuration_file walks /etc up to / and leaves its own
# subject behind in failure_context. Without restoring ours, a checkout
# that simply is not the authorized one refuses with "directory / must be
# root-owned and not group- or world-writable" -- naming a directory that
# passed, and sending the reader after a filesystem problem that is not
# there.
failure_context="the dev-link authorization in $omarchy_conf, which must name $source_root"
quoted_source_root=$source_root
quoted_source_root=${quoted_source_root//\\/\\\\}
quoted_source_root=${quoted_source_root//\"/\\\"}
quoted_source_root=${quoted_source_root//\$/\\\$}
quoted_source_root=${quoted_source_root//\`/\\\`}
expected_config_line="export OMARCHY_PATH=\"$quoted_source_root\""
mapfile -t omarchy_config_lines <"$omarchy_conf"
(( ${#omarchy_config_lines[@]} == 1 ))
[[ ${omarchy_config_lines[0]} == "$expected_config_line" ]]
development_source=true
failure_hint=
fi
if [[ $mode == "set" ]]; then
[[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]
[[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]
fi
theme_dir=/usr/share/plymouth/themes/omarchy
sddm_dir=/usr/share/sddm/themes/omarchy
plymouth_theme_assets=(
bullet.png
entry.png
lock.png
logo.png
omarchy.plymouth
omarchy.script
preview-unlock.png
progress_bar.png
progress_box.png
)
plymouth_default_assets=("${plymouth_theme_assets[@]}" logos/oma.png)
sddm_theme_assets=(Main.qml bullet.png entry-failed.png entry.png lock-failed.png lock.png logo.png)
sddm_default_assets=("${sddm_theme_assets[@]}" metadata.desktop theme.conf)
plymouth_assets=()
sddm_assets=()
case "$mode" in
set)
plymouth_assets=("${plymouth_theme_assets[@]}")
sddm_assets=("${sddm_theme_assets[@]}")
;;
refresh-plymouth)
plymouth_assets=("${plymouth_default_assets[@]}")
;;
refresh-sddm)
sddm_assets=("${sddm_default_assets[@]}")
;;
esac
validate_trusted_file() {
local source=$1 canonical uid file_mode size
failure_context="packaged source file $source"
[[ -f $source && ! -L $source ]]
canonical=$(realpath -e -- "$source")
[[ $canonical == "$source" ]]
file_mode=$(stat -c %a -- "$source")
size=$(stat -c %s -- "$source")
(( size > 0 && size <= max_asset_size ))
if ! $development_source; then
validate_trusted_directory "${source%/*}"
uid=$(stat -c %u -- "$source")
(( uid == 0 ))
(( (8#$file_mode & 0022) == 0 ))
fi
}
copy_trusted_file() {
local source=$1 destination=$2
validate_trusted_file "$source"
install -o 0 -g 0 -m 0600 -- "$source" "$destination"
}
staging_dir=$(mktemp -d /tmp/omarchy-plymouth.XXXXXXXX)
temporary=
cleanup() {
[[ -z $temporary ]] || rm -f -- "$temporary"
rm -rf -- "$staging_dir"
}
trap cleanup EXIT HUP INT TERM
chown 0:0 -- "$staging_dir"
chmod 0700 -- "$staging_dir"
plymouth_stage=$staging_dir/plymouth
sddm_stage=$staging_dir/sddm
mkdir -m 0700 -p -- "$plymouth_stage/logos" "$sddm_stage"
for asset in "${plymouth_assets[@]}"; do
copy_trusted_file "$source_root/default/plymouth/$asset" "$plymouth_stage/$asset"
done
if [[ $mode == "set" ]]; then
# stdin was opened by the unprivileged caller. Read no more than the
# documented limit into the root-owned stage before doing other work.
failure_context="the selected logo (expected 1 to $max_asset_size bytes)"
head -c "$((max_asset_size + 1))" >"$plymouth_stage/logo.png"
logo_size=$(stat -c %s -- "$plymouth_stage/logo.png")
(( logo_size > 0 && logo_size <= max_asset_size ))
chown 0:0 -- "$plymouth_stage/logo.png"
chmod 0600 -- "$plymouth_stage/logo.png"
cp --reflink=never -- "$plymouth_stage/logo.png" "$sddm_stage/logo.png"
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) "BEGIN{printf \"%.3f\", n/255}")
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) "BEGIN{printf \"%.3f\", n/255}")
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) "BEGIN{printf \"%.3f\", n/255}")
sed -i \
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
"$plymouth_stage/omarchy.script"
for asset in bullet.png entry.png lock.png progress_bar.png; do
magick "$plymouth_stage/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$plymouth_stage/$asset"
done
copy_trusted_file "$source_root/default/sddm/omarchy/Main.qml" "$sddm_stage/Main.qml"
sed -i \
-e "s/#1a1b26/#__OMARCHY_SDDM_BG__/g" \
-e "s/#ffffff/#__OMARCHY_SDDM_TEXT__/g" \
-e "s/#__OMARCHY_SDDM_BG__/#$bg_hex/g" \
-e "s/#__OMARCHY_SDDM_TEXT__/#$text_hex/g" \
"$sddm_stage/Main.qml"
for asset in bullet.png entry.png lock.png; do
cp --reflink=never -- "$plymouth_stage/$asset" "$sddm_stage/$asset"
done
for asset in entry lock; do
magick "$plymouth_stage/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$sddm_stage/$asset-failed.png"
done
chown -R 0:0 -- "$staging_dir"
find "$staging_dir" -type f -exec chmod 0600 -- {} +
elif (( ${#sddm_assets[@]} )); then
for asset in "${sddm_assets[@]}"; do
copy_trusted_file "$source_root/default/sddm/omarchy/$asset" "$sddm_stage/$asset"
done
fi
if (( ${#plymouth_assets[@]} )); then
validate_trusted_directory "$theme_dir"
if [[ $mode == "refresh-plymouth" ]]; then
validate_trusted_directory "$theme_dir/logos"
fi
fi
if (( ${#sddm_assets[@]} )); then
validate_trusted_directory "$sddm_dir"
fi
publish_asset() {
local source=$1 destination=$2 parent filename source_size copied_size
failure_context="destination $destination"
[[ -f $source && ! -L $source ]]
(( $(stat -c %u -- "$source") == 0 ))
source_size=$(stat -c %s -- "$source")
(( source_size > 0 && source_size <= max_asset_size ))
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
parent=${destination%/*}
filename=${destination##*/}
[[ -n $parent && -n $filename && $filename != "." && $filename != ".." ]]
validate_trusted_directory "$parent"
temporary=$(mktemp --tmpdir="$parent" ".$filename.omarchy-new.XXXXXXXX")
install -o 0 -g 0 -m 0644 -- "$source" "$temporary"
copied_size=$(stat -c %s -- "$temporary")
(( copied_size == source_size ))
cmp -s -- "$source" "$temporary"
sync -f -- "$temporary"
mv --no-copy -fT -- "$temporary" "$destination"
temporary=
}
if (( ${#plymouth_assets[@]} )); then
for asset in "${plymouth_assets[@]}"; do
publish_asset "$plymouth_stage/$asset" "$theme_dir/$asset"
done
fi
if (( ${#sddm_assets[@]} )); then
for asset in "${sddm_assets[@]}"; do
publish_asset "$sddm_stage/$asset" "$sddm_dir/$asset"
done
validate_trusted_directory "$sddm_dir"
rm -f -- "$sddm_dir/logo.svg"
fi
' bash "$mode" "$OMARCHY_PATH" "$bg_hex" "$text_hex" "$((64 * 1024 * 1024))"
}
if [[ $mode == "set" ]]; then
run_root_transaction <&"$logo_fd"
else
run_root_transaction </dev/null
fi
if [[ $mode != "refresh-sddm" ]]; then
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
sudo mkinitcpio -P
fi
fi
+16 -6
View File
@@ -677,11 +677,15 @@ user_groups() {
if [[ -f $PROVISIONING_DIR/groups ]]; then if [[ -f $PROVISIONING_DIR/groups ]]; then
while IFS= read -r group; do while IFS= read -r group; do
[[ -n $group ]] || continue [[ -n $group ]] || continue
# Never grant docker at first boot, even if an older install recorded it # Never replay old privileged group defaults. Docker is always opt-in.
# (or a factory snapshot predating the opt-in default carries it): the # Input is only retained when the factory image has one of the features
# docker group is root-equivalent. It is opt-in via # whose installer deliberately grants access to raw input devices.
# omarchy-setup-security-sudoless-docker.
[[ $group == "docker" ]] && continue [[ $group == "docker" ]] && continue
if [[ $group == "input" ]] &&
! pacman -Qq xpadneo-dkms &>/dev/null &&
! pacman -Qq ydotool &>/dev/null; then
continue
fi
getent group "$group" >/dev/null || continue getent group "$group" >/dev/null || continue
[[ ",$groups," == *",$group,"* ]] || groups+=",$group" [[ ",$groups," == *",$group,"* ]] || groups+=",$group"
done <"$PROVISIONING_DIR/groups" done <"$PROVISIONING_DIR/groups"
@@ -742,6 +746,12 @@ create_user() {
# for specific commands), and a duplicate grant is harmless. # for specific commands), and a duplicate grant is harmless.
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
chmod 440 /etc/sudoers.d/00-omarchy-wheel chmod 440 /etc/sudoers.d/00-omarchy-wheel
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
} }
install_authorized_keys() { install_authorized_keys() {
@@ -782,7 +792,7 @@ configure_login() {
# After=) is what makes it deterministic — no sleep/race against SDDM's startup. # After=) is what makes it deterministic — no sleep/race against SDDM's startup.
install_autologin_once_cleanup() { install_autologin_once_cleanup() {
local unit=omarchy-provision-autologin-once.service local unit=omarchy-provision-autologin-once.service
cat >"/etc/systemd/system/$unit" <<UNIT sed "s|@UNIT@|$unit|g" >"/etc/systemd/system/$unit" <<'UNIT'
[Unit] [Unit]
Description=Drop the first-boot autologin before the next login Description=Drop the first-boot autologin before the next login
Before=display-manager.service Before=display-manager.service
@@ -791,7 +801,7 @@ ConditionPathExists=/etc/sddm.conf.d/autologin.conf
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/rm -f /etc/sddm.conf.d/autologin.conf ExecStart=/usr/bin/rm -f /etc/sddm.conf.d/autologin.conf
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/$unit /etc/systemd/system/$unit ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/@UNIT@ /etc/systemd/system/@UNIT@
[Install] [Install]
WantedBy=graphical.target WantedBy=graphical.target
+3 -8
View File
@@ -3,11 +3,6 @@
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it. # omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
sudo cp -r "$OMARCHY_PATH/default/plymouth/." /usr/share/plymouth/themes/omarchy/ # Reuse the fixed-file publisher so root never resolves the source checkout or
sudo plymouth-set-default-theme omarchy # follows a destination symlink while restoring the packaged assets.
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-default
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
sudo mkinitcpio -P
fi
+3 -2
View File
@@ -3,5 +3,6 @@
# omarchy:summary=Refresh the SDDM theme from default # omarchy:summary=Refresh the SDDM theme from default
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
sudo rm -rf /usr/share/sddm/themes/omarchy # Reuse the fixed-file publisher so root never resolves an untrusted source or
sudo cp -r "$OMARCHY_PATH/default/sddm/omarchy" /usr/share/sddm/themes/omarchy # follows a destination symlink while restoring the packaged SDDM theme.
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-sddm-default
+3
View File
@@ -41,6 +41,9 @@ setup_pam_config() {
fi fi
else else
echo "Creating polkit configuration with fingerprint authentication..." echo "Creating polkit configuration with fingerprint authentication..."
# omarchy:heredoc-expands paths=none -- $fprintd_gate is the literal PAM
# line defined above, shared with the two sed insertions so the gate cannot
# drift between files. The only path in it is the fixed /usr/bin one.
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
$fprintd_gate $fprintd_gate
auth sufficient pam_fprintd.so auth sufficient pam_fprintd.so
+47
View File
@@ -107,6 +107,50 @@ authorize_pasted_key() {
authorize_key "$key" || exit 1 authorize_key "$key" || exit 1
} }
# Only called after a key is authorized. Disabling password authentication
# before then could lock the owner out of the machine.
disable_password_auth() {
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
local effective_config
if [[ ! -s $AUTHORIZED_KEYS ]]; then
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
return 1
fi
echo "Disabling SSH password authentication, now that a key is authorized..."
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
# Delete this file and reload sshd to allow password logins again.
PasswordAuthentication no
KbdInteractiveAuthentication no
CONF
# Validate before reloading: a config sshd rejects would otherwise take the
# service down on its next restart, potentially stranding a remote owner.
if ! sudo sshd -t; then
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
sudo rm -f "$config"
return 1
fi
# Syntax alone is insufficient because sshd uses the first value it reads for
# these settings. An earlier administrator rule could leave passwords enabled.
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
# in CamelCase.
if ! effective_config=$(sudo sshd -T) ||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
sudo rm -f "$config"
return 1
fi
# Reload rather than restart so an administrator already connected keeps
# their session.
sudo systemctl reload sshd.service
}
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m" echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
setup_sshd setup_sshd
@@ -123,5 +167,8 @@ else
esac esac
fi fi
disable_password_auth
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m" echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
echo "Password logins are off; this machine now accepts authorized keys only."
echo "You can now connect with: ssh $USER@$(hostname)" echo "You can now connect with: ssh $USER@$(hostname)"
+16 -6
View File
@@ -23,16 +23,26 @@ omarchy-git-url-check "$REPO_URL" || exit 1
THEMES_DIR="$HOME/.config/omarchy/themes" THEMES_DIR="$HOME/.config/omarchy/themes"
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path # Strip user@host: prefix from scp-style SSH URLs so basename sees just the path.
# git reads a URL as scp-style when a colon appears before any slash, so the path
# after it need not hold one: `git@host:omarchy-blue-theme.git` is a repo in that
# user's home, and leaving its prefix on names the theme after the whole URL.
REPO_PATH="$REPO_URL" REPO_PATH="$REPO_URL"
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}" [[ $REPO_PATH != *"://"* && $REPO_PATH == *:* && ${REPO_PATH%%:*} != */* ]] && REPO_PATH="${REPO_PATH#*:}"
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]') THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
THEME_PATH="$THEMES_DIR/$THEME_NAME" THEME_PATH="$THEMES_DIR/$THEME_NAME"
# The name comes from the URL and is joined into a path that is about to be # The name comes from the URL, is joined into a path that is about to be
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading # removed, and then names a directory the rest of Omarchy passes around by
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`. # name: Style > Unlock builds a command line out of the one the picker
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then # returned. So it is held to the characters a theme name needs rather than
# screened for the harm of the day -- a repo called `..` would take
# ~/.config/omarchy with it, and one called `a';'id` would carry its own
# command into that picker. The leading character is kept out of `.` and `-`,
# which also covers `host:-s/foo.git` leaving basename with `.git`.
# A bracket range follows the locale's collation, not ASCII: `[a-z]` takes in
# `é` under en_US.UTF-8. Pin the locale so the set is the one written here.
if ! (LC_ALL=C; [[ $THEME_NAME =~ ^[a-z0-9_][a-z0-9._+-]*$ ]]); then
echo "Error: '$REPO_URL' does not give a usable theme name." echo "Error: '$REPO_URL' does not give a usable theme name."
exit 1 exit 1
fi fi
+9 -19
View File
@@ -3,23 +3,15 @@
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave # omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
# omarchy:hidden=true # omarchy:hidden=true
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
if [[ -f $CHROMIUM_THEME ]]; then if [[ -f $CHROMIUM_THEME ]]; then
THEME_RGB_COLOR=$(<$CHROMIUM_THEME) THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
else
# Use a default, neutral grey if theme doesn't have a color
THEME_HEX_COLOR="#1c2027"
fi fi
set_browser_policy() {
local policy_dir="$1"
[[ -d $policy_dir ]] || return
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
}
refresh_running_browser() { refresh_running_browser() {
local process="$1" local process="$1"
local command="$2" local command="$2"
@@ -30,17 +22,15 @@ refresh_running_browser() {
fi fi
} }
set_browser_policy /etc/chromium/policies/managed failed=0
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
refresh_running_browser chromium chromium refresh_running_browser chromium chromium
set_browser_policy /etc/opt/chrome/policies/managed
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
set_browser_policy /etc/opt/edge/policies/managed
refresh_running_browser msedge microsoft-edge-stable refresh_running_browser msedge microsoft-edge-stable
set_browser_policy /etc/brave/policies/managed
refresh_running_browser brave brave refresh_running_browser brave brave
# Match on the binary path: the running process is named plain "brave", and a # Match on the binary path: the running process is named plain "brave", and a
# bare -f brave-origin pattern would also match the installer's own terminal. # bare -f brave-origin pattern would also match the installer's own terminal.
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
exit "$failed"
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# omarchy:summary=Write the current theme color into the browser policy directories
# omarchy:args=<rrggbb>
# omarchy:hidden=true
set -euo pipefail
# Whenever this runs as root — invoked directly through the passwordless
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
# prepends a user-writable checkout bin/ to it. Every helper this script calls
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
# never an omarchy-* command, so pin PATH to trusted system directories and keep
# root from resolving one out of that checkout. The unprivileged wrapper phase
# keeps the caller's PATH so it can still find sudo/pkexec.
if (( EUID == 0 )); then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
fi
# Enterprise policy trust roots. The list is fixed here rather than taken from
# the caller: the caller chooses a color, never a path.
POLICY_DIRS=(
/etc/chromium/policies/managed
/etc/opt/chrome/policies/managed
/etc/opt/edge/policies/managed
/etc/brave/policies/managed
)
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
# runs from there rather than from whichever copy was invoked, so the rule
# matches even where $OMARCHY_PATH points at a checkout.
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
usage() {
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
}
if (( $# != 1 )); then
usage
exit 1
fi
color="$1"
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
# is added when the JSON is written rather than passed in: "#" opens a comment
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
# argument as a plain six-character glob.
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
exit 1
fi
# True when sudo would run this exact command without stopping for a password.
# `sudo -l` on its own reports whether a command is permitted, which the blanket
# %wheel rule answers yes to for everything; the long listing prints the matched
# entry's tags, so !authenticate is the grant in
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
# and, under -n, prompts for nothing.
sudo_grants_passwordless() {
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
}
require_root() {
if (( EUID == 0 )); then
return
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
exec sudo "$PACKAGED_PATH" "$@"
else
exec pkexec "$PACKAGED_PATH" "$@"
fi
}
require_root "$color"
failed=0
staged=""
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
# so this handler must not end on a false test. Every successful run clears
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
cleanup() {
if [[ -n $staged ]]; then
rm -f "$staged"
fi
}
trap cleanup EXIT
for policy_dir in "${POLICY_DIRS[@]}"; do
# Only browsers Omarchy has installed have a policy directory. Creating one
# here would hand a browser a managed-policy root it does not otherwise have.
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
dest=$policy_dir/color.json
staged=$(mktemp) || {
failed=1
continue
}
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
if [[ -L $dest || -d $dest ]]; then
if ! rm -rf -- "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
failed=1
continue
fi
fi
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
failed=1
continue
fi
rm -f "$staged"
staged=""
done
exit "$failed"
+6
View File
@@ -5,3 +5,9 @@
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on # omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
omarchy-toggle bar-off "${1:-toggle}" omarchy-toggle bar-off "${1:-toggle}"
# The shell's watch on the toggles directory can miss flag changes that land in
# quick succession, stranding the bar off screen until the shell restarts.
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
# works when the shell is not up.
omarchy-shell -q omarchy.bar syncHidden
+40 -6
View File
@@ -597,6 +597,9 @@ preserve_kernel_cmdline_root() {
fi fi
log "Preserving the kernel cmdline root parameters in $default_conf" log "Preserving the kernel cmdline root parameters in $default_conf"
# omarchy:heredoc-expands paths=none -- ${boot_params[*]} is the kernel
# cmdline fragment assembled above from findmnt output (root=, rootflags=),
# not a filesystem path this script controls.
as_root tee -a "$default_conf" >/dev/null <<EOF as_root tee -a "$default_conf" >/dev/null <<EOF
# Written by omarchy-upgrade-to-quattro. The += drop-ins in # Written by omarchy-upgrade-to-quattro. The += drop-ins in
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading # /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
@@ -1187,11 +1190,19 @@ ensure_sleep_lock_service() {
} }
run_post_upgrade_migrations() { run_post_upgrade_migrations() {
PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0 local pending_status
log "Running Omarchy migrations" log "Running Omarchy migrations"
if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
warn "Could not run Omarchy migrations; the user may be prompted to run them after login." fail "Omarchy migrations did not complete. Fix the error above and rerun the upgrade before rebooting."
fi
if run_as_user_omarchy omarchy-migrate --pending >/dev/null; then
fail "Omarchy migrations are still pending. Rerun the upgrade before rebooting."
else
pending_status=$?
if (( pending_status != 1 )); then
fail "Could not verify that Omarchy migrations completed. Rerun the upgrade before rebooting."
fi
fi fi
} }
@@ -1312,9 +1323,23 @@ apply_system_transition() {
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg /usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
as_root install -d -m 0777 /etc/chromium/policies/managed local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
if ! as_root test -f "$browser_policy_helper"; then
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
else
as_root env OMARCHY_PATH=/usr/share/omarchy \
bash -euo pipefail -c '
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
'
fi
as_root install -d -m 0755 /usr/lib/chromium as_root install -d -m 0755 /usr/lib/chromium
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \ printf '%s\n' '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
as_root tee /usr/lib/chromium/initial_preferences >/dev/null as_root tee /usr/lib/chromium/initial_preferences >/dev/null
# Deliberately do NOT add the user to the docker group. That group is # Deliberately do NOT add the user to the docker group. That group is
@@ -1350,7 +1375,6 @@ EOF
as_root systemctl disable docker.service >/dev/null 2>&1 || true as_root systemctl disable docker.service >/dev/null 2>&1 || true
enable_system_service cups.service enable_system_service cups.service
enable_system_service cups-browsed.service
enable_system_service avahi-daemon.service enable_system_service avahi-daemon.service
enable_system_service linux-modules-cleanup.service enable_system_service linux-modules-cleanup.service
enable_system_service docker.socket enable_system_service docker.socket
@@ -1382,6 +1406,9 @@ EOF
autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true) autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
fi fi
[[ -n ${autologin_user:-} ]] || autologin_user="$target_user" [[ -n ${autologin_user:-} ]] || autologin_user="$target_user"
# omarchy:heredoc-expands paths=none -- $autologin_user is a username, read
# back from the root-owned drop-in or falling back to $target_user. Same
# mechanism as the old getty override: a name expands, no path does.
cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
[Autologin] [Autologin]
User=$autologin_user User=$autologin_user
@@ -1392,6 +1419,8 @@ EOF
fi fi
as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
# omarchy:heredoc-expands paths=none -- $target_user is a username, not a
# path; SDDM's state file records who logged in last.
cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
[Last] [Last]
Session=omarchy.desktop Session=omarchy.desktop
@@ -2306,6 +2335,11 @@ refresh_current_theme_after_upgrade() {
# hooks because one of them runs `hyprctl reload`. Still poke terminal # hooks because one of them runs `hyprctl reload`. Still poke terminal
# emulators so the active upgrade terminal picks up generated theme files. # emulators so the active upgrade terminal picks up generated theme files.
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
# apply_system_transition purged user-owned color.json. Headless theme-set
# skipped omarchy-theme-set-browser, so rewrite the colour here.
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
} }
# Everything below mutates the system, so a non-zero exit from here on leaves a # Everything below mutates the system, so a non-zero exit from here on leaves a
@@ -2340,8 +2374,8 @@ run_as_user_omarchy omarchy-bar defaults ||
cleanup_retired_services cleanup_retired_services
ensure_sleep_lock_service ensure_sleep_lock_service
remove_retired_default_packages remove_retired_default_packages
run_post_upgrade_migrations
run_final_system_package_upgrade run_final_system_package_upgrade
run_post_upgrade_migrations
run_post_upgrade_update_steps run_post_upgrade_update_steps
refresh_current_theme_after_upgrade refresh_current_theme_after_upgrade
# Do not force-reload Hyprland in the live upgraded session. The legacy # Do not force-reload Hyprland in the live upgraded session. The legacy
+2
View File
@@ -14,6 +14,8 @@ fi
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
pkgs="stable" pkgs="stable"
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
pkgs="rc"
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
pkgs="edge" pkgs="edge"
else else
+98 -16
View File
@@ -13,6 +13,18 @@ safe_icon_name() {
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
} }
require_plain_name() {
# The name becomes a filename. A slash would turn it into directory levels, so
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
# is stuck in the launcher; a leading ../ leaves the applications directory
# altogether. Refuse rather than silently renaming what the user typed -- most
# often it is a URL entered in the name field.
if [[ $1 == */* ]]; then
echo "App name cannot contain '/': $1"
exit 1
fi
}
icon_name_from_ref() { icon_name_from_ref() {
local ref="$1" local ref="$1"
local name local name
@@ -42,6 +54,34 @@ download_icon() {
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]] [[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
} }
# Chromium --app= treats javascript:, file:, and data: as a document to
# run. Prefix schemeless input with https as before, then refuse anything
# that is not http(s).
normalize_webapp_url() {
local url=$1
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
url="https://$url"
fi
printf '%s' "$url"
}
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
# the desktop entry; before Exec argument quoting, it also split browser flags
# and additional URLs into separate arguments. Schemes are case-insensitive.
require_http_url() {
local url=$1
if [[ $url =~ [[:space:]] ]]; then
echo "Error: web app URL must not contain whitespace." >&2
exit 1
fi
if [[ ! ${url,,} =~ ^https?:// ]]; then
echo "Error: web app URL must be http or https." >&2
exit 1
fi
}
fetch_site_icon() { fetch_site_icon() {
local site_url="$1" dest="$2" local site_url="$1" dest="$2"
local origin page icon_url local origin page icon_url
@@ -65,13 +105,44 @@ fetch_site_icon() {
download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest" download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest"
} }
desktop_string_escape() {
# Desktop Entry "string" value (freedesktop Desktop Entry Spec, "Value types"):
# a raw newline would start a new key line and let a value inject a second
# Exec=. Escape backslash first, then tab/CR/LF and a leading space. Every value
# written into the .desktop file passes through here.
#
# Parameter expansion rather than sed: GNU sed's N auto-prints the pattern space
# and exits at end of input, so a `:a;N;$!ba` slurp skips every following s///
# for a value with no newline in it - which is every value except the injection
# attempt this exists to stop.
local value="$1"
value=${value//\\/\\\\}
value=${value//$'\t'/\\t}
value=${value//$'\r'/\\r}
value=${value//$'\n'/\\n}
[[ $value == " "* ]] && value="\\s${value# }"
printf '%s' "$value"
}
desktop_exec_arg() {
# One Exec argument, double-quoted per the freedesktop Exec spec: inside quotes
# " ` $ \ take a backslash and a literal % becomes %%. Only the default Exec's
# URL needs this; $CUSTOM_EXEC stays a whole command line (file-syntax only).
local escaped
escaped=$(printf '%s' "$1" \
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')
printf '"%s"' "$escaped"
}
if (( $# < 3 )); then if (( $# < 3 )); then
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m" echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app") APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
require_plain_name "$APP_NAME"
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com") APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then APP_URL=$(normalize_webapp_url "$APP_URL")
APP_URL="https://$APP_URL" require_http_url "$APP_URL"
fi
# Try to fetch the site's icon automatically first. # Try to fetch the site's icon automatically first.
mkdir -p "$ICON_DIR" mkdir -p "$ICON_DIR"
@@ -88,10 +159,8 @@ if (( $# < 3 )); then
INTERACTIVE_MODE=true INTERACTIVE_MODE=true
else else
APP_NAME="$1" APP_NAME="$1"
APP_URL="$2" APP_URL=$(normalize_webapp_url "$2")
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then require_http_url "$APP_URL"
APP_URL="https://$APP_URL"
fi
ICON_REF="$3" ICON_REF="$3"
CUSTOM_EXEC="$4" # Optional custom exec command CUSTOM_EXEC="$4" # Optional custom exec command
MIME_TYPES="$5" # Optional mime types MIME_TYPES="$5" # Optional mime types
@@ -104,6 +173,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
exit 1 exit 1
fi fi
require_plain_name "$APP_NAME"
if [[ -z $ICON_REF ]]; then if [[ -z $ICON_REF ]]; then
ICON_VALUE=$(safe_icon_name "$APP_NAME") ICON_VALUE=$(safe_icon_name "$APP_NAME")
mkdir -p "$ICON_DIR" mkdir -p "$ICON_DIR"
@@ -128,28 +199,39 @@ else
ICON_VALUE=$(icon_name_from_ref "$ICON_REF") ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
fi fi
# Use custom exec if provided, otherwise default behavior # Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}" # the file-syntax escaping below (unescaped first at read time per spec, so the
# layers compose). $CUSTOM_EXEC is a full command line, so it gets file-syntax only.
if [[ -n $CUSTOM_EXEC ]]; then
EXEC_COMMAND=$CUSTOM_EXEC
else
EXEC_COMMAND="omarchy-launch-webapp $(desktop_exec_arg "$APP_URL")"
fi
# Create application .desktop file # Create application .desktop file
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop" DESKTOP_DIR="$HOME/.local/share/applications"
mkdir -p "$(dirname "$DESKTOP_FILE")" DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
mkdir -p "$DESKTOP_DIR"
name_field=$(desktop_string_escape "$APP_NAME")
exec_field=$(desktop_string_escape "$EXEC_COMMAND")
icon_field=$(desktop_string_escape "$ICON_VALUE")
cat >"$DESKTOP_FILE" <<EOF cat >"$DESKTOP_FILE" <<EOF
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=$APP_NAME Name=$name_field
Comment=$APP_NAME Comment=$name_field
Exec=$EXEC_COMMAND Exec=$exec_field
Terminal=false Terminal=false
Type=Application Type=Application
Icon=$ICON_VALUE Icon=$icon_field
StartupNotify=true StartupNotify=true
EOF EOF
# Add mime types if provided # Add mime types if provided
if [[ -n $MIME_TYPES ]]; then if [[ -n $MIME_TYPES ]]; then
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE" printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
fi fi
chmod +x "$DESKTOP_FILE" chmod +x "$DESKTOP_FILE"
+26 -8
View File
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
OLD_ICON_DIR="$HOME/.local/share/applications/icons" OLD_ICON_DIR="$HOME/.local/share/applications/icons"
DESKTOP_DIR="$HOME/.local/share/applications/" DESKTOP_DIR="$HOME/.local/share/applications/"
if (( $# == 0 )); then # Always index the launchers, so removal deletes the file that was found rather
# Find all web apps # than a path rebuilt from the displayed name. Installs predating the name
while IFS= read -r -d '' file; do # validation could nest the launcher inside directories, and those are exactly
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then # the ones a reconstructed path cannot reach.
WEB_APPS+=("$(basename "${file%.desktop}")") WEB_APP_PATHS=()
fi while IFS= read -r -d '' file; do
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0) if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
WEB_APP_PATHS+=("$file")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
# The launcher matching a chosen name, or empty when nothing was indexed under
# it (an app removed between the scan and the pick, say).
path_for_web_app() {
local wanted="$1" i
for i in "${!WEB_APPS[@]}"; do
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
printf '%s\n' "${WEB_APP_PATHS[$i]}"
return 0
fi
done
}
if (( $# == 0 )); then
if ((${#WEB_APPS[@]})); then if ((${#WEB_APPS[@]})); then
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort) mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520) APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
fi fi
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//') icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
rm -f "$DESKTOP_DIR/$APP_NAME.desktop" desktop_file=$(path_for_web_app "$APP_NAME")
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
+898 -125
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -98,7 +98,7 @@
// Style // Style
"style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""}, "style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""},
"style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""}, "style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""},
"style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme '$unlock'\"; fi"}, "style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme $(printf %q \"$unlock\")\"; fi"},
"style.font": {"icon":"","label":"Font","provider":"fonts"}, "style.font": {"icon":"","label":"Font","provider":"fonts"},
"style.bar": {"icon":"󰍜","label":"Menu Bar"}, "style.bar": {"icon":"󰍜","label":"Menu Bar"},
"style.bar.position": {"icon":"","label":"Position"}, "style.bar.position": {"icon":"","label":"Position"},
-1
View File
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist
[omarchy] [omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch Server = https://pkgs.omarchy.org/edge/$arch
# Repositories for debug symbol packages. # Repositories for debug symbol packages.
+1 -2
View File
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist
[omarchy] [omarchy]
SigLevel = Optional TrustAll Server = https://pkgs.omarchy.org/rc/$arch
Server = https://pkgs.omarchy.org/edge/$arch
-1
View File
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist
[omarchy] [omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/stable/$arch Server = https://pkgs.omarchy.org/stable/$arch
+3
View File
@@ -124,6 +124,7 @@ New migration format:
- Start with an `echo` describing what the migration does. - Start with an `echo` describing what the migration does.
- Use `$OMARCHY_PATH` to reference the Omarchy directory. - Use `$OMARCHY_PATH` to reference the Omarchy directory.
- Be idempotent. Check existing state before changing it. - Be idempotent. Check existing state before changing it.
- Migrations are strictly ordered and synchronous. A migration that cannot finish must exit non-zero, remain pending, and stop the queue; never mark later migrations complete against state an earlier migration has not established.
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`, - Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and `omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
`omarchy-pkg-missing` when appropriate. `omarchy-pkg-missing` when appropriate.
@@ -163,3 +164,5 @@ omarchy-migrate
Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through the Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through the
normal migration runner. Do not add compatibility migrations for old installer normal migration runner. Do not add compatibility migrations for old installer
layouts; put pre-4 package-layout transition work in the upgrade command instead. layouts; put pre-4 package-layout transition work in the upgrade command instead.
Clearing a privileged file that a retired installer left on disk is the exception, and belongs in a migration whether or not that installer was part of a package layout transition. The upgrade command only runs on a machine still making the 3 to 4 crossing, so anything put there never reaches an install that crossed already, and it never runs at all for an installer that was retired on its own — while the file the installer wrote is still sitting on those machines. The upgrade command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so one migration reaches every population; a copy in the upgrade command would only be a second copy of the same predicate to keep correct. Such a migration must name the defect it clears and match what the old installer actually produced before deleting it. Leave safe administrator-authored files alone; if one still contains the vulnerable privileged action, preserve it under an inactive name rather than discarding custom content or leaving the action executable. A user config that depends on the same retired compatibility path may be repaired in that migration when doing so eliminates an overlapping migration, but only by matching and replacing the exact legacy path while preserving the rest of the file.
+8 -4
View File
@@ -1,4 +1,8 @@
# Omarchy override of cups-browsed's shipped config. The only behavioural # Keep state away from /var/cache/cups, which is writable by the account CUPS
# change vs the upstream default (all-commented) is enabling auto-registration # uses for print filters. cups-browsed is the only writer to this directory.
# of remote IPP printers discovered via Avahi/mDNS. CacheDir /var/cache/cups-browsed
CreateRemotePrinters Yes
# Auto-create queues only for modern driverless IPP printers. Remote queues
# exported by another CUPS server can still be added manually when needed.
CreateIPPPrinterQueues Driverless
CreateRemoteCUPSPrinterQueues No
+90
View File
@@ -0,0 +1,90 @@
#
# File/directory/user/group configuration file for the CUPS scheduler.
# See "man cups-files.conf" for a complete description of this file.
#
# List of events that are considered fatal errors for the scheduler...
#FatalErrors config
# Strip domain in local username?
#StripUserDomain No
# Do we call fsync() after writing configuration or status files?
#SyncOnClose No
# Default user and group for filters/backends/helper programs; this cannot be
# any user or group that resolves to ID 0 for security reasons...
User 209
Group 209
# Administrator user group, used to match @SYSTEM in cupsd.conf policy rules...
# This cannot contain the Group value for security reasons...
SystemGroup cups-browsed sys root
# Are Unix domain socket peer credentials used for authorization?
PeerCred on
# User that is substituted for unauthenticated (remote) root accesses...
#RemoteRoot remroot
# Do we allow file: device URIs other than to /dev/null?
#FileDevice No
# Permissions for configuration and log files...
#ConfigFilePerm 0640
#LogFilePerm 0644
# Location of the file logging all access to the scheduler; may be the name
# "syslog". If not an absolute path, the value of ServerRoot is used as the
# root directory. Also see the "AccessLogLevel" directive in cupsd.conf.
AccessLog /var/log/cups/access_log
# Location of cache files used by the scheduler...
#CacheDir /var/cache/cups
# Location of data files used by the scheduler...
#DataDir /usr/share/cups
# Location of the static web content served by the scheduler...
#DocumentRoot /usr/share/cups/doc
# Location of the file logging all messages produced by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "LogLevel"
# directive in cupsd.conf.
ErrorLog /var/log/cups/error_log
# Location of the file logging all pages printed by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "PageLogFormat"
# directive in cupsd.conf.
PageLog /var/log/cups/page_log
# Location of the file listing all of the local printers...
#Printcap /etc/printcap
# Format of the Printcap file...
#PrintcapFormat bsd
#PrintcapFormat plist
#PrintcapFormat solaris
# Location of all spool files...
#RequestRoot /var/spool/cups
# Location of helper programs...
#ServerBin /usr/lib/cups
# SSL/TLS keychain for the scheduler...
#ServerKeychain ssl
# Location of other configuration files...
#ServerRoot /etc/cups
# Location of scheduler state files...
#StateDir /run/cups
# Location of scheduler/helper temporary files. This directory is emptied on
# scheduler startup and cannot be one of the standard (public) temporary
# directory locations for security reasons...
#TempDir /var/spool/cups/tmp
-1
View File
@@ -1 +0,0 @@
%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol
+8
View File
@@ -0,0 +1,8 @@
# Theme switching is a menu action with no terminal to carry a password prompt,
# and it repaints the browser accent on every switch, so this one write must not
# stop for a password. The argument is spelled out as six hex digits rather than
# a wildcard: the grant covers a color and nothing else, and sudoers matches a
# command's arguments exactly, so it cannot be stretched into extra ones. The
# helper revalidates the same shape, since the terminal path does not come
# through this rule.
%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-theme-set-browser-policy [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+1 -1
View File
@@ -1 +1 @@
%wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl set-timezone * %wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl ^set-timezone [A-Za-z0-9_+][A-Za-z0-9_+.-]*(/[A-Za-z0-9_+][A-Za-z0-9_+.-]*)*$
@@ -0,0 +1,11 @@
[Service]
User=cups-browsed
Group=cups-browsed
CacheDirectory=cups-browsed
CacheDirectoryMode=0750
UMask=0027
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
RestrictSUIDSGID=yes
+1
View File
@@ -0,0 +1 @@
u cups-browsed - "CUPS printer discovery" / -
+1
View File
@@ -1,4 +1,5 @@
run_logged "$OMARCHY_INSTALL/config/theme-system.sh" run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh" run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh" run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
+2
View File
@@ -0,0 +1,2 @@
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
+3 -5
View File
@@ -1,16 +1,14 @@
# Enable services only. Installs are followed by reboot, so don't start/reload # Enable services only. Installs are followed by reboot, so don't start/reload
# daemons mid-install. UFW and hardware-gated services stay in their own scripts. # daemons mid-install. UFW and hardware-gated services stay in their own scripts.
systemctl enable cups.service systemctl enable cups.service
systemctl enable cups-browsed.service
systemctl enable avahi-daemon.service systemctl enable avahi-daemon.service
systemctl enable linux-modules-cleanup.service systemctl enable linux-modules-cleanup.service
systemctl enable docker.socket systemctl enable docker.socket
systemctl enable systemd-resolved.service systemctl enable systemd-resolved.service
systemctl enable NetworkManager.service systemctl enable NetworkManager.service
# Don't let network-online.target (pulled in by cups-browsed) hold up # Don't let network-online.target hold up graphical.target waiting for
# graphical.target waiting for DHCP/Wi-Fi association. Nothing in the session # DHCP/Wi-Fi association. Nothing in the session needs to block on the network.
# needs to block on the network. Mirrors the systemd-networkd-wait-online mask # Mirrors the systemd-networkd-wait-online mask in install/hardware/network.sh.
# in install/hardware/network.sh.
systemctl mask NetworkManager-wait-online.service systemctl mask NetworkManager-wait-online.service
systemctl enable power-profiles-daemon.service systemctl enable power-profiles-daemon.service
systemctl enable sddm.service systemctl enable sddm.service
+3 -6
View File
@@ -6,11 +6,8 @@ ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg /usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true
# Chromium policy directory for theme # Seed Chromium's first run: follow system appearance ("device") instead of dark,
mkdir -p /etc/chromium/policies/managed # and skip the terms-of-service dialog Chromium 151 turned on by default.
chmod a+rw /etc/chromium/policies/managed
# Default Chromium to follow system appearance ("device") instead of dark
mkdir -p /usr/lib/chromium mkdir -p /usr/lib/chromium
echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \ echo '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
/usr/lib/chromium/initial_preferences /usr/lib/chromium/initial_preferences
-1
View File
@@ -4,7 +4,6 @@ run_logged "$OMARCHY_INSTALL/hardware/dell-xps-touchpad-haptics.sh"
run_logged "$OMARCHY_INSTALL/hardware/surface.sh" run_logged "$OMARCHY_INSTALL/hardware/surface.sh"
run_logged "$OMARCHY_INSTALL/hardware/network.sh" run_logged "$OMARCHY_INSTALL/hardware/network.sh"
run_logged "$OMARCHY_INSTALL/hardware/input-group.sh"
run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh" run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh"
-11
View File
@@ -1,11 +0,0 @@
# Give this user privileged input access for dictation tools + xbox controllers to work.
# Recorded for provisioning first-boot user creation and factory reset, granted directly
# when the install user already exists (deferred-provisioning installs create the user at
# first boot instead).
provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}"
mkdir -p "$provisioning_dir"
grep -qxF input "$provisioning_dir/groups" 2>/dev/null || echo input >>"$provisioning_dir/groups"
if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then
usermod -aG input "$OMARCHY_INSTALL_USER"
fi
+7
View File
@@ -0,0 +1,7 @@
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
+168
View File
@@ -0,0 +1,168 @@
# Chromium-family machine policy is mandatory for every profile. Directories
# stay 0755 root:root; omarchy-theme-set-browser-policy is the privileged
# write for color.json.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh"
BROWSER_POLICY_MANAGED_DIRS=(
/etc/chromium/policies/managed
/etc/opt/chrome/policies/managed
/etc/opt/edge/policies/managed
/etc/brave/policies/managed
)
# Ancestors of the managed dirs, shortest first. A writable or attacker-owned
# parent can rename the leaf aside; install -d follows a planted symlink.
BROWSER_POLICY_PARENT_DIRS=(
/etc/chromium
/etc/chromium/policies
/etc/opt/chrome
/etc/opt/chrome/policies
/etc/opt/edge
/etc/opt/edge/policies
/etc/brave
/etc/brave/policies
)
BROWSER_POLICY_FIREFOX_DIRS=(
/usr/lib/firefox/distribution
/opt/zen-browser/distribution
)
BROWSER_POLICY_DEFAULT_COLOR="#1c2027"
browser_policy_purge_dir() {
local dir=$1
as_root find "$dir" -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +
}
browser_policy_parent_hardened() {
local dir=$1
[[ -d $dir && ! -L $dir ]] || return 1
[[ $(stat -c '%a' "$dir") == "755" ]] || return 1
[[ $(stat -c '%U' "$dir") == "root" ]] || return 1
}
browser_policy_dir_hardened() {
browser_policy_parent_hardened "$1"
}
browser_policy_parents_hardened() {
local dir=$1
local parent
for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do
[[ $dir == "$parent"/* ]] || continue
[[ -e $parent || -L $parent ]] || continue
browser_policy_parent_hardened "$parent" || return 1
done
}
browser_policy_setup_parent() {
local dir=$1
if [[ -L $dir || ( -e $dir && ! -d $dir ) ]]; then
as_root rm -rf -- "$dir"
fi
as_root install -d -m 0755 -o root -g root "$dir"
}
browser_policy_setup_parents_for() {
local dir=$1
local parent
for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do
[[ $dir == "$parent"/* ]] || continue
browser_policy_setup_parent "$parent"
done
}
browser_policy_setup_dir() {
local dir=$1
browser_policy_setup_parents_for "$dir"
browser_policy_setup_parent "$dir"
browser_policy_purge_dir "$dir"
}
# Themes are user-installed. Accept only three 0-255 components.
browser_policy_theme_hex() {
local theme_rgb=$1
if [[ $theme_rgb =~ ^[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*$ ]] &&
(( 10#${BASH_REMATCH[1]} < 256 && 10#${BASH_REMATCH[2]} < 256 && 10#${BASH_REMATCH[3]} < 256 )); then
printf '#%02x%02x%02x' "$((10#${BASH_REMATCH[1]}))" "$((10#${BASH_REMATCH[2]}))" "$((10#${BASH_REMATCH[3]}))"
return
fi
printf '%s' "$BROWSER_POLICY_DEFAULT_COLOR"
}
browser_policy_install_color() {
local policy_dir=$1
local hex=$2
local dest=$policy_dir/color.json
local tmp
[[ -d $policy_dir && ! -L $policy_dir ]] || return 0
[[ $hex =~ ^#[0-9a-f]{6}$ ]] || return 1
tmp=$(mktemp) || return 1
printf '{"BrowserThemeColor": "%s", "BrowserColorScheme": "device"}\n' "$hex" >"$tmp"
if [[ -L $dest || -d $dest ]]; then
if ! rm -rf -- "$dest" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
fi
if install -m 0644 -T "$tmp" "$dest" 2>/dev/null; then
rm -f "$tmp"
return 0
fi
rm -f "$tmp"
return 1
}
browser_policy_firefox_policy_file_ok() {
local file=$1
local mode
local group_write
local other_write
[[ -f $file && ! -L $file ]] || return 1
[[ $(stat -c '%U' "$file") == "root" ]] || return 1
mode=$(stat -c '%a' "$file")
group_write=$((8#${mode: -2:1}))
other_write=$((8#${mode: -1}))
(( (group_write & 2) == 0 && (other_write & 2) == 0 ))
}
browser_policy_firefox_hardened() {
local dir=$1
[[ -d $dir && ! -L $dir ]] || return 1
[[ $(stat -c '%a' "$dir") == "755" ]] || return 1
[[ $(stat -c '%U' "$dir") == "root" ]] || return 1
browser_policy_firefox_policy_file_ok "$dir/policies.json"
}
browser_policy_install_firefox_policies() {
local distribution_dir=$1
local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json}
as_root install -m 644 -o root -g root -T "$policies" "$distribution_dir/policies.json"
}
browser_policy_setup_firefox_distribution() {
local distribution_dir=$1
local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json}
browser_policy_setup_parent "$distribution_dir"
browser_policy_purge_dir "$distribution_dir"
browser_policy_install_firefox_policies "$distribution_dir" "$policies"
}
+1 -2
View File
@@ -17,9 +17,8 @@ chromium
clang clang
cliamp cliamp
cups cups
cups-browsed
cups-filters cups-filters
cups-pdf cups-pk-helper
ddcutil ddcutil
docker docker
docker-buildx docker-buildx
+5 -5
View File
@@ -3,11 +3,11 @@
cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf
cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist
# omarchy-settings skips this override until cups-browsed is actually present # Wait for CUPS to own the file, the way omarchy-settings does, so pacman does
# to avoid pacman creating cups-browsed.conf.pacnew during ISO package install. # not turn the override into a .pacnew during ISO package installation.
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -d /etc/cups ]]; then if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-files.conf && -f /etc/cups/cups-files.conf ]]; then
cp -f "$OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf" /etc/cups/cups-browsed.conf install -m 0640 -o root -g cups "$OMARCHY_PATH/etc-overrides/cups-cups-files.conf" /etc/cups/cups-files.conf
rm -f /etc/cups/cups-browsed.conf.pacnew rm -f /etc/cups/cups-files.conf.pacnew
fi fi
source "$OMARCHY_INSTALL/hardware/pacman.sh" source "$OMARCHY_INSTALL/hardware/pacman.sh"
+1 -1
View File
@@ -79,7 +79,7 @@ Turkish|trq
Ukrainian|ua' Ukrainian|ua'
OMARCHY_USERNAME_PATTERN='^[a-z_][a-z0-9_-]*[$]?$' OMARCHY_USERNAME_PATTERN='^[a-z_][a-z0-9_-]*[$]?$'
OMARCHY_RESERVED_USERNAMES='^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$' OMARCHY_RESERVED_USERNAMES='^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|cups-browsed|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$'
OMARCHY_HOSTNAME_PATTERN='^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$' OMARCHY_HOSTNAME_PATTERN='^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$'
OMARCHY_HOSTNAME_DEFAULT='omarchy' OMARCHY_HOSTNAME_DEFAULT='omarchy'
+9 -3
View File
@@ -4,7 +4,7 @@ Omarchy offers an easy way to run Windows through a Docker VM. You can install i
Your machine needs KVM virtualization for this, which most do — but it's sometimes switched off in the BIOS, and the installer will tell you if that's the case. You'll also want the disk space: whatever you give Windows, plus about 10GB for the image itself. Your machine needs KVM virtualization for this, which most do — but it's sometimes switched off in the BIOS, and the installer will tell you if that's the case. You'll also want the disk space: whatever you give Windows, plus about 10GB for the image itself.
The installer asks how much RAM, how many CPU cores, and how much disk to hand over (64GB or more is the sensible floor), then for a Windows username and password. Leave those blank and you get `docker` / `admin`. The download takes a while — 10-15 minutes is normal — and you can follow the progress in the browser at `http://127.0.0.1:8006`. The installer asks how much RAM, how many CPU cores, and how much disk to hand over (64GB or more is the sensible floor), then for a Windows username and password. Leave those blank and you get `docker` / `admin`. The download takes a while — 10-15 minutes is normal — and you can follow the progress in the browser at `http://127.0.0.1:8006`. The browser prompts for the same username and password before opening the console.
![windows-vm](images/windows-vm.webp) ![windows-vm](images/windows-vm.webp)
@@ -26,9 +26,15 @@ omarchy windows vm launch # start and connect
## Sharing files ## Sharing files
The directory `~/Windows` in your home directory is automatically shared with the VM. Put files there if you want them accessible to Windows. The VM has no access to any other part of your file system, so you're safe from anything nasty on the Windows side. Its own virtual disk lives in `~/.windows`. The directory `~/Windows` in your home directory is automatically shared with the VM. Put files there if you want them accessible to Windows. The VM has no access to any other part of your file system, so you're safe from anything nasty on the Windows side. Its own virtual disk is available at `~/.windows`.
The VM's ports are bound to localhost only, so nothing on your network can reach the Windows machine. Those familiar home paths stay on their own filesystems. They can also be symlinks to directories you own, which is useful when the virtual disk lives on a larger drive. The installer measures free space on the filesystem that actually contains `~/.windows`, not necessarily the filesystem containing your home directory.
Keep the disk and shared paths as separate, non-overlapping directories. Removal deliberately empties the disk directory but preserves the shared directory. Immediately before deletion, Omarchy performs a bounded containment check and refuses to remove anything if that check times out or cannot prove the two trees are separate.
Before the VM starts, Omarchy opens and pins those two directories, then bind-mounts the exact directory inodes onto private per-user anchors below `/var/lib/omarchy/windows/mounts`. Docker only sees those root-protected anchors. This preserves custom disk locations while preventing another process running as you from swapping a checked path before the privileged container consumes it. Existing disk and shared directories are tightened to mode `0700` during migration so other local accounts cannot browse their contents.
The VM's ports are bound to localhost only, so nothing on your network can reach the Windows machine. The web console also requires the configured Windows username and password, preventing another local account from driving the VM through port 8006.
## Limits and licensing ## Limits and licensing
+2
View File
@@ -38,6 +38,8 @@ There's a fully commented `alacritty.toml.tpl.sample` in that folder to copy fro
If you want to distribute your theme so others can use it, you need to put it on a public git server, like GitHub. Then people can install it using _Install > Style > Theme_ in the Omarchy menu using that URL. It's recommended that you follow the naming convention of `omarchy-[themename]-theme`, as the theme will show correctly as just `[themename]` in the theme selection menu after installation. If you want to distribute your theme so others can use it, you need to put it on a public git server, like GitHub. Then people can install it using _Install > Style > Theme_ in the Omarchy menu using that URL. It's recommended that you follow the naming convention of `omarchy-[themename]-theme`, as the theme will show correctly as just `[themename]` in the theme selection menu after installation.
That leftover `[themename]` becomes the theme's directory name, so it has to be one Omarchy can hand around safely: it must start with a letter, a digit, or an underscore, and the rest may hold letters, digits, `.`, `_`, `+`, and `-`. Capitals are lowercased for you, but anything else — a space, a quote, a non-English character — is refused at install time rather than turned into a directory name. So `omarchy-tokyo-night-theme`, `omarchy-flexoki_light-theme`, and `omarchy-c++-theme` all install fine.
Remember that once it's installed from a repo, any `.lua`, terminal config or `vscode.json` it ships is dropped, so don't build the theme around those. Remember that once it's installed from a repo, any `.lua`, terminal config or `vscode.json` it ships is dropped, so don't build the theme around those.
You can have your theme added to [the extra themes page](https://omarchy.org/themes/) by sending a pull request to [the omarchy-site repo](https://github.com/omacom-io/omarchy-site). You can have your theme added to [the extra themes page](https://omarchy.org/themes/) by sending a pull request to [the omarchy-site repo](https://github.com/omacom-io/omarchy-site).
+7 -1
View File
@@ -44,7 +44,13 @@ The plain open source Chromium build doesn't ship with the OAuth credentials tha
### How do I add a printer? ### How do I add a printer?
Printing is set up and running out of the box, so a printer on your network is usually already discovered. Launch _Print Settings_ from the app launcher (`Super + Space`) to see what's there, add one by hand, or set the default. Printing to a PDF file works without any printer at all. Printing is set up and running out of the box, and you add each printer yourself from _Print Settings_ in the app launcher (`Super + Space`).
Choose _Add_, and give it a moment to look: a printer plugged in over USB, and most network printers, are found for you. If yours isn't in the list, pick _Network Printer > Internet Printing Protocol (ipp)_ and enter its address — the printer's own display or its web page will tell you what that is, usually something like `192.168.1.50` with a queue of `ipp/print`. _Forward_ then offers a driver, where a modern printer works best on the driverless _IPP Everywhere_ profile and an older one wants the model's own driver.
Right-click a printer and choose _Set as Default_ to pick which one your apps reach for first, and _Properties_ to set paper size, duplex and quality.
Automatic discovery, where printers on the network appear without being added, is temporarily switched off while it's reworked, which is why the first step above is yours rather than automatic. Printing to a PDF file works without any printer at all.
### How do I change where screenshots or screenrecordings are saved? ### How do I change where screenshots or screenrecordings are saved?
+30
View File
@@ -0,0 +1,30 @@
echo "Stop world-writable Chromium and Firefox policy directories"
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
repaired=0
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
repaired=1
done
# Repainting the policy color is cosmetic and the next theme change redoes it.
# Under bash -euo pipefail a failure here would abort the migration before the
# Firefox directories below are hardened, and the marker would never be written.
if (( repaired )); then
omarchy-theme-set-browser || true
fi
for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
if browser_policy_firefox_hardened "$dir"; then
browser_policy_purge_dir "$dir"
continue
fi
browser_policy_setup_parent "$dir"
browser_policy_purge_dir "$dir"
if ! browser_policy_firefox_policy_file_ok "$dir/policies.json"; then
browser_policy_install_firefox_policies "$dir"
fi
done
+20
View File
@@ -0,0 +1,20 @@
echo "Require signed packages from the Omarchy repository"
# The [omarchy] repo predates the Omarchy packaging key, so existing installs
# carry a SigLevel override that also accepts unsigned packages. Packages are
# signed now, so drop the override and let the repo inherit the global
# SigLevel = Required DatabaseOptional like every other repo. Machine-wide and
# self-detecting, so another user's rerun no-ops.
omarchy_sig_override='SigLevel = Optional TrustAll'
if [[ -f /etc/pacman.conf ]] &&
sed -n '/^\[omarchy\]/,/^\[/p' /etc/pacman.conf | grep -qxF "$omarchy_sig_override"; then
# Requiring signatures with an untrusted packaging key would fail every
# omarchy transaction, including the one that could repair it.
if omarchy-pkg-missing omarchy-keyring ||
! sudo pacman-key --list-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 &>/dev/null; then
omarchy-update-keyring
fi
sudo sed -i "/^\[omarchy\]/,/^\[/{/^$omarchy_sig_override$/d}" /etc/pacman.conf
fi
+18
View File
@@ -0,0 +1,18 @@
echo "Skip Chromium's new first-run EULA on machines already on Quattro"
# Chromium 151 flipped MasterPrefs::eula_required from false to true, so an
# unconfigured first run now stops on a blank terms-of-service dialog before the
# browser opens. Omarchy answers that in the seed it writes next to the Chromium
# binary, but that seed is only laid down by a fresh install and by the one-time
# 3.x upgrade, so machines already on Quattro never receive it. Retrofit it here.
#
# The literal is deliberately duplicated rather than sourced: a migration repairs
# the state of its own moment, and must not drift when the seed later changes.
chromium_prefs="/usr/lib/chromium/initial_preferences"
chromium_seed='{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}'
if [[ $(cat "$chromium_prefs" 2>/dev/null) != "$chromium_seed" ]]; then
sudo mkdir -p "$(dirname "$chromium_prefs")"
echo "$chromium_seed" | sudo tee "$chromium_prefs" >/dev/null
fi
+57
View File
@@ -0,0 +1,57 @@
echo "Separate printer discovery from root and print-filter access"
machine_marker="${OMARCHY_CUPS_MIGRATION_MARKER:-/var/lib/omarchy/migrations/1787815267}"
[[ ! -e $machine_marker ]] || exit 0
# Existing releases allowed a desktop user or shared group named cups-browsed,
# which systemd-sysusers would silently reuse for passwordless CUPS access.
if omarchy-pkg-present cups; then
cups_browsed_account=$(getent passwd cups-browsed || true)
cups_browsed_group=$(getent group cups-browsed || true)
if [[ -n $cups_browsed_account || -n $cups_browsed_group ]]; then
IFS=: read -r _ _ cups_browsed_uid cups_browsed_gid cups_browsed_description cups_browsed_home cups_browsed_shell <<<"$cups_browsed_account"
IFS=: read -r _ _ cups_browsed_group_gid cups_browsed_group_members <<<"$cups_browsed_group"
other_primary_user=$(getent passwd | awk -F: -v gid="$cups_browsed_gid" '$1 != "cups-browsed" && $4 == gid { print $1; exit }')
if [[ ! $cups_browsed_uid =~ ^[0-9]+$ || ! $cups_browsed_group_gid =~ ^[0-9]+$ ]] ||
((cups_browsed_uid <= 0 || cups_browsed_uid >= 1000)) ||
[[ $cups_browsed_gid != $cups_browsed_group_gid ]] ||
[[ $cups_browsed_description != "CUPS printer discovery" || $cups_browsed_home != "/" || $cups_browsed_shell != "/usr/bin/nologin" ]] ||
[[ -n $cups_browsed_group_members || -n $other_primary_user ]]; then
echo "Cannot harden printer discovery: the existing cups-browsed user or group is not a dedicated system account." >&2
false
fi
fi
fi
# CUPS-PDF accepts a job-controlled post-processing command in a backend that
# CUPS launches as root. Native application print-to-file support replaces it.
omarchy-pkg-drop cups-pdf
# system-config-printer uses this helper to request printer administration
# through Polkit now that the desktop user's wheel group is no longer @SYSTEM.
if omarchy-pkg-present cups; then
omarchy-pkg-add cups-pk-helper
fi
# Stop the root-running daemon before changing the authorization it relies on.
if systemctl is-active --quiet cups-browsed.service 2>/dev/null; then
sudo systemctl stop cups-browsed.service
fi
if omarchy-pkg-present cups; then
sudo systemctl daemon-reload
sudo systemctl try-reload-or-restart cups.service
fi
# Resume on whether the unit is enabled, not on whether it was running when this
# run started: an interrupted earlier run leaves it stopped, and a retry that
# recomputed that would skip the restart and still write the marker below. A
# masked or disabled unit reports not-enabled and is left alone.
if systemctl is-enabled --quiet cups-browsed.service 2>/dev/null; then
sudo systemctl restart cups-browsed.service
fi
sudo install -Dm644 /dev/null "$machine_marker"
+18
View File
@@ -0,0 +1,18 @@
echo "Drop the default input group grant, which allowed unprivileged keylogging"
# Membership of `input` gives raw read/write access to /dev/input/event*: any
# process running as the user can capture keystrokes and synthesize input. The
# blanket grant is unnecessary: the Xbox-controller and ydotool installers add
# the group themselves when those features are deliberately installed.
#
# Preserve membership where one of those opt-in features is present; removing
# it there would break the feature the user chose to install.
if id -nG "$USER" | grep -qw input; then
if pacman -Qq xpadneo-dkms &>/dev/null || pacman -Qq ydotool &>/dev/null; then
echo "Keeping $USER in the input group: controller or ydotool support is installed."
else
sudo gpasswd -d "$USER" input >/dev/null
echo "Removed $USER from the input group. Log out and back in to apply."
omarchy-state set reboot-required
fi
fi
+72
View File
@@ -0,0 +1,72 @@
echo "Temporarily remove automatic printer discovery"
machine_marker="${OMARCHY_CUPS_BROWSED_REMOVAL_MARKER:-/var/lib/omarchy/migrations/1788009111}"
[[ ! -e $machine_marker ]] || exit 0
omarchy-pkg-present cups-browsed || exit 0
# Check the full removal transaction before changing the service or queues.
pacman -Rs --print cups-browsed >/dev/null
# Disable the unit while its package still owns the unit file so systemd can
# remove the enable symlink cleanly.
if systemctl is-enabled --quiet cups-browsed.service 2>/dev/null; then
sudo systemctl disable --now cups-browsed.service >/dev/null
elif systemctl is-active --quiet cups-browsed.service 2>/dev/null; then
sudo systemctl stop cups-browsed.service >/dev/null
fi
# cups-browsed leaves its implicitclass queues behind when stopped. Remove idle
# discovery queues before removing the backend they require, but leave queues
# with jobs for the user to resolve.
#
# A healthy CUPS server with no configured printers reports this condition on
# stderr and exits 1. Treat that as an empty queue list; every other failure
# keeps the migration pending so it can be retried.
if queue_report=$(LC_ALL=C lpstat -v 2>&1); then
:
elif [[ $queue_report == "lpstat: No destinations added." ]]; then
queue_report=""
else
printf '%s\n' "$queue_report" >&2
exit 1
fi
generated_queues=$(printf '%s\n' "$queue_report" |
sed -n 's|^device for \(.*\): implicitclass://.*|\1|p')
while IFS= read -r queue; do
[[ -n $queue ]] || continue
if ! reject_error=$(sudo cupsreject -r "Printer discovery has been removed from Omarchy" "$queue" 2>&1); then
if LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then
printf '%s\n' "$reject_error" >&2
exit 1
else
continue
fi
fi
if job_report=$(LC_ALL=C lpstat -o "$queue" 2>&1); then
[[ -z $job_report ]] || continue
elif LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then
printf '%s\n' "$job_report" >&2
exit 1
else
# The queue disappeared after the initial snapshot, which is already the
# desired state.
continue
fi
if ! delete_error=$(sudo lpadmin -x "$queue" 2>&1); then
# Treat a concurrent disappearance as success. A queue that still exists
# means CUPS did not complete the deletion, so retry the migration later.
if LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then
printf '%s\n' "$delete_error" >&2
exit 1
fi
fi
done <<<"$generated_queues"
omarchy-pkg-drop cups-browsed >/dev/null
sudo install -Dm644 /dev/null "$machine_marker"
+339
View File
@@ -0,0 +1,339 @@
echo "Remove privileged files left behind by retired Omarchy installers"
sudoers_dir="${OMARCHY_SUDOERS_DIR:-/etc/sudoers.d}"
systemd_dir="${OMARCHY_SYSTEMD_SYSTEM_DIR:-/etc/systemd/system}"
machine_marker="${OMARCHY_RETIRED_INSTALLER_ARTIFACTS_MARKER:-/var/lib/omarchy/migrations/1788025225}"
reload_needed_marker="$machine_marker.daemon-reload"
[[ ! -e $machine_marker ]] || exit 0
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
# Three installers that no longer exist each left a root-owned file behind, and
# nothing in Omarchy has ever removed any of them. Each is judged against what
# the installer that wrote it actually produced, so a file of the same name that
# an administrator wrote themselves is left alone.
#
# Emit the lines a parser would act on: comments and blanks dropped, backslash
# continuations joined, and runs of whitespace collapsed so a reformatted copy
# still compares equal. Reads the body on stdin, because /etc/sudoers.d is 0750
# root:root and the caller has to hand us an elevated read.
#
# Comments are tested before continuations are joined, which is the order every
# consumer here uses: udev's parse_file discards a '#' line without looking at a
# trailing backslash (`udevadm verify` on "# disabled \" plus a bogus key reports
# the error on line 2), sudo's toke.l comment rule consumes to the newline and
# clears its continuation flag, and systemd's config_parse tests the comment
# characters before appending to a continuation. Joining first would let a
# comment ending in a backslash swallow the live line beneath it.
#
# FORMAT is sudoers or systemd. systemd takes ';' as well as '#'. sudo does not
# treat every '#' as a comment: toke.l has INITIAL rules for ^#include and
# ^#includedir, and its comment pattern excludes '#' followed by a digit or
# -digit so those reach the ID token as a numeric uid user spec. Those lines are
# active directives, and a file carrying one must not read as though it held only
# generated lines.
active_lines() {
local format="$1"
local comments='#'
local line logical=""
[[ $format == "systemd" ]] && comments='#;'
while IFS= read -r line || [[ -n $line ]]; do
if [[ $line =~ ^[[:space:]]*[$comments] ]] &&
! { [[ $format == "sudoers" ]] && sudoers_hash_is_active "$line"; }; then
# The two consumers part company here. sudo ends the logical line at a
# comment and keeps what came before it, so `visudo -cf` reads a spec
# ending in a backslash, then a comment, then a second spec as two live
# specs; dropping the pending half would hide an administrator's grant and
# let this file read as though the installer had written all of it. systemd
# resumes the continuation instead: `systemd-analyze verify` on "ExecStop=\"
# + "; c" + a path resolves that path, so the pending half has to stay.
if [[ $format == "sudoers" ]]; then
emit_logical "$logical"
logical=""
fi
continue
fi
if [[ $line == *\\ ]]; then
logical+="${line%\\} "
continue
fi
emit_logical "$logical$line"
logical=""
done
# A file whose last line ends in a backslash still carries a live directive for
# systemd: `systemd-analyze verify` resolves an ExecStop= written that way.
# udev ignores the dangling line and sudo rejects the file outright, so emitting
# it costs those two nothing.
emit_logical "$logical"
}
# One logical line, whitespace collapsed so a reformatted copy still compares
# equal, and nothing at all for a line that held only whitespace.
emit_logical() {
local -a parts
read -ra parts <<<"$1"
if (( ${#parts[@]} )); then
printf '%s\n' "${parts[*]}"
fi
}
sudoers_hash_is_active() {
local line="$1"
[[ $line =~ ^[[:space:]]*#include[[:blank:]] ]] && return 0
[[ $line =~ ^[[:space:]]*#includedir[[:blank:]] ]] && return 0
[[ $line =~ ^[[:space:]]*#-?[0-9] ]] && return 0
return 1
}
# install/preflight/first-run-mode.sh (2025-08-25 to 2026-05-25) granted the
# installing account passwordless sudo for the rest of the first boot, including
# an unrestricted /usr/bin/systemctl from 2025-10-14 on -- enough to link and
# start a unit of the user's own, which is root. bin/omarchy-first-run was meant
# to delete the grant, but it clears its first-run.mode guard as the very first
# statement and only reaches the removal after eight set -e steps, two of which
# touch the network. Any failure in between leaves the grant on the machine with
# nothing left to retry it.
#
# The installer rewrote this file nine times across two locations -- the last two
# bodies came from install/post-install/first-run-mode.sh, whose cleanup alias
# names /usr/bin/rm as well as /bin/rm -- and only the later ones carry both
# Cmnd_Alias lines, so keying on those would walk past the earlier ones. Instead
# require every active line to be one the installer itself emitted, plus at least
# one line that is unmistakably this grant: its own self-cleanup. One
# hand-written line anywhere in the file and it is not ours to delete.
first_run_sudoers_is_generated() {
local spec_pattern='^([^[:space:]]+) ALL=\(ALL\) NOPASSWD: (.+)$'
local marker_pattern='^/bin/rm -f /home/([^/]+)/\.local/state/omarchy/first-run\.mode$'
local line user command marker_user generated_user=""
local seen_any=0 seen_marker=0 seen_spec=0
while IFS= read -r line; do
seen_any=1
case "$line" in
"Cmnd_Alias SYMLINK_RESOLVED = /usr/bin/ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf")
continue
;;
"Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run" | \
"Cmnd_Alias FIRST_RUN_CLEANUP = /bin/rm -f /etc/sudoers.d/first-run, /bin/rm -f /etc/sudoers.d/99-omarchy-installer-reboot" | \
"Cmnd_Alias FIRST_RUN_CLEANUP = /usr/bin/rm -f /etc/sudoers.d/first-run, /bin/rm -f /etc/sudoers.d/first-run")
seen_marker=1
continue
;;
esac
# Everything else the installer wrote is a user spec naming the installing
# account, whose name cannot be assumed here: it may since have been renamed
# or removed, and a second account runs this migration too.
if [[ ! $line =~ $spec_pattern ]]; then
return 1
fi
user=${BASH_REMATCH[1]}
command=${BASH_REMATCH[2]}
if [[ -n $generated_user && $user != "$generated_user" ]]; then
return 1
fi
generated_user=$user
seen_spec=1
case "$command" in
"/usr/bin/systemctl" | "/usr/bin/ufw" | "/usr/bin/ufw-docker" | \
"/usr/bin/gtk-update-icon-cache" | "/usr/bin/udevadm" | \
"/usr/bin/tee /etc/udev/rules.d/*" | "SYMLINK_RESOLVED")
continue
;;
"FIRST_RUN_CLEANUP" | "/bin/rm -f /etc/sudoers.d/first-run")
seen_marker=1
continue
;;
esac
if [[ $command =~ $marker_pattern ]]; then
marker_user=${BASH_REMATCH[1]}
[[ $marker_user == "$generated_user" ]] || return 1
seen_marker=1
continue
fi
return 1
done < <(active_lines sudoers)
(( seen_any && seen_marker && seen_spec ))
}
# bin/omarchy-install-tailscale (2025-08-22 to 2026-02-02) ran
# "echo \"\$USER ALL=(ALL) NOPASSWD: \$(which tsui)\" | sudo tee
# /etc/sudoers.d/tsui" one line after installing tsui by piping a vendor script
# to bash with no sudo at all, so the path it resolved was usually the user's own
# ~/.local/bin. Overwrite that file, run sudo tsui, and you are root. The grant
# goes whatever the path turned out to be: the feature was dropped from Omarchy,
# and unrestricted NOPASSWD on a TUI that can shell out is an escalation from a
# root-owned path too.
tsui_sudoers_is_generated() {
local spec_pattern='^[^[:space:]]+ ALL=\(ALL\) NOPASSWD: ([^[:space:]]+)$'
local line command="" count=0
while IFS= read -r line; do
count=$(( count + 1 ))
if (( count > 1 )); then
return 1
fi
if [[ ! $line =~ $spec_pattern ]]; then
return 1
fi
command=${BASH_REMATCH[1]}
done < <(active_lines sudoers)
if (( count == 1 )) && [[ ${command##*/} == "tsui" ]]; then
return 0
fi
return 1
}
# install/plymouth.sh wrote this unit for two days (2025-07-05 to 2025-07-07)
# with an unquoted heredoc, so ExecStop names the installing user's home. The
# unit is enabled WantedBy=multi-user.target, so systemd runs that path as uid 0
# on every shutdown, with no hardware event needed to reach it.
plymouth_unit_runs_from_home() {
local binary="omarchy-plymouth-shutdown-sync"
local exec_stop_pattern='^ExecStop[[:space:]]*=[[:space:]]*(.*)$'
local home_pattern="^/.+/\\.local/share/omarchy/bin/$binary\$"
local line word
local -a words
local matched=1
while IFS= read -r line; do
if [[ ! $line =~ $exec_stop_pattern ]]; then
continue
fi
read -ra words <<<"${BASH_REMATCH[1]}"
if (( ! ${#words[@]} )); then
# An empty assignment resets the list, so nothing named before it still
# runs. `systemd-analyze verify` reports the missing command for a unit
# with one ExecStop=, and reports nothing once a bare ExecStop= follows it.
# An administrator who neutralised the unit this way is left alone.
matched=1
continue
fi
# systemd reads -, @, +, ! and : ahead of the command as flags, not as part
# of the path it runs.
word=${words[0]}
while [[ $word == [-@+!:]* ]]; do
word=${word:1}
done
if [[ $word =~ $home_pattern ]]; then
# Non-empty ExecStop= assignments append to the command list. Once a
# vulnerable command is present it stays live until an empty assignment
# explicitly resets the list; a later packaged command does not replace it.
matched=0
fi
done < <(active_lines systemd)
return $matched
}
# /etc/sudoers.d is 0750 root:root as shipped, and omarchy-migrate runs as the
# logged-in user, so an unelevated [[ -f ]] on a file in there is false whether or
# not the file exists and an unelevated read returns nothing. Both tests and both
# reads have to be elevated or this migration reports success having done nothing.
first_run_sudoers="$sudoers_dir/first-run"
tsui_sudoers="$sudoers_dir/tsui"
fail_privileged_repair() {
echo "Cannot complete the privileged installer-artifact repair. An administrator must run omarchy-migrate to repair this machine." >&2
exit 1
}
# This is a machine-wide repair with per-user migration markers. A root-owned,
# readable marker lets later non-sudo users finish their own migration run after
# one privileged account has inspected and repaired the machine. Until then,
# the migration fails loudly and remains pending. After an administrator repairs
# the machine, this marker lets every other account complete without using sudo.
if ! as_root true 2>/dev/null; then
fail_privileged_repair
fi
# Removing a unit and reloading systemd are one repair. Persist the second half
# before removing the file so a failed daemon-reload cannot be forgotten on a
# retry that now sees no unit on disk.
if [[ -e $reload_needed_marker ]]; then
if ! as_root systemctl daemon-reload >/dev/null 2>&1; then
fail_privileged_repair
fi
if ! as_root rm -f "$reload_needed_marker"; then
fail_privileged_repair
fi
fi
inspect_sudoers_file() {
local file="$1" predicate="$2" kind content
# Emit an explicit state from the elevated process. A bare `sudo test -f` in
# an if-condition makes "file missing" indistinguishable from "sudo failed",
# which could mark a live grant repaired without ever reading it.
if ! kind=$(as_root bash -c 'if [[ -f $1 ]]; then printf file; elif [[ -e $1 ]]; then printf other; else printf missing; fi' bash "$file"); then
fail_privileged_repair
fi
[[ $kind == "file" ]] || return 0
if ! content=$(as_root cat "$file"); then
fail_privileged_repair
fi
if "$predicate" <<<"$content"; then
if ! as_root rm -f "$file"; then
fail_privileged_repair
fi
fi
}
inspect_sudoers_file "$first_run_sudoers" first_run_sudoers_is_generated
inspect_sudoers_file "$tsui_sudoers" tsui_sudoers_is_generated
# /etc/systemd/system is 0755, so this one needs no elevation to look at.
plymouth_unit="$systemd_dir/omarchy-plymouth-shutdown.service"
if [[ -f $plymouth_unit ]] && plymouth_unit_runs_from_home <"$plymouth_unit"; then
# Disable, never stop. Stopping the unit is precisely what runs ExecStop, and
# ExecStop is the path this migration exists to keep root away from; disabling
# only drops the multi-user.target symlink.
if ! as_root install -Dm644 /dev/null "$reload_needed_marker"; then
fail_privileged_repair
fi
if ! as_root systemctl disable omarchy-plymouth-shutdown.service >/dev/null 2>&1; then
fail_privileged_repair
fi
if ! as_root rm -f "$plymouth_unit"; then
fail_privileged_repair
fi
# systemd keeps serving the copy it already loaded until it rereads the
# directory, so without this the unit is still there to run at shutdown.
if ! as_root systemctl daemon-reload >/dev/null 2>&1; then
fail_privileged_repair
fi
if ! as_root rm -f "$reload_needed_marker"; then
fail_privileged_repair
fi
fi
if ! as_root install -Dm644 /dev/null "$machine_marker"; then
fail_privileged_repair
fi
+257
View File
@@ -0,0 +1,257 @@
echo "Repair legacy XCompose and remove vulnerable Omarchy 3 power udev rules"
xcompose="$HOME/.XCompose"
packaged_xcompose="$OMARCHY_PATH/default/xcompose"
legacy_xcompose_pattern='^[[:space:]]*include[[:space:]]+"[^"]*/\.local/share/omarchy/default/xcompose"[[:space:]]*$'
# Omarchy 3 pointed the user's compose file through the checkout compatibility
# link. Preserve their own sequences while moving that include to the packaged
# tree. A failed live restart is harmless: the next graphical login reads the
# repaired file.
if [[ -f $xcompose ]] && grep -Eq "$legacy_xcompose_pattern" "$xcompose"; then
xcompose_replacement=${packaged_xcompose//\\/\\\\}
xcompose_replacement=${xcompose_replacement//&/\\&}
xcompose_replacement=${xcompose_replacement//|/\\|}
sed -i -E "s|^([[:space:]]*include[[:space:]]+\")[^\"]*/\\.local/share/omarchy/default/xcompose\"[[:space:]]*$|\\1$xcompose_replacement\"|" "$xcompose"
omarchy-restart-xcompose >/dev/null 2>&1 || true
fi
rules_dir=/etc/udev/rules.d
reload_marker_prefix=/var/lib/omarchy/migrations/1788102906-udev-reload-needed
udev_control=/run/udev/control
install_command=/usr/bin/install
mv_command=/usr/bin/mv
rm_command=/usr/bin/rm
udevadm_command=/usr/bin/udevadm
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
# Omarchy 3 generated these two rules with an unquoted heredoc, so the installing
# user's $HOME was expanded and the file on disk names that absolute home path.
# udev runs RUN+= as root, and
# ~/.local/share/omarchy is a symlink that same unprivileged user owns: replacing
# it with a tree of their own and provoking a power_supply event runs their code
# as root. Quattro ships the rules as 99-omarchy-*.rules under /usr/bin, but the
# one-shot migration that swept the old filenames was itself dropped, so an
# install that came up through the 3.x line keeps the old file until this
# migration removes it.
#
# Pre-4 layout work normally belongs in the Omarchy 4 upgrade command, but that
# command only runs on a machine still making the crossing, so an install that
# crossed already would never see it. The upgrade command ends by running
# omarchy-migrate, so this covers the installs still to upgrade as well.
#
# Only remove an exact two-line body that one of the retired installers wrote.
# A same-named file with no active vulnerable RUN survives; one that still has
# the vulnerable command but also contains administrator changes is quarantined
# under a non-.rules suffix for review.
rule_runs_from_home() {
local file="$1" binary="$2"
local run_pattern='RUN([[:space:]]*\{[^}]*\})?[[:space:]]*(\+|:)?=[[:space:]]*(e)?"([^"]*)"'
local line logical="" rest command
while IFS= read -r line || [[ -n $line ]]; do
# udev tests for a comment before it joins continuations, and skipping one
# does not end a continuation already under way. Both halves verified with
# `udevadm verify`: "# disabled \" followed by a bogus key reports the error
# on line 2, so a comment's own trailing backslash swallows nothing, while
# 'SUBSYSTEM=="power_supply" \' + "# c" + ', RUN+="..."' reports its style
# warning on line 1, so the rule spans the comment. Testing the comment after
# the join would hide a live rule; clearing the pending line here would hide
# one just as well.
if [[ $line =~ ^[[:space:]]*# ]]; then
continue
fi
# A trailing backslash continues the rule on the next line.
if [[ $line == *\\ ]]; then
logical+=${line%\\}
continue
fi
rest=$logical$line
logical=""
while [[ $rest =~ $run_pattern ]]; do
command=${BASH_REMATCH[4]}
rest=${rest#*"${BASH_REMATCH[0]}"}
# This is only the fail-closed detector; wholesale deletion still requires
# an exact historical body below. Match spacing edits and wrapper arguments
# conservatively so a modified active rule is never mistaken for a safe one.
if [[ $command == *"/.local/share/omarchy/bin/$binary"* ]]; then
return 0
fi
done
done <"$file"
return 1
}
mark_reload_needed() {
if ! as_root "$install_command" -Dm644 /dev/null "$reload_needed_marker"; then
echo "Administrator privileges are required to repair the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
}
# The retired installers overwrote each file with one of five known two-line
# bodies. Only those exact bodies are safe to delete wholesale. If a vulnerable
# rule has since been edited or extended, preserve it under an inactive name
# rather than taking unrelated rules with it.
rule_is_exact_generated() {
local file="$1" binary="$2"
local prefix suffix home expected
local -a lines
mapfile -t lines <"$file"
(( ${#lines[@]} == 2 )) || return 1
case "$binary" in
omarchy-powerprofiles-set)
# Initial userland helper: separate AC/battery units and arguments.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile-battery --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set battery"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile-ac --property=After=power-profiles-daemon.service '"$home"'/.local/share/omarchy/bin/omarchy-powerprofiles-set ac"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# USB-C support: one fixed transient-unit name and no profile argument.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile --property=After=power-profiles-daemon.service '"$home"'/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# The final Omarchy 3 revision dropped the fixed transient-unit name.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service '"$home"'/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
;;
omarchy-wifi-powersave)
# Initial Wi-Fi helper: invoke the userland command directly.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="'
suffix='/.local/share/omarchy/bin/omarchy-wifi-powersave on"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="'"$home"'/.local/share/omarchy/bin/omarchy-wifi-powersave off"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# Later revision deferred each change through its own transient unit.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-on '
suffix='/.local/share/omarchy/bin/omarchy-wifi-powersave on"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-off '"$home"'/.local/share/omarchy/bin/omarchy-wifi-powersave off"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
;;
esac
return 1
}
finish_pending_reload() {
# With no control socket there is no running udevd holding the deleted rule;
# the next daemon start reads the directory from disk. If a daemon is running,
# a failed reload must keep this migration pending so the in-memory root rule
# cannot outlive the per-user completion marker.
if [[ -e $udev_control ]] && ! as_root "$udevadm_command" control --reload 2>/dev/null; then
echo "Could not reload udev after removing a vulnerable legacy rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if ! as_root "$rm_command" -f "$reload_needed_marker"; then
echo "Could not finish the legacy udev-rule repair. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
}
quarantine_rule() {
local rule_file="$1"
local backup="$rule_file.omarchy-disabled"
local suffix=0
# udev only loads files ending in .rules. Preserve an administrator-modified
# file byte-for-byte under a suffix udev ignores instead of either deleting
# their additions or leaving its user-controlled command active as root.
while [[ -e $backup || -L $backup ]]; do
((++suffix))
backup="$rule_file.omarchy-disabled.$suffix"
done
mark_reload_needed
if ! as_root "$mv_command" --no-clobber -- "$rule_file" "$backup"; then
echo "Administrator privileges are required to quarantine the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if [[ -e $rule_file || -L $rule_file ]]; then
echo "Could not quarantine the vulnerable legacy udev rule at $rule_file. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
finish_pending_reload
echo "Quarantined the modified legacy rule as $backup so udev cannot execute it. Review the preserved file before restoring any safe custom actions." >&2
}
if [[ -d $rules_dir && ! -x $rules_dir ]]; then
echo "Could not inspect legacy udev rules under $rules_dir. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
for legacy_rule in "99-power-profile.rules:omarchy-powerprofiles-set" "99-wifi-powersave.rules:omarchy-wifi-powersave"; do
rule_name="${legacy_rule%%:*}"
rule_file="$rules_dir/$rule_name"
binary="${legacy_rule##*:}"
reload_needed_marker="$reload_marker_prefix-$rule_name"
# The marker is written before removal so a crash cannot lose the need to
# reload. Only consume it early when the corresponding active file is already
# gone; another user's concurrent run may still be between those two steps.
if [[ -e $reload_needed_marker && ! -e $rule_file && ! -L $rule_file ]]; then
finish_pending_reload
fi
if [[ -f $rule_file && ! -r $rule_file ]]; then
echo "Could not inspect the legacy udev rule at $rule_file. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if [[ -f $rule_file ]] && rule_is_exact_generated "$rule_file" "$binary"; then
mark_reload_needed
if ! as_root "$rm_command" -f "$rule_file"; then
echo "Administrator privileges are required to remove the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
finish_pending_reload
elif [[ -f $rule_file ]] && rule_runs_from_home "$rule_file" "$binary"; then
quarantine_rule "$rule_file"
fi
# If an interrupted removal was followed by an administrator installing a
# safe replacement, reload that replacement before clearing the old marker.
if [[ -e $reload_needed_marker ]]; then
finish_pending_reload
fi
done
+12
View File
@@ -0,0 +1,12 @@
echo "Point rc-channel installs at the rc package repository"
# pacman-rc.conf shipped with [omarchy] pointing at the edge repository, a
# leftover from when release candidates published there. Candidates now publish
# to the dedicated rc channel, so a machine on the rc mirror was taking its
# omarchy packages from edge. Repoint only a conf that still carries the
# shipped pairing: an administrator who chose another combination keeps it.
if grep -q "https://rc-mirror.omarchy.org/" /etc/pacman.d/mirrorlist &&
grep -q "^Server = https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
sudo sed -i "s|^Server = https://pkgs.omarchy.org/edge/|Server = https://pkgs.omarchy.org/rc/|" /etc/pacman.conf
echo "Switched the [omarchy] repository to the rc channel to match this machine's rc mirror."
fi
+133
View File
@@ -0,0 +1,133 @@
echo "Disable SSH password authentication, or sshd itself when no key is authorized"
config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
authorized_keys="$HOME/.ssh/authorized_keys"
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
# Passwords staying enabled is the state the machine has been living with, so a
# condition this migration cannot repair completes with a notice instead of
# failing and holding up every migration queued behind it. Only missing
# privileges stay pending below, because rerunning from a terminal fixes that.
skip() {
echo "$1 SSH password authentication remains enabled; run omarchy-setup-security-sshd to harden manually."
exit 0
}
# The fixed setup command writes this file itself. Its presence is also the
# machine-wide completion state, so migrations run by another account no-op.
if [[ -e $config || -L $config ]]; then
exit 0
fi
# Earlier versions enabled sshd before importing the key, but did not leave a
# marker saying that Omarchy configured it. Limit the repair to a daemon that is
# enabled or currently exposed and a user who already has a usable authorized
# key. A machine that never set SSH up exits without prompting for privileges.
if ! systemctl is-enabled --quiet sshd.service 2>/dev/null &&
! systemctl is-active --quiet sshd.service 2>/dev/null; then
exit 0
fi
# sshd reads authorized_keys one entry per line, while ssh-keygen -lf
# fingerprints whole files in formats sshd does not accept there — a private
# key copied in by mistake passes the file-level check even though sshd finds
# no usable entry in it. Ask sshd's question instead: does any single line
# parse as a public key?
has_usable_key() {
local line
while IFS= read -r line || [[ -n $line ]]; do
if [[ $line =~ ^[[:space:]]*(#|$) ]]; then
continue
fi
if ssh-keygen -lf /dev/stdin <<<"$line" >/dev/null 2>&1; then
return 0
fi
done <"$authorized_keys"
return 1
}
# A file that exists but cannot be read leaves the key question unanswered; do
# not treat it as proof the machine is password-only. [[ -f ]] and the read
# both follow symlinks on purpose: a dotfiles-managed authorized_keys link with
# a working key must not count as keyless.
if [[ -f $authorized_keys && ! -r $authorized_keys ]]; then
skip "Could not read $authorized_keys to check for a usable key."
fi
# The old setup command enabled sshd before importing a key, so an aborted run
# left a password-only server exposed. Without a usable key there is nothing to
# harden: close the hole Omarchy opened by disabling the server. Omarchy is a
# desktop distro, so the console remains; re-enabling password SSH afterwards
# is an intentional, informed choice the warning explains how to make.
if [[ ! -f $authorized_keys ]] || ! has_usable_key; then
if ! as_root systemctl disable --now sshd.service; then
echo "Administrator privileges are required to close the password-only SSH server. Run omarchy-migrate again from a terminal." >&2
exit 1
fi
echo "No usable SSH key is authorized, so sshd only accepted password logins. The SSH server has been disabled: run omarchy-setup-security-sshd to set it up with key-based authentication, or re-enable sshd to accept password logins anyway."
exit 0
fi
# Under StrictModes, sshd's default, a group- or world-writable home directory,
# ~/.ssh, or authorized_keys makes sshd ignore the key that just validated, and
# passwords would then be the only way in. Tighten the two paths the setup
# command owns, exactly as it does; the home directory is not ours to change.
home_mode=$(stat -c '%a' "$HOME" 2>/dev/null) || skip "Could not inspect the permissions on $HOME."
if (( 8#$home_mode & 8#022 )); then
skip "$HOME is group- or world-writable, so sshd would ignore the authorized key."
fi
if ! chmod 700 "$HOME/.ssh" || ! chmod 600 "$authorized_keys"; then
skip "Could not tighten the permissions on $authorized_keys."
fi
echo "Disabling SSH password authentication on the existing key-based SSH setup..."
if ! as_root install -Dm644 /dev/stdin "$config" <<'CONF'
# Written by Omarchy once an SSH key was already authorized.
# Delete this file and reload sshd to allow password logins again.
PasswordAuthentication no
KbdInteractiveAuthentication no
CONF
then
echo "Administrator privileges are required to harden the existing SSH setup. Run omarchy-migrate again from a terminal." >&2
exit 1
fi
# The drop-in itself is always valid, so a rejection means the configuration
# was already broken before it arrived — the administrator's to repair.
if ! as_root sshd -t; then
as_root rm -f -- "$config" || true
skip "sshd rejected its configuration."
fi
effective_config=$(as_root sshd -T) || {
as_root rm -f -- "$config" || true
skip "Could not inspect sshd's effective configuration."
}
# Syntax alone is insufficient because sshd uses the first value it reads. An
# sshd_config predating the packaged sshd_config.d Include never reads the
# drop-in at all, and an earlier administrator rule overrides it. Either way
# the file is ineffective: remove it rather than claiming the machine is
# protected.
if ! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
as_root rm -f -- "$config" || true
skip "sshd does not apply the hardening drop-in, so an earlier rule or a config without the sshd_config.d include wins."
fi
# An enabled but deliberately stopped daemon picks the file up on its next
# start. Reload only a daemon that is currently serving connections so existing
# sessions survive while new ones get the hardened policy.
if systemctl is-active --quiet sshd.service 2>/dev/null; then
if ! as_root systemctl reload sshd.service; then
echo "The hardening config is installed and valid, but sshd did not reload; it takes effect when sshd next restarts." >&2
exit 0
fi
fi
+3
View File
@@ -138,6 +138,7 @@ BorderSurface {
radius: 0 radius: 0
} }
contentItem: Text { contentItem: Text {
textFormat: Text.PlainText
text: root.tooltipText text: root.tooltipText
color: root.tooltipForeground color: root.tooltipForeground
font.family: root.fontFamily font.family: root.fontFamily
@@ -158,6 +159,7 @@ BorderSurface {
spacing: Style.spacing.controlGap spacing: Style.spacing.controlGap
Text { Text {
textFormat: Text.PlainText
visible: root.iconText !== "" visible: root.iconText !== ""
text: root.iconText text: root.iconText
color: root.selected ? root._selectedColor : root.foreground color: root.selected ? root._selectedColor : root.foreground
@@ -177,6 +179,7 @@ BorderSurface {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.text !== "" visible: root.text !== ""
text: root.text text: root.text
color: root.selected ? root._selectedColor : root.foreground color: root.selected ? root._selectedColor : root.foreground
+2
View File
@@ -69,6 +69,7 @@ Item {
Text { Text {
id: messageText id: messageText
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
@@ -105,6 +106,7 @@ Item {
radius: 0 radius: 0
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: modelData text: modelData
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground) color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
+3
View File
@@ -71,6 +71,7 @@ Item {
spacing: Style.spacing.labelGap spacing: Style.spacing.labelGap
Text { Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== "" visible: root.showLabel && root.label !== ""
text: root.label text: root.label
color: Qt.darker(root.foreground, 1.4) color: Qt.darker(root.foreground, 1.4)
@@ -110,6 +111,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: chevron.left anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -214,6 +216,7 @@ Item {
: "transparent" : "transparent"
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
+6
View File
@@ -259,6 +259,7 @@ Item {
spacing: Style.spacing.labelGap spacing: Style.spacing.labelGap
Text { Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== "" visible: root.showLabel && root.label !== ""
text: root.label text: root.label
color: Qt.darker(root.foreground, 1.4) color: Qt.darker(root.foreground, 1.4)
@@ -298,6 +299,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: chevron.left anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -451,6 +453,7 @@ Item {
: Border.controlSpec("normal", root.foreground, root.accent) : Border.controlSpec("normal", root.foreground, root.accent)
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: root.loadingOptions ? "󰦖" : "󰑐" text: root.loadingOptions ? "󰦖" : "󰑐"
color: root.foreground color: root.foreground
@@ -486,6 +489,7 @@ Item {
height: popup.height - searchHeader.height - Style.spacing.xxs - 1 height: popup.height - searchHeader.height - Style.spacing.xxs - 1
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
visible: resultList.count === 0 visible: resultList.count === 0
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText) text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
@@ -581,6 +585,7 @@ Item {
spacing: Style.spacing.xxs spacing: Style.spacing.xxs
Text { Text {
textFormat: Text.PlainText
text: modelData.label text: modelData.label
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -589,6 +594,7 @@ Item {
width: parent.width width: parent.width
} }
Text { Text {
textFormat: Text.PlainText
visible: text !== "" visible: text !== ""
text: modelData.description text: modelData.description
color: Qt.darker(root.foreground, 1.5) color: Qt.darker(root.foreground, 1.5)
+1
View File
@@ -25,6 +25,7 @@ Column {
spacing: Style.spacing.md spacing: Style.spacing.md
Text { Text {
textFormat: Text.PlainText
visible: root.label !== "" visible: root.label !== ""
text: root.label text: root.label
color: Qt.darker(root.foreground, 1.4) color: Qt.darker(root.foreground, 1.4)
+1
View File
@@ -25,6 +25,7 @@ Item {
Text { Text {
id: glyph id: glyph
textFormat: Text.PlainText
// Keep the shared line box and baseline intact. Correcting only the // Keep the shared line box and baseline intact. Correcting only the
// horizontal painted bounds avoids per-glyph vertical drift. // horizontal painted bounds avoids per-glyph vertical drift.
anchors.centerIn: parent anchors.centerIn: parent
+1
View File
@@ -69,6 +69,7 @@ BorderSurface {
Behavior on color { ColorAnimation { duration: 60 } } Behavior on color { ColorAnimation { duration: 60 } }
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: root.iconText text: root.iconText
color: root.enabled color: root.enabled
+3
View File
@@ -48,6 +48,7 @@ Item {
width: parent.width width: parent.width
Text { Text {
textFormat: Text.PlainText
visible: root.title !== "" visible: root.title !== ""
text: root.title text: root.title
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0))) width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
@@ -75,6 +76,7 @@ Item {
Text { Text {
id: detailText id: detailText
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: root.detail text: root.detail
color: root.dim color: root.dim
@@ -87,6 +89,7 @@ Item {
Text { Text {
id: metaText id: metaText
textFormat: Text.PlainText
width: parent.width width: parent.width
text: root.meta.toUpperCase() text: root.meta.toUpperCase()
visible: text !== "" visible: text !== ""
+4
View File
@@ -11,6 +11,10 @@ Text {
property string fontFamily: Style.font.family property string fontFamily: Style.font.family
property real fontSize: Style.font.caption property real fontSize: Style.font.caption
// Callers bind `text` from outside this file, so the default has to be set
// here. AutoText would let a section title that happens to carry a device or
// network name promote itself to rich text.
textFormat: Text.PlainText
color: Qt.darker(foreground, 1.4) color: Qt.darker(foreground, 1.4)
font.family: fontFamily font.family: fontFamily
font.pixelSize: fontSize font.pixelSize: fontSize
+1
View File
@@ -36,6 +36,7 @@ ToolTip {
} }
contentItem: Text { contentItem: Text {
textFormat: Text.PlainText
text: root.text text: root.text
color: root.panelForeground color: root.panelForeground
font.family: root.fontFamily font.family: root.fontFamily
+5
View File
@@ -93,6 +93,7 @@ Item {
spacing: Style.spacing.labelGap spacing: Style.spacing.labelGap
Text { Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== "" visible: root.showLabel && root.label !== ""
text: root.label text: root.label
color: Qt.darker(root.foreground, 1.4) color: Qt.darker(root.foreground, 1.4)
@@ -132,6 +133,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: chevron.left anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -246,6 +248,7 @@ Item {
height: popup.height - searchHeader.height - Style.spacing.xxs - 1 height: popup.height - searchHeader.height - Style.spacing.xxs - 1
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
visible: resultList.count === 0 visible: resultList.count === 0
text: root.emptyText text: root.emptyText
@@ -313,6 +316,7 @@ Item {
spacing: Style.spacing.xxs spacing: Style.spacing.xxs
Text { Text {
textFormat: Text.PlainText
text: root.optionLabel(modelData) text: root.optionLabel(modelData)
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -321,6 +325,7 @@ Item {
width: parent.width width: parent.width
} }
Text { Text {
textFormat: Text.PlainText
visible: text !== "" visible: text !== ""
text: root.optionDescription(modelData) text: root.optionDescription(modelData)
color: Qt.darker(root.foreground, 1.5) color: Qt.darker(root.foreground, 1.5)
+5
View File
@@ -130,6 +130,7 @@ PanelWindow {
spacing: Style.space(16) spacing: Style.space(16)
Text { Text {
textFormat: Text.PlainText
visible: root.title !== "" visible: root.title !== ""
text: root.title.toUpperCase() text: root.title.toUpperCase()
color: root.onScrimDim color: root.onScrimDim
@@ -182,6 +183,7 @@ PanelWindow {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.failed visible: root.failed
text: root.error text: root.error
color: root.onScrimUrgent color: root.onScrimUrgent
@@ -368,6 +370,7 @@ PanelWindow {
spacing: 0 spacing: 0
Text { Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
// Both branches go through the locale: a reading is a measurement, so // Both branches go through the locale: a reading is a measurement, so
// its separators follow the system's number conventions rather than the // its separators follow the system's number conventions rather than the
@@ -383,6 +386,7 @@ PanelWindow {
} }
Text { Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: root.unit text: root.unit
color: root.onScrimDim color: root.onScrimDim
@@ -394,6 +398,7 @@ PanelWindow {
// The 90° gap at the bottom of the scale is where a cluster prints its // The 90° gap at the bottom of the scale is where a cluster prints its
// unit; here it names the direction. // unit; here it names the direction.
Text { Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
text: dial.label text: dial.label
+2
View File
@@ -69,6 +69,7 @@ BorderSurface {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Text { Text {
textFormat: Text.PlainText
text: root.label text: root.label
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -79,6 +80,7 @@ BorderSurface {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.description !== "" visible: root.description !== ""
text: root.description text: root.description
color: Qt.darker(root.foreground, 1.5) color: Qt.darker(root.foreground, 1.5)
+1
View File
@@ -74,6 +74,7 @@ Item {
Text { Text {
id: label id: label
textFormat: Text.PlainText
visible: root.labelVisible visible: root.labelVisible
anchors.centerIn: parent anchors.centerIn: parent
text: root.text text: root.text
+12
View File
@@ -434,6 +434,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
visible: heroMarkImage.status !== Image.Ready visible: heroMarkImage.status !== Image.Ready
text: button.text text: button.text
@@ -504,6 +505,7 @@ Panel {
Text { Text {
id: statusText id: statusText
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -558,6 +560,7 @@ Panel {
Text { Text {
id: balanceValue id: balanceValue
textFormat: Text.PlainText
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : "" text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
color: root.balanceAlarming ? root.urgent : root.foreground color: root.balanceAlarming ? root.urgent : root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -575,6 +578,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
visible: text !== "" visible: text !== ""
width: parent.width width: parent.width
text: root.balanceDetailText(root.balance) text: root.balanceDetailText(root.balance)
@@ -680,6 +684,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
visible: text !== "" visible: text !== ""
width: parent.width width: parent.width
topPadding: Style.space(2) topPadding: Style.space(2)
@@ -710,6 +715,7 @@ Panel {
Text { Text {
id: limitLabel id: limitLabel
textFormat: Text.PlainText
// A model-scoped window is titled after its model, and those names run // A model-scoped window is titled after its model, and those names run
// long enough to reach the percentage, so the title gives way first. // long enough to reach the percentage, so the title gives way first.
text: limitRow.window ? limitRow.window.title : "" text: limitRow.window ? limitRow.window.title : ""
@@ -725,6 +731,7 @@ Panel {
Text { Text {
id: limitValue id: limitValue
textFormat: Text.PlainText
text: limitRow.window && limitRow.window.percent >= 0 text: limitRow.window && limitRow.window.percent >= 0
? Math.round(limitRow.window.percent * 100) + "%" ? Math.round(limitRow.window.percent * 100) + "%"
: "—" : "—"
@@ -744,6 +751,7 @@ Panel {
Text { Text {
id: resetText id: resetText
textFormat: Text.PlainText
width: parent.width width: parent.width
text: { text: {
var remainingMs = root.resetMsFor(limitRow.window) var remainingMs = root.resetMsFor(limitRow.window)
@@ -798,6 +806,7 @@ Panel {
Text { Text {
id: dayLabel id: dayLabel
textFormat: Text.PlainText
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today) text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
color: dayRow.today ? root.foreground : root.dim color: dayRow.today ? root.foreground : root.dim
font.family: root.fontFamily font.family: root.fontFamily
@@ -835,6 +844,7 @@ Panel {
Text { Text {
id: dayValue id: dayValue
textFormat: Text.PlainText
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0) text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
color: dayRow.today ? root.foreground : root.dim color: dayRow.today ? root.foreground : root.dim
font.family: root.fontFamily font.family: root.fontFamily
@@ -890,6 +900,7 @@ Panel {
Text { Text {
id: modelName id: modelName
textFormat: Text.PlainText
text: modelRow.row ? modelRow.row.name : "" text: modelRow.row ? modelRow.row.name : ""
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -904,6 +915,7 @@ Panel {
Text { Text {
id: modelTokens id: modelTokens
textFormat: Text.PlainText
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : "" text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
color: root.dim color: root.dim
font.family: root.fontFamily font.family: root.fontFamily
+16
View File
@@ -948,6 +948,21 @@ Item {
onFileChanged: barHiddenProbe.running = true onFileChanged: barHiddenProbe.running = true
} }
// The directory watch can permanently stop delivering events after flag
// changes land in quick succession, stranding the bar off screen until the
// shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag
// so the probe re-reads it even when the watch has gone quiet.
IpcHandler {
target: "omarchy.bar"
// Start rather than restart: a probe already in flight was launched by the
// directory watch after the flag flipped, so its answer is current, and
// killing it here can swallow the result entirely.
function syncHidden(): void {
barHiddenProbe.running = true
}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
@@ -1090,6 +1105,7 @@ Item {
Text { Text {
id: tooltipLabel id: tooltipLabel
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: root.tooltipText text: root.tooltipText
color: Color.tooltip.text color: Color.tooltip.text
@@ -29,6 +29,7 @@ BarWidget {
Text { Text {
id: labelText id: labelText
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
width: parent.width width: parent.width
+4
View File
@@ -467,6 +467,7 @@ BarWidget {
} }
Text { Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: rowIcon.right anchors.left: rowIcon.right
anchors.leftMargin: Style.space(10) anchors.leftMargin: Style.space(10)
@@ -577,6 +578,7 @@ BarWidget {
} }
Text { Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Style.space(28) anchors.leftMargin: Style.space(28)
@@ -681,6 +683,7 @@ BarWidget {
} }
Text { Text {
textFormat: Text.PlainText
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
@@ -709,6 +712,7 @@ BarWidget {
} }
Text { Text {
textFormat: Text.PlainText
visible: !menuRow.modelData.isSeparator visible: !menuRow.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
+4
View File
@@ -432,6 +432,7 @@ Item {
color: "transparent" color: "transparent"
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -500,6 +501,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0) width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
height: parent.height height: parent.height
text: parent.parent.previewText text: parent.parent.previewText
@@ -546,6 +548,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
visible: parent.activeRow && !parent.activeRow.previewImage visible: parent.activeRow && !parent.activeRow.previewImage
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: root.contentMargin anchors.leftMargin: root.contentMargin
@@ -593,6 +596,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”" text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
color: root.foreground color: root.foreground
opacity: 0.7 opacity: 0.7
@@ -519,12 +519,14 @@ Item {
width: Style.space(140) width: Style.space(140)
spacing: Style.space(1) spacing: Style.space(1)
Text { Text {
textFormat: Text.PlainText
text: "Style.font." + modelData.key text: "Style.font." + modelData.key
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: modelData.size + " px" text: modelData.size + " px"
color: Qt.darker(root.foreground, 1.5) color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily font.family: root.fontFamily
@@ -534,6 +536,7 @@ Item {
Text { Text {
id: sampleText id: sampleText
textFormat: Text.PlainText
anchors.left: metaCol.right anchors.left: metaCol.right
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -574,6 +577,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.font.family text: Style.font.family
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -587,6 +591,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.font.resolvedFamily text: Style.font.resolvedFamily
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -600,6 +605,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.font.baseSize + " px" text: Style.font.baseSize + " px"
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -613,6 +619,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.bar.sizeHorizontal + " px" text: Style.bar.sizeHorizontal + " px"
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -626,6 +633,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.bar.sizeVertical + " px" text: Style.bar.sizeVertical + " px"
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -639,6 +647,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.spacing.scale.toFixed(2) text: Style.spacing.scale.toFixed(2)
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -652,6 +661,7 @@ Item {
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
} }
Text { Text {
textFormat: Text.PlainText
text: Style.spacing.panelPadding + " px" text: Style.spacing.panelPadding + " px"
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -818,6 +828,7 @@ Item {
Text { Text {
id: csLabel id: csLabel
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -1273,6 +1284,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%" text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
+3
View File
@@ -247,6 +247,7 @@ Item {
color: "transparent" color: "transparent"
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -284,6 +285,7 @@ Item {
color: hasCursor ? root.selectedBackground : "transparent" color: hasCursor ? root.selectedBackground : "transparent"
Text { Text {
textFormat: Text.PlainText
text: parent.emoji text: parent.emoji
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: Style.font.display font.pixelSize: Style.font.display
@@ -326,6 +328,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
text: "No matches for “" + root.filterText + "”" text: "No matches for “" + root.filterText + "”"
color: root.foreground color: root.foreground
opacity: 0.7 opacity: 0.7
@@ -545,6 +545,7 @@ Item {
Text { Text {
id: selectedLabel id: selectedLabel
textFormat: Text.PlainText
visible: root.showLabels visible: root.showLabels
anchors.top: carousel.bottom anchors.top: carousel.bottom
anchors.topMargin: Style.space(16) anchors.topMargin: Style.space(16)
@@ -561,6 +562,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.filterable && root.filterText visible: root.filterable && root.filterText
anchors.top: selectedLabel.bottom anchors.top: selectedLabel.bottom
anchors.topMargin: Style.space(8) anchors.topMargin: Style.space(8)
+1
View File
@@ -184,6 +184,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
anchors.fill: passwordInput anchors.fill: passwordInput
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText) text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
visible: passwordInput.text.length === 0 visible: passwordInput.text.length === 0
+7
View File
@@ -1151,6 +1151,7 @@ Item {
color: "transparent" color: "transparent"
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -1235,6 +1236,7 @@ Item {
Text { Text {
id: iconText id: iconText
textFormat: Text.PlainText
visible: row.hasIcon && !row.isApp visible: row.hasIcon && !row.isApp
text: row.icon text: row.icon
color: row.hasCursor ? root.selectedText : root.foreground color: row.hasCursor ? root.selectedText : root.foreground
@@ -1276,6 +1278,7 @@ Item {
Text { Text {
id: labelText id: labelText
textFormat: Text.PlainText
width: parent.width width: parent.width
text: row.label text: row.label
color: row.hasCursor ? root.selectedText : root.foreground color: row.hasCursor ? root.selectedText : root.foreground
@@ -1286,6 +1289,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
width: parent.width width: parent.width
text: row.detail text: row.detail
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0 visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
@@ -1306,6 +1310,7 @@ Item {
spacing: 0 spacing: 0
Text { Text {
textFormat: Text.PlainText
visible: false visible: false
text: row.childCount text: row.childCount
color: root.foreground color: root.foreground
@@ -1316,6 +1321,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
text: row.kind === "menu" || row.kind === "link" ? "›" : "" text: row.kind === "menu" || row.kind === "link" ? "›" : ""
color: row.hasCursor ? root.selectedText : root.foreground color: row.hasCursor ? root.selectedText : root.foreground
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0 opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
@@ -1399,6 +1405,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet" text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
color: root.foreground color: root.foreground
opacity: 0.7 opacity: 0.7
@@ -5,8 +5,92 @@ function isChromiumDerived(app, appIcon) {
source.indexOf("opera") >= 0 source.indexOf("opera") >= 0
} }
// True when a `<...>` run is an image tag, so the name is read the way Qt's
// parser reads it: after the `<`, the leading run of letters and digits.
//
// Skip everything up to that run rather than matching the separator, because
// there is no JavaScript expression for what Qt skips. QQuickStyledText calls
// skipSpace(), which is QChar::isSpace(), and that set is not `\s`: Qt counts
// U+0085 NEL and `\s` does not, while `\s` counts U+FEFF and Qt does not. A
// name read with `\s` therefore misses a tag written as `<`, U+0085, `img`:
// Qt skips the NEL, reads `img` and issues the GET, while the regex finds no
// name at all and the tag is kept. Measured against Qt 6.11.2.
//
// Over-skipping is the safe direction. It can only classify more runs as
// images, and dropping a run never manufactures a tag: a dropped run joins two
// stretches of text that each contain no `<`.
function isImageTag(tag) {
var name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
return !!name && name[1].toLowerCase() === "img"
}
// The body renders as StyledText so notifications can use the markup the
// body-markup capability advertises (see Service.qml). StyledText honours
// <img src>, and a remote src makes the shell issue an unauthenticated GET
// with no user action, so image tags go before the renderer sees them.
//
// Work in whole tags, never in substrings of one. A `<` opens a tag that runs
// to the next `>`, nested `<` and all, and only a tag whose own name is `img`
// is dropped.
//
// That is the conservative bound, not Qt's exact one: Qt lets a `>` inside a
// quoted attribute value pass without closing the tag, so a Qt tag can be
// longer than the run taken here. Do not "correct" this to match Qt. Taking
// the shorter run only ever splits one Qt tag into several, and a split can
// only expose an `<img` to be dropped, never hide one — whereas honouring
// quotes would let `<b title="a>b"><img src="http://host/x.png">` through.
//
// Deleting a substring is what makes a naive `/<img[^>]*>/g` unsafe. Given
//
// <im<img src="http://a/decoy.png">g src="http://a/beacon.png">
//
// Qt reads ONE malformed tag named `im` and renders nothing, but removing the
// inner match closes the surviving halves up into `<img src=".../beacon.png">`
// — a live tag the input never contained. The stripper would be manufacturing
// the very thing it exists to remove.
//
// Because every `<` opens a tag, the text between tags never contains one, so
// dropping a tag cannot splice its neighbours into a new one. That makes a
// single pass sufficient, with no re-scanning and no input bound to police.
function stripImageTags(text) {
var out = ""
var i = 0
while (i < text.length) {
var open = text.indexOf("<", i)
if (open === -1) {
out += text.slice(i)
break
}
out += text.slice(i, open)
// An unterminated tag at the end of the string still reaches the renderer,
// which closes it itself, so treat the remainder as one tag.
var close = text.indexOf(">", open)
var tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
if (!isImageTag(tag)) out += tag
i = close === -1 ? text.length : close + 1
}
return out
}
// What the card renders, and the last thing to touch the string before Qt parses
// it. The newline rewrite belongs here rather than in the card because it inserts
// `<br/>` into text stripImageTags chose to KEEP, and a kept tag may hold a `<` of
// its own: `<x`, newline, `<img src="http://…">` is one tag named `x` to both the
// stripper and Qt, until the rewrite splits it into `<x<br/>` and a live image tag
// the input never contained. Measured against Qt 6.11.2 — the rewritten form
// fetches, the original does not. So strip again after, and what Qt parses is what
// was checked last.
function styledBody(body, app, appIcon) {
return stripImageTags(sanitizeBody(body, app, appIcon).replace(/\r\n|\r|\n/g, "<br/>"))
}
function sanitizeBody(body, app, appIcon) { function sanitizeBody(body, app, appIcon) {
var text = String(body || "").replace(/<img[^>]*>/gi, "") var text = stripImageTags(String(body || ""))
if (!isChromiumDerived(app, appIcon)) return text if (!isChromiumDerived(app, appIcon)) return text
return text return text
@@ -366,6 +450,7 @@ if (typeof module !== "undefined") {
module.exports = { module.exports = {
isChromiumDerived: isChromiumDerived, isChromiumDerived: isChromiumDerived,
sanitizeBody: sanitizeBody, sanitizeBody: sanitizeBody,
styledBody: styledBody,
summaryStartsWithGlyph: summaryStartsWithGlyph, summaryStartsWithGlyph: summaryStartsWithGlyph,
shouldBypassDnd: shouldBypassDnd, shouldBypassDnd: shouldBypassDnd,
isEphemeralApp: isEphemeralApp, isEphemeralApp: isEphemeralApp,
@@ -44,7 +44,7 @@ BorderSurface {
readonly property bool singleLineToast: sanitizedBody.length === 0 readonly property bool singleLineToast: sanitizedBody.length === 0
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
readonly property string sanitizedBody: sanitizeBody(body) readonly property string sanitizedBody: sanitizeBody(body)
readonly property string styledBody: sanitizedBody.replace(/\r\n|\r|\n/g, "<br/>") readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon)
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4) readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15) readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
@@ -133,6 +133,7 @@ BorderSurface {
// Glyph fallback (Nerd Font character) when no image icon is // Glyph fallback (Nerd Font character) when no image icon is
// available. Used by omarchy-notification-send's `-g` flag. // available. Used by omarchy-notification-send's `-g` flag.
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
visible: root.hasGlyph && smallIconImage.status !== Image.Ready visible: root.hasGlyph && smallIconImage.status !== Image.Ready
text: root.glyph text: root.glyph
@@ -143,6 +144,7 @@ BorderSurface {
} }
Text { Text {
textFormat: Text.PlainText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: root.compactGlyph visible: root.compactGlyph
text: root.glyph text: root.glyph
@@ -157,6 +159,11 @@ BorderSurface {
spacing: Style.space(2) spacing: Style.space(2)
Text { Text {
// The spec defines the summary as a single line of plain text, so
// AutoText could only ever promote a hostile string to rich text.
// The body below is StyledText on purpose — see Service.qml's
// bodyMarkupSupported — and is stripped in NotificationLogic.
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
visible: root.summary.length > 0 visible: root.summary.length > 0
text: root.summary text: root.summary
+2
View File
@@ -159,6 +159,7 @@ Item {
width: root.iconWidth width: root.iconWidth
height: parent.height height: parent.height
Text { Text {
textFormat: Text.PlainText
// Sit the glyph's ink flush in the column, centered when the // Sit the glyph's ink flush in the column, centered when the
// column is wider than this particular glyph. // column is wider than this particular glyph.
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x) x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
@@ -186,6 +187,7 @@ Item {
} }
} }
Text { Text {
textFormat: Text.PlainText
visible: root.message !== "" visible: root.message !== ""
width: root.hasProgress ? root.valueWidth : root.messageWidth width: root.hasProgress ? root.valueWidth : root.messageWidth
// The readout hugs the card edge so a short percentage doesn't leave // The readout hugs the card edge so a short percentage doesn't leave
+11
View File
@@ -711,6 +711,7 @@ Panel {
// Status only — the switch owns muting, mouse and keyboard alike. // Status only — the switch owns muting, mouse and keyboard alike.
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
text: root.outputIcon() text: root.outputIcon()
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -761,6 +762,7 @@ Panel {
Text { Text {
id: heroLabel id: heroLabel
textFormat: Text.PlainText
text: root.outputVolumeName( text: root.outputVolumeName(
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume, outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
root.outputMuted root.outputMuted
@@ -800,6 +802,7 @@ Panel {
Text { Text {
id: outputPercent id: outputPercent
textFormat: Text.PlainText
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%" text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -886,6 +889,7 @@ Panel {
Text { Text {
id: microphonePercent id: microphonePercent
textFormat: Text.PlainText
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%" text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1030,6 +1034,7 @@ Panel {
spacing: Style.space(8) spacing: Style.space(8)
Text { Text {
textFormat: Text.PlainText
text: root.sinkGlyph(sinkRow.node) text: root.sinkGlyph(sinkRow.node)
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1040,6 +1045,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: root.nodeLabel(sinkRow.node) text: root.nodeLabel(sinkRow.node)
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1089,6 +1095,7 @@ Panel {
spacing: Style.space(8) spacing: Style.space(8)
Text { Text {
textFormat: Text.PlainText
text: root.sourceGlyph(sourceRow.node) text: root.sourceGlyph(sourceRow.node)
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1099,6 +1106,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: root.nodeLabel(sourceRow.node) text: root.nodeLabel(sourceRow.node)
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1159,6 +1167,7 @@ Panel {
Text { Text {
id: streamMuteIcon id: streamMuteIcon
textFormat: Text.PlainText
text: streamRow.streamMuted ? "󰝟" : "󰕾" text: streamRow.streamMuted ? "󰝟" : "󰕾"
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1179,6 +1188,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: root.streamLabel(streamRow.node) text: root.streamLabel(streamRow.node)
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1191,6 +1201,7 @@ Panel {
Text { Text {
id: streamPct id: streamPct
textFormat: Text.PlainText
text: Math.round(streamRow.streamVolume * 100) + "%" text: Math.round(streamRow.streamVolume * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.5) color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
+6
View File
@@ -698,6 +698,7 @@ Panel {
// Status only — the switch owns toggling, mouse and keyboard alike. // Status only — the switch owns toggling, mouse and keyboard alike.
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.icon text: root.icon
@@ -748,6 +749,7 @@ Panel {
Text { Text {
id: heroStatus id: heroStatus
textFormat: Text.PlainText
text: root.heroStatusText.toUpperCase() text: root.heroStatusText.toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -863,6 +865,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0 visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
text: !root.adapter ? "No Bluetooth adapter" text: !root.adapter ? "No Bluetooth adapter"
: !root.adapter.enabled ? "Turn Bluetooth on to scan" : !root.adapter.enabled ? "Turn Bluetooth on to scan"
@@ -971,6 +974,7 @@ Panel {
Text { Text {
id: deviceIcon id: deviceIcon
textFormat: Text.PlainText
text: row.isConnected ? "󰂱" : "󰂯" text: row.isConnected ? "󰂱" : "󰂯"
color: row.statusColor color: row.statusColor
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -989,6 +993,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Text { Text {
textFormat: Text.PlainText
text: root.deviceLabel(row.dev) || "Device" text: root.deviceLabel(row.dev) || "Device"
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -997,6 +1002,7 @@ Panel {
width: parent.width width: parent.width
} }
Text { Text {
textFormat: Text.PlainText
visible: row.statusText !== "" visible: row.statusText !== ""
text: row.statusText text: row.statusText
color: row.statusColor color: row.statusColor
+8
View File
@@ -311,6 +311,7 @@ Panel {
Text { Text {
id: heroDate id: heroDate
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDate(root.today, "MMMM d") text: Qt.formatDate(root.today, "MMMM d")
color: heroMouse.containsMouse color: heroMouse.containsMouse
@@ -413,6 +414,7 @@ Panel {
Text { Text {
id: yearLabel id: yearLabel
textFormat: Text.PlainText
visible: !root.editingLife visible: !root.editingLife
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -425,6 +427,7 @@ Panel {
Text { Text {
id: yearPercent id: yearPercent
textFormat: Text.PlainText
visible: !root.editingLife visible: !root.editingLife
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -485,6 +488,7 @@ Panel {
Text { Text {
id: lifePercent id: lifePercent
textFormat: Text.PlainText
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.lifeDonePercent + "%" text: root.lifeDonePercent + "%"
@@ -608,6 +612,7 @@ Panel {
model: root.weekdays model: root.weekdays
Text { Text {
textFormat: Text.PlainText
required property var modelData required property var modelData
width: root.cellWidth width: root.cellWidth
height: Style.space(16) height: Style.space(16)
@@ -631,6 +636,7 @@ Panel {
spacing: root.cellSpacing spacing: root.cellSpacing
Text { Text {
textFormat: Text.PlainText
width: root.weekColumnWidth width: root.weekColumnWidth
height: root.cellHeight height: root.cellHeight
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -662,6 +668,7 @@ Panel {
border.color: Style.normalBorderFor(root.contentForeground, Color.accent) border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: modelData.day text: modelData.day
color: modelData.inMonth color: modelData.inMonth
@@ -707,6 +714,7 @@ Panel {
Text { Text {
id: monthLabel id: monthLabel
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Fixed width so the chevrons hold still between a // Fixed width so the chevrons hold still between a
+8
View File
@@ -281,6 +281,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
visible: dropbox.actionStatus !== "" || dropbox.lastError !== "" visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
width: parent.width width: parent.width
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
@@ -421,6 +422,7 @@ Panel {
spacing: Style.space(1) spacing: Style.space(1)
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed" text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
color: root.foreground color: root.foreground
@@ -430,6 +432,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu" text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
color: root.dim color: root.dim
@@ -478,6 +481,7 @@ Panel {
spacing: Style.space(8) spacing: Style.space(8)
Text { Text {
textFormat: Text.PlainText
text: Model.fileGlyph(fileRow.fileName) text: Model.fileGlyph(fileRow.fileName)
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -491,6 +495,7 @@ Panel {
spacing: Style.space(1) spacing: Style.space(1)
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: fileRow.fileName text: fileRow.fileName
color: root.foreground color: root.foreground
@@ -500,6 +505,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: Model.fileMeta(fileRow.file) text: Model.fileMeta(fileRow.file)
color: root.dim color: root.dim
@@ -524,6 +530,7 @@ Panel {
} }
component InfoLabel: Text { component InfoLabel: Text {
textFormat: Text.PlainText
color: root.foreground color: root.foreground
opacity: 0.6 opacity: 0.6
font.family: root.fontFamily font.family: root.fontFamily
@@ -531,6 +538,7 @@ Panel {
} }
component InfoValue: Text { component InfoValue: Text {
textFormat: Text.PlainText
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
+7
View File
@@ -531,6 +531,7 @@ Panel {
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
text: root.displays.length > 1 ? "󰍺" : "󰍹" text: root.displays.length > 1 ? "󰍺" : "󰍹"
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -559,6 +560,7 @@ Panel {
Text { Text {
id: heroLabel id: heroLabel
textFormat: Text.PlainText
text: { text: {
if (root.brightnessAvailable) { if (root.brightnessAvailable) {
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase() return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
@@ -602,6 +604,7 @@ Panel {
Text { Text {
id: brightnessPercent id: brightnessPercent
textFormat: Text.PlainText
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%" text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -674,6 +677,7 @@ Panel {
Text { Text {
id: textSizePx id: textSizePx
textFormat: Text.PlainText
text: (textSizeSlider.dragging text: (textSizeSlider.dragging
? root.textSizeStops[Math.round(textSizeSlider.liveValue)] ? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
: root.displayedTextPx()) + "px" : root.displayedTextPx()) + "px"
@@ -747,6 +751,7 @@ Panel {
// focused one. // focused one.
Text { Text {
id: scaleMonitor id: scaleMonitor
textFormat: Text.PlainText
text: root.focusedMonitor text: root.focusedMonitor
// Only worth naming when more than one display is in play. // Only worth naming when more than one display is in play.
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1 visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
@@ -887,6 +892,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "") text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -897,6 +903,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: monitorRow.display.enabled ? "󰄬" : "" text: monitorRow.display.enabled ? "󰄬" : ""
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
+10
View File
@@ -1090,6 +1090,7 @@ Panel {
// Status only — the switch owns toggling, mouse and keyboard alike. // Status only — the switch owns toggling, mouse and keyboard alike.
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
text: root.icon text: root.icon
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1170,6 +1171,7 @@ Panel {
// rather than in a pill, which crowded the on/off switch. // rather than in a pill, which crowded the on/off switch.
Text { Text {
id: heroSsid id: heroSsid
textFormat: Text.PlainText
width: parent.width width: parent.width
readonly property string title: { readonly property string title: {
@@ -1189,6 +1191,7 @@ Panel {
Text { Text {
id: heroMeta id: heroMeta
textFormat: Text.PlainText
width: parent.width width: parent.width
text: { text: {
if (root.info.type === "wifi") { if (root.info.type === "wifi") {
@@ -1711,6 +1714,7 @@ Panel {
Text { Text {
id: networkIcon id: networkIcon
textFormat: Text.PlainText
text: row.net ? root.wifiIconFor(row.net.signal) : "" text: row.net ? root.wifiIconFor(row.net.signal) : ""
color: row.statusColor color: row.statusColor
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1732,6 +1736,7 @@ Panel {
Text { Text {
id: lockIndicator id: lockIndicator
textFormat: Text.PlainText
visible: row.requiresCredentials || row.forgetVisible visible: row.requiresCredentials || row.forgetVisible
width: parent.width width: parent.width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -1779,6 +1784,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Text { Text {
textFormat: Text.PlainText
text: row.net ? (row.net.ssid || "Hidden") : "" text: row.net ? (row.net.ssid || "Hidden") : ""
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1787,6 +1793,7 @@ Panel {
width: parent.width width: parent.width
} }
Text { Text {
textFormat: Text.PlainText
// Signal strength is conveyed by the wifi-bars icon and the // Signal strength is conveyed by the wifi-bars icon and the
// right-edge glyph/buttons carry protection or forget affordances, // right-edge glyph/buttons carry protection or forget affordances,
// so the second line only carries action status (Connecting…, // so the second line only carries action status (Connecting…,
@@ -1893,6 +1900,7 @@ Panel {
radius: Style.cornerRadius radius: Style.cornerRadius
Text { Text {
textFormat: Text.PlainText
anchors.fill: parent anchors.fill: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -1946,6 +1954,7 @@ Panel {
} }
component InfoLabel: Text { component InfoLabel: Text {
textFormat: Text.PlainText
color: root.bar.foreground color: root.bar.foreground
opacity: 0.6 opacity: 0.6
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -1953,6 +1962,7 @@ Panel {
} }
component InfoValue: Text { component InfoValue: Text {
textFormat: Text.PlainText
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
+5
View File
@@ -325,6 +325,7 @@ Panel {
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
text: root.batteryIcon() text: root.batteryIcon()
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -356,6 +357,7 @@ Panel {
Text { Text {
id: heroStatus id: heroStatus
textFormat: Text.PlainText
text: root.heroStatusText.toUpperCase() text: root.heroStatusText.toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -369,6 +371,7 @@ Panel {
Text { Text {
id: heroPercent id: heroPercent
textFormat: Text.PlainText
text: root.batteryInfo.percentage || "—" text: root.batteryInfo.percentage || "—"
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -517,6 +520,7 @@ Panel {
} }
component InfoLabel: Text { component InfoLabel: Text {
textFormat: Text.PlainText
color: root.bar.foreground color: root.bar.foreground
opacity: 0.6 opacity: 0.6
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -524,6 +528,7 @@ Panel {
} }
component InfoValue: Text { component InfoValue: Text {
textFormat: Text.PlainText
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall font.pixelSize: Style.font.bodySmall
+10
View File
@@ -498,6 +498,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
visible: tailscale.actionStatus !== "" || tailscale.lastError !== "" visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
width: parent.width width: parent.width
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
@@ -841,6 +842,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: accountRow.accountText text: accountRow.accountText
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -933,6 +935,7 @@ Panel {
spacing: Style.space(8) spacing: Style.space(8)
Text { Text {
textFormat: Text.PlainText
text: tailscale.osIcon(peer ? peer.OS : "") text: tailscale.osIcon(peer ? peer.OS : "")
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -946,6 +949,7 @@ Panel {
spacing: Style.space(1) spacing: Style.space(1)
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: peerRow.peerName text: peerRow.peerName
color: root.foreground color: root.foreground
@@ -955,6 +959,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: { text: {
var parts = [] var parts = []
@@ -1087,6 +1092,7 @@ Panel {
spacing: Style.space(10) spacing: Style.space(10)
Text { Text {
textFormat: Text.PlainText
Layout.fillWidth: true Layout.fillWidth: true
text: copyChoice.label text: copyChoice.label
color: root.foreground color: root.foreground
@@ -1134,6 +1140,7 @@ Panel {
Text { Text {
id: exitNodeGlyph id: exitNodeGlyph
textFormat: Text.PlainText
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "󰖂" : "󱇢") text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "󰖂" : "󱇢")
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
font.family: root.fontFamily font.family: root.fontFamily
@@ -1154,6 +1161,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
text: exitNodeRow.peerName text: exitNodeRow.peerName
color: root.foreground color: root.foreground
font.family: root.fontFamily font.family: root.fontFamily
@@ -1224,6 +1232,7 @@ Panel {
spacing: Style.space(1) spacing: Style.space(1)
Text { Text {
textFormat: Text.PlainText
width: parent.width width: parent.width
text: regionRow.regionName text: regionRow.regionName
color: root.foreground color: root.foreground
@@ -1234,6 +1243,7 @@ Panel {
} }
Text { Text {
textFormat: Text.PlainText
width: parent.width width: parent.width
text: regionRow.regionDetail text: regionRow.regionDetail
visible: text !== "" visible: text !== ""
+14
View File
@@ -531,6 +531,7 @@ Panel {
Text { Text {
id: heroIcon id: heroIcon
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 5 anchors.verticalCenterOffset: 5
text: root.label || "—" text: root.label || "—"
@@ -547,6 +548,7 @@ Panel {
Text { Text {
id: tempBig id: tempBig
textFormat: Text.PlainText
text: root.reportTempNum || "—" text: root.reportTempNum || "—"
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -556,6 +558,7 @@ Panel {
font.bold: true font.bold: true
} }
Text { Text {
textFormat: Text.PlainText
text: root.current ? root.tempUnit : "" text: root.current ? root.tempUnit : ""
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -593,6 +596,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
textFormat: Text.PlainText
text: (root.reportLocation || "").toUpperCase() text: (root.reportLocation || "").toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -643,6 +647,7 @@ Panel {
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent" color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
Text { Text {
textFormat: Text.PlainText
anchors.centerIn: parent anchors.centerIn: parent
text: root.savingLocation ? "󰦖" : "✕" text: root.savingLocation ? "󰦖" : "✕"
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -683,6 +688,7 @@ Panel {
font.letterSpacing: 1 font.letterSpacing: 1
} }
Text { Text {
textFormat: Text.PlainText
text: root.reportFeels text: root.reportFeels
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -700,6 +706,7 @@ Panel {
font.letterSpacing: 1 font.letterSpacing: 1
} }
Text { Text {
textFormat: Text.PlainText
text: root.reportWind text: root.reportWind
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -717,6 +724,7 @@ Panel {
font.letterSpacing: 1 font.letterSpacing: 1
} }
Text { Text {
textFormat: Text.PlainText
text: root.reportHumidity text: root.reportHumidity
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -752,12 +760,14 @@ Panel {
spacing: Style.space(8) spacing: Style.space(8)
Text { Text {
textFormat: Text.PlainText
text: modelData.name text: modelData.name
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
font.pixelSize: Style.font.body font.pixelSize: Style.font.body
} }
Text { Text {
textFormat: Text.PlainText
visible: text !== "" visible: text !== ""
text: modelData.description text: modelData.description
color: Qt.darker(root.bar.foreground, 1.5) color: Qt.darker(root.bar.foreground, 1.5)
@@ -817,6 +827,7 @@ Panel {
spacing: Style.space(10) spacing: Style.space(10)
Text { Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.dayIcon(modelData) text: root.dayIcon(modelData)
color: root.bar.foreground color: root.bar.foreground
@@ -829,6 +840,7 @@ Panel {
spacing: Style.space(2) spacing: Style.space(2)
Text { Text {
textFormat: Text.PlainText
text: root.dayName(modelData.date).toUpperCase() text: root.dayName(modelData.date).toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4) color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
@@ -840,12 +852,14 @@ Panel {
spacing: Style.space(6) spacing: Style.space(6)
Text { Text {
textFormat: Text.PlainText
text: root.bareTempForDay(modelData, "max") text: root.bareTempForDay(modelData, "max")
color: root.bar.foreground color: root.bar.foreground
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
font.pixelSize: Style.font.body font.pixelSize: Style.font.body
} }
Text { Text {
textFormat: Text.PlainText
text: root.bareTempForDay(modelData, "min") text: root.bareTempForDay(modelData, "min")
color: Qt.darker(root.bar.foreground, 1.5) color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily font.family: root.bar.fontFamily
+3
View File
@@ -257,6 +257,7 @@ Item {
spacing: Style.space(16) spacing: Style.space(16)
Text { Text {
textFormat: Text.PlainText
text: (root.ssid || "Wi-Fi").toUpperCase() text: (root.ssid || "Wi-Fi").toUpperCase()
color: root.onScrimDim color: root.onScrimDim
font.family: root.fontFamily font.family: root.fontFamily
@@ -318,6 +319,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.error !== "" visible: root.error !== ""
text: root.error text: root.error
color: root.onScrimUrgent color: root.onScrimUrgent
@@ -340,6 +342,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
visible: root.showingQr && root.secured visible: root.showingQr && root.secured
text: root.passwordError !== "" ? root.passwordError text: root.passwordError !== "" ? root.passwordError
: root.passwordVisible ? root.password : root.passwordVisible ? root.password
+2
View File
@@ -332,6 +332,7 @@ Item {
} }
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -374,6 +375,7 @@ Item {
Text { Text {
id: justificationText id: justificationText
textFormat: Text.PlainText
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Style.space(12) anchors.leftMargin: Style.space(12)
anchors.rightMargin: Style.space(12) anchors.rightMargin: Style.space(12)
+1
View File
@@ -156,6 +156,7 @@ Item {
anchors.leftMargin: card.contentLeftInset anchors.leftMargin: card.contentLeftInset
Text { Text {
textFormat: Text.PlainText
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff. Show more