Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
346e69e1ce | ||
|
|
0e1ce609d8 | ||
|
|
3cb3b2d20b | ||
|
|
f8e35ec654 | ||
|
|
f802ae107d | ||
|
|
6f8b62f04c | ||
|
|
9aa0545c2a | ||
|
|
ebd6480387 | ||
|
|
07579f5e94 | ||
|
|
ff4cf8afbc | ||
|
|
cf3d69c38d | ||
|
|
47ce81ebca | ||
|
|
2421e76fb8 | ||
|
|
bffc6a2a5d | ||
|
|
7678e5b0d8 | ||
|
|
be730884f6 | ||
|
|
a73a312e1b | ||
|
|
159163bb16 | ||
|
|
521779b114 | ||
|
|
51af7bf794 | ||
|
|
92c8f87730 | ||
|
|
3bb9867245 | ||
|
|
da0fe6d89f | ||
|
|
2645b82bcd | ||
|
|
9c1ec524cd | ||
|
|
880605f22b | ||
|
|
294c38a2f0 | ||
|
|
1082b7864d |
No files matched your search
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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//,/ }
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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,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,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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
Executable
+123
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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"},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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 +0,0 @@
|
|||||||
%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol
|
|
||||||
@@ -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 @@
|
|||||||
%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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
u cups-browsed - "CUPS printer discovery" / -
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
|
browser_policy_setup_dir /etc/chromium/policies/managed
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
as_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 !== ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 !== ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
Reference in new issue
Block a user