Merge quattro into the Chromium first-run EULA branch
Quattro stopped making the Chromium managed-policy directory world-writable while this branch was open, and the block it deleted from the theme install leaf sat directly above the comment this branch rewrites, so the two edits landed in one hunk. The resolution keeps the hardening — the policy directory is set up through install/config/browser-policy.sh now — along with the first-run seed and the comment that names both things the seed does.
This commit is contained in:
@@ -40,6 +40,7 @@ GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
|
||||
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
||||
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
||||
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
||||
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
|
||||
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
||||
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||
|
||||
+3
-1
@@ -92,8 +92,10 @@ omp)
|
||||
;;
|
||||
ori)
|
||||
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
|
||||
# A prompt alone means one headless turn there, printed after the turn ends,
|
||||
# so --interactive is what seeds the session with it and keeps the window.
|
||||
command=(ori code)
|
||||
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
|
||||
;;
|
||||
pi)
|
||||
command=(pi)
|
||||
|
||||
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
|
||||
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
# 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%
|
||||
|
||||
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
|
||||
if [[ ${1:-} == "--no-osd" ]]; then
|
||||
no_osd=1
|
||||
@@ -28,9 +34,14 @@ find_apple_display_device() {
|
||||
local cached=""
|
||||
local device=""
|
||||
|
||||
if [[ -r $device_cache ]]; then
|
||||
if [[ -n $device_cache && -r $device_cache ]]; then
|
||||
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"
|
||||
return 0
|
||||
fi
|
||||
@@ -39,7 +50,9 @@ find_apple_display_device() {
|
||||
device="$(detect_apple_display_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"
|
||||
}
|
||||
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
|
||||
# omarchy:args=[--] [<program>] [on|off|toggle]
|
||||
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
|
||||
|
||||
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
|
||||
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
# Only regular files, because that is all the watcher honours: anything else in
|
||||
# there would be reported as muted while the crashes kept arriving. The dotted
|
||||
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
|
||||
# same -f test that keeps them out.
|
||||
list() {
|
||||
local entry found=0
|
||||
|
||||
for entry in "$MUTES"/* "$MUTES"/.*; do
|
||||
[[ -f $entry ]] || continue
|
||||
printf '%s\n' "${entry##*/}"
|
||||
found=1
|
||||
done
|
||||
|
||||
((found)) || echo "No programs muted. Crashes all notify."
|
||||
}
|
||||
|
||||
# A program may be named -h, and the router answers that with its own help
|
||||
# before this ever runs. `omarchy crash mute -- -h` is the way through.
|
||||
[[ ${1:-} == "--" ]] && shift
|
||||
|
||||
if (($# == 0)); then
|
||||
list
|
||||
exit 0
|
||||
fi
|
||||
|
||||
program=$1
|
||||
action=${2:-on}
|
||||
|
||||
# The watcher keys the mute on the executable's basename, so accept the path it
|
||||
# reports as readily as the name, and reduce either the same way it does.
|
||||
program=${program##*/}
|
||||
|
||||
if [[ -z $program || $program == "." || $program == ".." ]]; then
|
||||
echo "Not a program name: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
on|off|toggle) ;;
|
||||
*)
|
||||
echo "Not an action: $action" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
|
||||
|
||||
# Report what is now true rather than what was asked for: the flag is what the
|
||||
# watcher reads, and a toggle does not say which way it went.
|
||||
if omarchy-toggle-enabled "crash-ignore/$program"; then
|
||||
echo "Muted crash notifications for $program."
|
||||
else
|
||||
echo "Crash notifications for $program are back on."
|
||||
fi
|
||||
+28
-5
@@ -48,12 +48,17 @@ announce() {
|
||||
# -n 0 so a restart does not re-announce crashes already dealt with.
|
||||
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
while IFS= read -r entry; do
|
||||
# A dash for a field that is empty as well as one that is missing: tab is
|
||||
# IFS whitespace, so an empty field collapses into the next delimiter and
|
||||
# every field after it shifts along one. A process can set its own comm to
|
||||
# nothing, and that crash used to be read as somebody else's and dropped.
|
||||
IFS=$'\t' read -r uid comm pid exe signal < <(
|
||||
jq -r '[(._UID // "-"),
|
||||
(.COREDUMP_COMM // "-"),
|
||||
(.COREDUMP_PID // "-"),
|
||||
(.COREDUMP_EXE // "-"),
|
||||
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
||||
jq -r 'def field: if . == null or . == "" then "-" else . end;
|
||||
[(._UID | field),
|
||||
(.COREDUMP_COMM | field),
|
||||
(.COREDUMP_PID | field),
|
||||
(.COREDUMP_EXE | field),
|
||||
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
|
||||
)
|
||||
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
@@ -71,11 +76,29 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
name=$comm
|
||||
[[ $exe == /* ]] && name=${exe##*/}
|
||||
|
||||
# A process can set its own comm to anything prctl takes, slashes included,
|
||||
# and a crash with no recorded executable falls back to it. The mute below
|
||||
# turns this name into a path, so keep it one component: a crash must not
|
||||
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
|
||||
name=${name##*/}
|
||||
|
||||
# What that leaves is not always a name. "/" leaves nothing, which is no
|
||||
# kind of array subscript and no kind of toast; a dot component names a
|
||||
# directory rather than a flag, so a mute on it would touch that directory
|
||||
# and then never match; and a dash is what the read above puts there when
|
||||
# the crash recorded no name at all.
|
||||
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
|
||||
|
||||
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
|
||||
|
||||
# Never announce our own machinery, or it notifies about itself.
|
||||
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
|
||||
|
||||
# Muted at the end of a diagnosis, when the user was offered it and said
|
||||
# yes. A flag per program rather than one list, so omarchy-crash-mute can
|
||||
# lift one without reading, rewriting and re-parsing the rest.
|
||||
omarchy-toggle-enabled "crash-ignore/$name" && continue
|
||||
|
||||
now=$EPOCHSECONDS
|
||||
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
||||
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Match the Dell XPS 13 DX13260 that requires the sidecar amplifier workaround.
|
||||
|
||||
product_sku="${OMARCHY_DMI_PRODUCT_SKU:-/sys/class/dmi/id/product_sku}"
|
||||
|
||||
omarchy-hw-match "DX13260" &&
|
||||
grep -qix "0E53" "$product_sku" 2>/dev/null
|
||||
+11
-17
@@ -6,9 +6,10 @@
|
||||
|
||||
set -e
|
||||
|
||||
setup_policy_directory() {
|
||||
sudo mkdir -p "$1"
|
||||
sudo chmod a+rw "$1"
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
setup_chromium_policy_directory() {
|
||||
browser_policy_setup_dir "$1"
|
||||
}
|
||||
|
||||
announce_browser_installed() {
|
||||
@@ -23,13 +24,6 @@ copy_chromium_flags() {
|
||||
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() {
|
||||
mkdir -p ~/.config/environment.d
|
||||
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
|
||||
@@ -40,7 +34,7 @@ chromium)
|
||||
echo "Installing Chromium..."
|
||||
omarchy-pkg-add chromium
|
||||
|
||||
setup_policy_directory /etc/chromium/policies/managed
|
||||
setup_chromium_policy_directory /etc/chromium/policies/managed
|
||||
copy_chromium_flags ~/.config/chromium-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Chromium"
|
||||
@@ -49,7 +43,7 @@ chrome)
|
||||
echo "Installing Chrome..."
|
||||
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
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Chrome"
|
||||
@@ -58,7 +52,7 @@ edge)
|
||||
echo "Installing Edge..."
|
||||
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
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Edge"
|
||||
@@ -67,7 +61,7 @@ brave)
|
||||
echo "Installing Brave..."
|
||||
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
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave"
|
||||
@@ -76,7 +70,7 @@ brave-origin)
|
||||
echo "Installing Brave Origin..."
|
||||
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
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave Origin"
|
||||
@@ -85,7 +79,7 @@ firefox)
|
||||
echo "Installing Firefox..."
|
||||
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
|
||||
announce_browser_installed "Firefox"
|
||||
;;
|
||||
@@ -93,7 +87,7 @@ zen)
|
||||
echo "Installing Zen..."
|
||||
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
|
||||
announce_browser_installed "Zen"
|
||||
;;
|
||||
|
||||
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
|
||||
sudo systemctl enable --now once-background.service
|
||||
|
||||
echo -e "\nLaunching ONCE..."
|
||||
once
|
||||
sudo once
|
||||
|
||||
@@ -742,6 +742,12 @@ create_user() {
|
||||
# for specific commands), and a duplicate grant is harmless.
|
||||
echo "%wheel ALL=(ALL:ALL) ALL" >/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() {
|
||||
|
||||
@@ -23,16 +23,26 @@ omarchy-git-url-check "$REPO_URL" || exit 1
|
||||
|
||||
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_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_PATH="$THEMES_DIR/$THEME_NAME"
|
||||
|
||||
# The name comes from the URL and is joined into a path that is about to be
|
||||
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading
|
||||
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`.
|
||||
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||
# The name comes from the URL, is joined into a path that is about to be
|
||||
# removed, and then names a directory the rest of Omarchy passes around by
|
||||
# name: Style > Unlock builds a command line out of the one the picker
|
||||
# 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."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -3,23 +3,15 @@
|
||||
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
|
||||
# omarchy:hidden=true
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
|
||||
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
|
||||
|
||||
if [[ -f $CHROMIUM_THEME ]]; then
|
||||
THEME_RGB_COLOR=$(<$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"
|
||||
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
|
||||
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() {
|
||||
local process="$1"
|
||||
local command="$2"
|
||||
@@ -30,17 +22,15 @@ refresh_running_browser() {
|
||||
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
|
||||
|
||||
set_browser_policy /etc/opt/chrome/policies/managed
|
||||
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
|
||||
|
||||
set_browser_policy /etc/brave/policies/managed
|
||||
refresh_running_browser brave brave
|
||||
# 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.
|
||||
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"
|
||||
@@ -1312,7 +1312,21 @@ apply_system_transition() {
|
||||
/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 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
|
||||
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
|
||||
@@ -2306,6 +2320,11 @@ refresh_current_theme_after_upgrade() {
|
||||
# hooks because one of them runs `hyprctl reload`. Still poke terminal
|
||||
# emulators so the active upgrade terminal picks up generated theme files.
|
||||
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
|
||||
|
||||
+98
-16
@@ -13,6 +13,18 @@ safe_icon_name() {
|
||||
| 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() {
|
||||
local ref="$1"
|
||||
local name
|
||||
@@ -42,6 +54,34 @@ download_icon() {
|
||||
[[ -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() {
|
||||
local site_url="$1" dest="$2"
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
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")
|
||||
require_plain_name "$APP_NAME"
|
||||
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$APP_URL")
|
||||
require_http_url "$APP_URL"
|
||||
|
||||
# Try to fetch the site's icon automatically first.
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -88,10 +159,8 @@ if (( $# < 3 )); then
|
||||
INTERACTIVE_MODE=true
|
||||
else
|
||||
APP_NAME="$1"
|
||||
APP_URL="$2"
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$2")
|
||||
require_http_url "$APP_URL"
|
||||
ICON_REF="$3"
|
||||
CUSTOM_EXEC="$4" # Optional custom exec command
|
||||
MIME_TYPES="$5" # Optional mime types
|
||||
@@ -104,6 +173,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_plain_name "$APP_NAME"
|
||||
|
||||
if [[ -z $ICON_REF ]]; then
|
||||
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -128,28 +199,39 @@ else
|
||||
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
|
||||
fi
|
||||
|
||||
# Use custom exec if provided, otherwise default behavior
|
||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||
# Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
|
||||
# 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
|
||||
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
|
||||
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
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
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=$APP_NAME
|
||||
Comment=$APP_NAME
|
||||
Exec=$EXEC_COMMAND
|
||||
Name=$name_field
|
||||
Comment=$name_field
|
||||
Exec=$exec_field
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=$ICON_VALUE
|
||||
Icon=$icon_field
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Add mime types if provided
|
||||
if [[ -n $MIME_TYPES ]]; then
|
||||
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE"
|
||||
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
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"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications/"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
# Find all web apps
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
|
||||
# Always index the launchers, so removal deletes the file that was found rather
|
||||
# than a path rebuilt from the displayed name. Installs predating the name
|
||||
# validation could nest the launcher inside directories, and those are exactly
|
||||
# the ones a reconstructed path cannot reach.
|
||||
WEB_APP_PATHS=()
|
||||
while IFS= read -r -d '' file; do
|
||||
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
|
||||
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)
|
||||
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
|
||||
fi
|
||||
|
||||
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"
|
||||
|
||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||
|
||||
@@ -87,7 +87,38 @@ ambiguous, say so rather than assembling confidence out of guesswork.
|
||||
|
||||
**Leave the system as you found it.** Diagnosis reads; it does not fix, tidy, or
|
||||
reconfigure. The one thing to clean up is your own: delete the core you extracted
|
||||
above, which is a copy of the crashed process's memory.
|
||||
above, which is a copy of the crashed process's memory. The single change a
|
||||
diagnosis may make is the mute below, and only when the user asks for it.
|
||||
|
||||
## Offer to stop the notifications for this program
|
||||
|
||||
A crash you have explained often keeps happening anyway. Finish by offering to
|
||||
silence notifications for **that one program**, and never run it unprompted. Say
|
||||
how to lift it in the same breath, so it is not a one-way door.
|
||||
|
||||
```bash
|
||||
omarchy-crash-mute '<program>' # silence it
|
||||
omarchy-crash-mute '<program>' off # let it speak again
|
||||
omarchy-crash-mute # list what is muted
|
||||
```
|
||||
|
||||
Pass the `binary:` path from the crash facts, or the `process:` name where no
|
||||
binary was recorded; the command reduces either to the name the watcher keys on.
|
||||
A diagnosis run by hand from `omarchy agent crash <pid>` has neither, so take
|
||||
them from `coredumpctl info`. Prefer the binary: a process name is truncated to
|
||||
15 characters and a basename is not, so muting the truncated form matches
|
||||
nothing, forever, while looking like it worked.
|
||||
|
||||
Quote it. The name is whatever the crashed program's author called a file, and a
|
||||
single quote inside one closes yours and runs the rest as your shell.
|
||||
|
||||
The key is a bare name, so anything run through an interpreter is keyed as the
|
||||
interpreter: muting `python3.13` silences every Python program on the machine.
|
||||
Say so rather than quietly doing it.
|
||||
|
||||
None of this fixes anything, and a mute offered in place of a fix that was within
|
||||
reach is the wrong answer. For every program rather than one, the switch is
|
||||
_Trigger > Toggle > Crash Capture_.
|
||||
|
||||
## If it is an Omarchy bug
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
o.window(".*[Rr]esolve.*", {
|
||||
float = true,
|
||||
stay_focused = true,
|
||||
-- Prevent modal dialog pointer warps when focus follows the mouse.
|
||||
no_follow_mouse = true,
|
||||
tag = "-default-opacity",
|
||||
opacity = "1 1",
|
||||
})
|
||||
|
||||
o.window({ class = ".*[Rr]esolve.*", title = "^DaVinci Resolve( Studio)? - .+$" }, { fullscreen = true })
|
||||
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager)$" }, { stay_focused = false })
|
||||
-- Resolve exposes the Voiceover panel under the generic "Dialog" title.
|
||||
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager|Preferences|Find Directory|Dialog)$" }, { stay_focused = false })
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Keep the Windows VM display opaque instead of applying the default window opacity.
|
||||
o.window({ class = "^xfreerdp$", title = "^Windows VM - Omarchy$" }, {
|
||||
tag = "-default-opacity",
|
||||
opacity = "1 1",
|
||||
})
|
||||
@@ -103,7 +103,7 @@
|
||||
// Style
|
||||
"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.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.bar": {"icon":"","label":"Menu Bar"},
|
||||
"style.bar.position": {"icon":"","label":"Position"},
|
||||
|
||||
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
# Repositories for debug symbol packages.
|
||||
|
||||
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/stable/$arch
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Omarchy override of cups-browsed's shipped config. The only behavioural
|
||||
# change vs the upstream default (all-commented) is enabling auto-registration
|
||||
# of remote IPP printers discovered via Avahi/mDNS.
|
||||
CreateRemotePrinters Yes
|
||||
# Keep state away from /var/cache/cups, which is writable by the account CUPS
|
||||
# uses for print filters. cups-browsed is the only writer to this directory.
|
||||
CacheDir /var/cache/cups-browsed
|
||||
|
||||
# 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
|
||||
@@ -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/browser-policy.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.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
|
||||
@@ -6,10 +6,6 @@ ln -snf /usr/share/icons/Adwaita/symbolic/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
|
||||
|
||||
# Chromium policy directory for theme
|
||||
mkdir -p /etc/chromium/policies/managed
|
||||
chmod a+rw /etc/chromium/policies/managed
|
||||
|
||||
# Seed Chromium's first run: follow system appearance ("device") instead of dark,
|
||||
# and skip the terms-of-service dialog Chromium 151 turned on by default.
|
||||
mkdir -p /usr/lib/chromium
|
||||
|
||||
@@ -25,6 +25,10 @@ run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh"
|
||||
|
||||
# Rebuilds the boot image, so it has to follow the Panther Lake kernel swap
|
||||
# above rather than sit with the other Dell leaf at the top of this file.
|
||||
run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-sidecar-amps.sh"
|
||||
|
||||
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-display-backlight.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-display.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-touchpad.sh"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Enable the temporary sidecar amplifier workaround on the exact Dell XPS 13 model that needs it.
|
||||
#
|
||||
# Pacman registers a package even when its post_install scriptlet fails, so the
|
||||
# apply command runs explicitly here: a failed cleanup or boot-image rebuild has
|
||||
# to reach the caller rather than hide behind a successfully registered package.
|
||||
|
||||
if omarchy-hw-dell-xps13-sidecar-amps; then
|
||||
omarchy-pkg-add dell-xps13-sidecar-amps &&
|
||||
sudo dell-xps13-sidecar-amps-apply
|
||||
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"
|
||||
}
|
||||
@@ -19,7 +19,7 @@ cliamp
|
||||
cups
|
||||
cups-browsed
|
||||
cups-filters
|
||||
cups-pdf
|
||||
cups-pk-helper
|
||||
ddcutil
|
||||
docker
|
||||
docker-buildx
|
||||
|
||||
@@ -61,6 +61,7 @@ linux-firmware-marvell
|
||||
|
||||
# Dell laptop support packages
|
||||
dell-xps-touchpad-haptics
|
||||
dell-xps13-sidecar-amps
|
||||
|
||||
# Speaker tunings (LV2 limiter every tuning ends in)
|
||||
lsp-plugins-lv2
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
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
|
||||
|
||||
# omarchy-settings skips this override until cups-browsed is actually present
|
||||
# to avoid pacman creating cups-browsed.conf.pacnew during ISO package install.
|
||||
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -d /etc/cups ]]; then
|
||||
# omarchy-settings skips these overrides until CUPS is actually present to
|
||||
# avoid pacman creating .pacnew files during ISO package installation.
|
||||
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -f /etc/cups/cups-files.conf ]]; then
|
||||
systemd-sysusers /etc/sysusers.d/omarchy-cups-browsed.conf
|
||||
cp -f "$OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf" /etc/cups/cups-browsed.conf
|
||||
rm -f /etc/cups/cups-browsed.conf.pacnew
|
||||
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 /etc/cups/cups-files.conf.pacnew
|
||||
fi
|
||||
|
||||
source "$OMARCHY_INSTALL/hardware/pacman.sh"
|
||||
|
||||
@@ -79,7 +79,7 @@ Turkish|trq
|
||||
Ukrainian|ua'
|
||||
|
||||
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_DEFAULT='omarchy'
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ Omarchy watches systemd-coredump for process crashes. When something segfaults,
|
||||
|
||||
The watching is on by default. Turn it off under _Trigger > Toggle > Crash Capture_ (or with `omarchy toggle crash-capture`) and the notifications stop; `omarchy agent crash <pid>` still works by hand.
|
||||
|
||||
Crashes can also be silenced one program at a time, which is what the diagnosis offers you at the end. `omarchy crash mute hyprland` stops the notifications for that program only, `omarchy crash mute hyprland off` brings them back, and `omarchy crash mute` on its own lists what you've muted. It takes the binary's path as happily as its name, so `omarchy crash mute /usr/bin/hyprland` does the same thing. Quote a name with a space in it, as in `omarchy crash mute 'Some App'`. Everything else still notifies, and the muted program still crashes — this hides the reminder, it doesn't fix anything.
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Apple M1/M2 chips
|
||||
|
||||
[Asahi Alarm](https://asahi-alarm.org/) is a version of Arch for Apple M1/M2 computers built on top of [Asahi Linux](https://asahilinux.org/). You can get Omarchy running on top of that with some effort. See [the user-driven guide](https://codeberg.org/malik-na/omarchy-mac).
|
||||
[Asahi Alarm](https://asahi-alarm.org/) is a version of Arch for Apple M1/M2 computers built on top of [Asahi Linux](https://asahilinux.org/). You can get Omarchy running on top of that with some effort. See [the user-driven guide](https://github.com/omarchy-mac/omarchy-mac).
|
||||
|
||||
### Apple Virtual Machine
|
||||
|
||||
|
||||
@@ -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,6 @@
|
||||
echo "Enable Dell XPS 13 sidecar speaker amplifiers"
|
||||
|
||||
if omarchy-hw-dell-xps13-sidecar-amps; then
|
||||
source "$OMARCHY_PATH/install/hardware/dell-xps13-sidecar-amps.sh"
|
||||
omarchy-state set reboot-required
|
||||
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"
|
||||
@@ -138,6 +138,7 @@ BorderSurface {
|
||||
radius: 0
|
||||
}
|
||||
contentItem: Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.tooltipText
|
||||
color: root.tooltipForeground
|
||||
font.family: root.fontFamily
|
||||
@@ -158,6 +159,7 @@ BorderSurface {
|
||||
spacing: Style.spacing.controlGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.iconText !== ""
|
||||
text: root.iconText
|
||||
color: root.selected ? root._selectedColor : root.foreground
|
||||
@@ -177,6 +179,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.text !== ""
|
||||
text: root.text
|
||||
color: root.selected ? root._selectedColor : root.foreground
|
||||
|
||||
@@ -69,6 +69,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: messageText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
@@ -105,6 +106,7 @@ Item {
|
||||
radius: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
|
||||
|
||||
@@ -71,6 +71,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -110,6 +111,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -214,6 +216,7 @@ Item {
|
||||
: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -259,6 +259,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -298,6 +299,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -451,6 +453,7 @@ Item {
|
||||
: Border.controlSpec("normal", root.foreground, root.accent)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.loadingOptions ? "" : ""
|
||||
color: root.foreground
|
||||
@@ -486,6 +489,7 @@ Item {
|
||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: resultList.count === 0
|
||||
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
|
||||
@@ -581,6 +585,7 @@ Item {
|
||||
spacing: Style.spacing.xxs
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.label
|
||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -589,6 +594,7 @@ Item {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: modelData.description
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -25,6 +25,7 @@ Column {
|
||||
spacing: Style.spacing.md
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
|
||||
@@ -25,6 +25,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
textFormat: Text.PlainText
|
||||
// Keep the shared line box and baseline intact. Correcting only the
|
||||
// horizontal painted bounds avoids per-glyph vertical drift.
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -69,6 +69,7 @@ BorderSurface {
|
||||
Behavior on color { ColorAnimation { duration: 60 } }
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.iconText
|
||||
color: root.enabled
|
||||
|
||||
@@ -48,6 +48,7 @@ Item {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
|
||||
@@ -75,6 +76,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: detailText
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.detail
|
||||
color: root.dim
|
||||
@@ -87,6 +89,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: metaText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: root.meta.toUpperCase()
|
||||
visible: text !== ""
|
||||
|
||||
@@ -11,6 +11,10 @@ Text {
|
||||
property string fontFamily: Style.font.family
|
||||
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)
|
||||
font.family: fontFamily
|
||||
font.pixelSize: fontSize
|
||||
|
||||
@@ -36,6 +36,7 @@ ToolTip {
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.text
|
||||
color: root.panelForeground
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -93,6 +93,7 @@ Item {
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
@@ -132,6 +133,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -246,6 +248,7 @@ Item {
|
||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: resultList.count === 0
|
||||
text: root.emptyText
|
||||
@@ -313,6 +316,7 @@ Item {
|
||||
spacing: Style.spacing.xxs
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.optionLabel(modelData)
|
||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -321,6 +325,7 @@ Item {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: root.optionDescription(modelData)
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -130,6 +130,7 @@ PanelWindow {
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.title !== ""
|
||||
text: root.title.toUpperCase()
|
||||
color: root.onScrimDim
|
||||
@@ -182,6 +183,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.failed
|
||||
text: root.error
|
||||
color: root.onScrimUrgent
|
||||
@@ -368,6 +370,7 @@ PanelWindow {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
// Both branches go through the locale: a reading is a measurement, so
|
||||
// its separators follow the system's number conventions rather than the
|
||||
@@ -383,6 +386,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.unit
|
||||
color: root.onScrimDim
|
||||
@@ -394,6 +398,7 @@ PanelWindow {
|
||||
// The 90° gap at the bottom of the scale is where a cluster prints its
|
||||
// unit; here it names the direction.
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
text: dial.label
|
||||
|
||||
@@ -69,6 +69,7 @@ BorderSurface {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.label
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -79,6 +80,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.description !== ""
|
||||
text: root.description
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
|
||||
@@ -74,6 +74,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: label
|
||||
textFormat: Text.PlainText
|
||||
visible: root.labelVisible
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
|
||||
@@ -434,6 +434,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: heroMarkImage.status !== Image.Ready
|
||||
text: button.text
|
||||
@@ -504,6 +505,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: statusText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -558,6 +560,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: balanceValue
|
||||
textFormat: Text.PlainText
|
||||
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
|
||||
color: root.balanceAlarming ? root.urgent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -575,6 +578,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
width: parent.width
|
||||
text: root.balanceDetailText(root.balance)
|
||||
@@ -680,6 +684,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
width: parent.width
|
||||
topPadding: Style.space(2)
|
||||
@@ -710,6 +715,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: limitLabel
|
||||
textFormat: Text.PlainText
|
||||
// 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.
|
||||
text: limitRow.window ? limitRow.window.title : ""
|
||||
@@ -725,6 +731,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: limitValue
|
||||
textFormat: Text.PlainText
|
||||
text: limitRow.window && limitRow.window.percent >= 0
|
||||
? Math.round(limitRow.window.percent * 100) + "%"
|
||||
: "—"
|
||||
@@ -744,6 +751,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: resetText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: {
|
||||
var remainingMs = root.resetMsFor(limitRow.window)
|
||||
@@ -798,6 +806,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: dayLabel
|
||||
textFormat: Text.PlainText
|
||||
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
|
||||
color: dayRow.today ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -835,6 +844,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: dayValue
|
||||
textFormat: Text.PlainText
|
||||
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
|
||||
color: dayRow.today ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -890,6 +900,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: modelName
|
||||
textFormat: Text.PlainText
|
||||
text: modelRow.row ? modelRow.row.name : ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -904,6 +915,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: modelTokens
|
||||
textFormat: Text.PlainText
|
||||
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -1090,6 +1090,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: tooltipLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.tooltipText
|
||||
color: Color.tooltip.text
|
||||
|
||||
@@ -29,6 +29,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
|
||||
@@ -467,6 +467,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: rowIcon.right
|
||||
anchors.leftMargin: Style.space(10)
|
||||
@@ -577,6 +578,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(28)
|
||||
@@ -681,6 +683,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
@@ -709,6 +712,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: !menuRow.modelData.isSeparator
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -432,6 +432,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -500,6 +501,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
|
||||
height: parent.height
|
||||
text: parent.parent.previewText
|
||||
@@ -546,6 +548,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: parent.activeRow && !parent.activeRow.previewImage
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.contentMargin
|
||||
@@ -593,6 +596,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -519,12 +519,14 @@ Item {
|
||||
width: Style.space(140)
|
||||
spacing: Style.space(1)
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: "Style.font." + modelData.key
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.size + " px"
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
@@ -534,6 +536,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: sampleText
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: metaCol.right
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -574,6 +577,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.family
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -587,6 +591,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.resolvedFamily
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -600,6 +605,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.font.baseSize + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -613,6 +619,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.bar.sizeHorizontal + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -626,6 +633,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.bar.sizeVertical + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -639,6 +647,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.spacing.scale.toFixed(2)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -652,6 +661,7 @@ Item {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Style.spacing.panelPadding + " px"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -818,6 +828,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: csLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1273,6 +1284,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
|
||||
@@ -247,6 +247,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -284,6 +285,7 @@ Item {
|
||||
color: hasCursor ? root.selectedBackground : "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: parent.emoji
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
@@ -326,6 +328,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: "No matches for “" + root.filterText + "”"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -545,6 +545,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: selectedLabel
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showLabels
|
||||
anchors.top: carousel.bottom
|
||||
anchors.topMargin: Style.space(16)
|
||||
@@ -561,6 +562,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.filterable && root.filterText
|
||||
anchors.top: selectedLabel.bottom
|
||||
anchors.topMargin: Style.space(8)
|
||||
|
||||
@@ -184,6 +184,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: passwordInput
|
||||
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
||||
visible: passwordInput.text.length === 0
|
||||
|
||||
@@ -1199,6 +1199,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1287,6 +1288,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
textFormat: Text.PlainText
|
||||
visible: row.hasIcon && !row.isApp
|
||||
text: row.icon
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
@@ -1328,6 +1330,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: row.label
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
@@ -1338,6 +1341,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: row.detail
|
||||
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
|
||||
@@ -1358,6 +1362,7 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: false
|
||||
text: row.childCount
|
||||
color: root.foreground
|
||||
@@ -1368,6 +1373,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: row.kind === "menu" || row.kind === "link" ? "›" : ""
|
||||
color: row.hasCursor ? root.selectedText : root.foreground
|
||||
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
|
||||
@@ -1452,6 +1458,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
|
||||
@@ -5,8 +5,92 @@ function isChromiumDerived(app, appIcon) {
|
||||
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) {
|
||||
var text = String(body || "").replace(/<img[^>]*>/gi, "")
|
||||
var text = stripImageTags(String(body || ""))
|
||||
if (!isChromiumDerived(app, appIcon)) return text
|
||||
|
||||
return text
|
||||
@@ -366,6 +450,7 @@ if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
isChromiumDerived: isChromiumDerived,
|
||||
sanitizeBody: sanitizeBody,
|
||||
styledBody: styledBody,
|
||||
summaryStartsWithGlyph: summaryStartsWithGlyph,
|
||||
shouldBypassDnd: shouldBypassDnd,
|
||||
isEphemeralApp: isEphemeralApp,
|
||||
|
||||
@@ -44,7 +44,7 @@ BorderSurface {
|
||||
readonly property bool singleLineToast: sanitizedBody.length === 0
|
||||
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
|
||||
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 bodyColor: Qt.darker(Color.notifications.text, 1.15)
|
||||
@@ -133,6 +133,7 @@ BorderSurface {
|
||||
// Glyph fallback (Nerd Font character) when no image icon is
|
||||
// available. Used by omarchy-notification-send's `-g` flag.
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
|
||||
text: root.glyph
|
||||
@@ -143,6 +144,7 @@ BorderSurface {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: root.compactGlyph
|
||||
text: root.glyph
|
||||
@@ -159,6 +161,11 @@ BorderSurface {
|
||||
spacing: Style.space(2)
|
||||
|
||||
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
|
||||
visible: root.summary.length > 0
|
||||
text: root.summary
|
||||
|
||||
@@ -159,6 +159,7 @@ Item {
|
||||
width: root.iconWidth
|
||||
height: parent.height
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
// Sit the glyph's ink flush in the column, centered when the
|
||||
// column is wider than this particular glyph.
|
||||
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
||||
@@ -186,6 +187,7 @@ Item {
|
||||
}
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.message !== ""
|
||||
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
||||
// 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.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.outputIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -761,6 +762,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroLabel
|
||||
textFormat: Text.PlainText
|
||||
text: root.outputVolumeName(
|
||||
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
|
||||
root.outputMuted
|
||||
@@ -800,6 +802,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: outputPercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -886,6 +889,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: microphonePercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1030,6 +1034,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.sinkGlyph(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1040,6 +1045,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.nodeLabel(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1089,6 +1095,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.sourceGlyph(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1099,6 +1106,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.nodeLabel(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1159,6 +1167,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: streamMuteIcon
|
||||
textFormat: Text.PlainText
|
||||
text: streamRow.streamMuted ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1179,6 +1188,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.streamLabel(streamRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1191,6 +1201,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: streamPct
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round(streamRow.streamVolume * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -698,6 +698,7 @@ Panel {
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.icon
|
||||
@@ -748,6 +749,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroStatus
|
||||
textFormat: Text.PlainText
|
||||
text: root.heroStatusText.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -863,6 +865,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
|
||||
text: !root.adapter ? "No Bluetooth adapter"
|
||||
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
||||
@@ -971,6 +974,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: deviceIcon
|
||||
textFormat: Text.PlainText
|
||||
text: row.isConnected ? "" : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -989,6 +993,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.deviceLabel(row.dev) || "Device"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -997,6 +1002,7 @@ Panel {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: row.statusText !== ""
|
||||
text: row.statusText
|
||||
color: row.statusColor
|
||||
|
||||
@@ -311,6 +311,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroDate
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Qt.formatDate(root.today, "MMMM d")
|
||||
color: heroMouse.containsMouse
|
||||
@@ -413,6 +414,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: yearLabel
|
||||
textFormat: Text.PlainText
|
||||
visible: !root.editingLife
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -425,6 +427,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: yearPercent
|
||||
textFormat: Text.PlainText
|
||||
visible: !root.editingLife
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -485,6 +488,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: lifePercent
|
||||
textFormat: Text.PlainText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.lifeDonePercent + "%"
|
||||
@@ -608,6 +612,7 @@ Panel {
|
||||
model: root.weekdays
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
required property var modelData
|
||||
width: root.cellWidth
|
||||
height: Style.space(16)
|
||||
@@ -631,6 +636,7 @@ Panel {
|
||||
spacing: root.cellSpacing
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: root.weekColumnWidth
|
||||
height: root.cellHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -662,6 +668,7 @@ Panel {
|
||||
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: modelData.day
|
||||
color: modelData.inMonth
|
||||
@@ -707,6 +714,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: monthLabel
|
||||
textFormat: Text.PlainText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Fixed width so the chevrons hold still between a
|
||||
|
||||
@@ -281,6 +281,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
||||
width: parent.width
|
||||
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
||||
@@ -421,6 +422,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
||||
color: root.foreground
|
||||
@@ -430,6 +432,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
||||
color: root.dim
|
||||
@@ -478,6 +481,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: Model.fileGlyph(fileRow.fileName)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -491,6 +495,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: fileRow.fileName
|
||||
color: root.foreground
|
||||
@@ -500,6 +505,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: Model.fileMeta(fileRow.file)
|
||||
color: root.dim
|
||||
@@ -524,6 +530,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.fontFamily
|
||||
@@ -531,6 +538,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -531,6 +531,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.displays.length > 1 ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -559,6 +560,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroLabel
|
||||
textFormat: Text.PlainText
|
||||
text: {
|
||||
if (root.brightnessAvailable) {
|
||||
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
|
||||
@@ -602,6 +604,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: brightnessPercent
|
||||
textFormat: Text.PlainText
|
||||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -674,6 +677,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: textSizePx
|
||||
textFormat: Text.PlainText
|
||||
text: (textSizeSlider.dragging
|
||||
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
|
||||
: root.displayedTextPx()) + "px"
|
||||
@@ -747,6 +751,7 @@ Panel {
|
||||
// focused one.
|
||||
Text {
|
||||
id: scaleMonitor
|
||||
textFormat: Text.PlainText
|
||||
text: root.focusedMonitor
|
||||
// Only worth naming when more than one display is in play.
|
||||
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
|
||||
@@ -887,6 +892,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -897,6 +903,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: monitorRow.display.enabled ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -1090,6 +1090,7 @@ Panel {
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.icon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1170,6 +1171,7 @@ Panel {
|
||||
// rather than in a pill, which crowded the on/off switch.
|
||||
Text {
|
||||
id: heroSsid
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
|
||||
readonly property string title: {
|
||||
@@ -1189,6 +1191,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroMeta
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: {
|
||||
if (root.info.type === "wifi") {
|
||||
@@ -1711,6 +1714,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: networkIcon
|
||||
textFormat: Text.PlainText
|
||||
text: row.net ? root.wifiIconFor(row.net.signal) : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1732,6 +1736,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: lockIndicator
|
||||
textFormat: Text.PlainText
|
||||
visible: row.requiresCredentials || row.forgetVisible
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1779,6 +1784,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: row.net ? (row.net.ssid || "Hidden") : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1787,6 +1793,7 @@ Panel {
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
// Signal strength is conveyed by the wifi-bars icon and the
|
||||
// right-edge glyph/buttons carry protection or forget affordances,
|
||||
// so the second line only carries action status (Connecting…,
|
||||
@@ -1893,6 +1900,7 @@ Panel {
|
||||
radius: Style.cornerRadius
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -1946,6 +1954,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -1953,6 +1962,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -325,6 +325,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
text: root.batteryIcon()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -356,6 +357,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroStatus
|
||||
textFormat: Text.PlainText
|
||||
text: root.heroStatusText.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -369,6 +371,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroPercent
|
||||
textFormat: Text.PlainText
|
||||
text: root.batteryInfo.percentage || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -517,6 +520,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -524,6 +528,7 @@ Panel {
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
textFormat: Text.PlainText
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
|
||||
@@ -498,6 +498,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||
width: parent.width
|
||||
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
|
||||
@@ -841,6 +842,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: accountRow.accountText
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -933,6 +935,7 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: tailscale.osIcon(peer ? peer.OS : "")
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -946,6 +949,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: peerRow.peerName
|
||||
color: root.foreground
|
||||
@@ -955,6 +959,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
var parts = []
|
||||
@@ -1087,6 +1092,7 @@ Panel {
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
Layout.fillWidth: true
|
||||
text: copyChoice.label
|
||||
color: root.foreground
|
||||
@@ -1134,6 +1140,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: exitNodeGlyph
|
||||
textFormat: Text.PlainText
|
||||
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "" : "")
|
||||
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
@@ -1154,6 +1161,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: exitNodeRow.peerName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
@@ -1224,6 +1232,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: regionRow.regionName
|
||||
color: root.foreground
|
||||
@@ -1234,6 +1243,7 @@ Panel {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
width: parent.width
|
||||
text: regionRow.regionDetail
|
||||
visible: text !== ""
|
||||
|
||||
@@ -531,6 +531,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: heroIcon
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 5
|
||||
text: root.label || "—"
|
||||
@@ -547,6 +548,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: tempBig
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportTempNum || "—"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -556,6 +558,7 @@ Panel {
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.current ? root.tempUnit : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -593,6 +596,7 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: (root.reportLocation || "").toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -643,6 +647,7 @@ Panel {
|
||||
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.centerIn: parent
|
||||
text: root.savingLocation ? "" : "✕"
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -683,6 +688,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportFeels
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -700,6 +706,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportWind
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -717,6 +724,7 @@ Panel {
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.reportHumidity
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -752,12 +760,14 @@ Panel {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: modelData.name
|
||||
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: text !== ""
|
||||
text: modelData.description
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
@@ -817,6 +827,7 @@ Panel {
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.dayIcon(modelData)
|
||||
color: root.bar.foreground
|
||||
@@ -829,6 +840,7 @@ Panel {
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.dayName(modelData.date).toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -840,12 +852,14 @@ Panel {
|
||||
spacing: Style.space(6)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.bareTempForDay(modelData, "max")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.bareTempForDay(modelData, "min")
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -257,6 +257,7 @@ Item {
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: (root.ssid || "Wi-Fi").toUpperCase()
|
||||
color: root.onScrimDim
|
||||
font.family: root.fontFamily
|
||||
@@ -318,6 +319,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.error !== ""
|
||||
text: root.error
|
||||
color: root.onScrimUrgent
|
||||
@@ -340,6 +342,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
visible: root.showingQr && root.secured
|
||||
text: root.passwordError !== "" ? root.passwordError
|
||||
: root.passwordVisible ? root.password
|
||||
|
||||
@@ -332,6 +332,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -374,6 +375,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: justificationText
|
||||
textFormat: Text.PlainText
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(12)
|
||||
anchors.rightMargin: Style.space(12)
|
||||
|
||||
@@ -156,6 +156,7 @@ Item {
|
||||
anchors.leftMargin: card.contentLeftInset
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -32,6 +32,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
textFormat: Text.PlainText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.playIcon
|
||||
color: activePlayer && activePlayer.isPlaying ? root.bar.barForeground : Qt.darker(root.bar.barForeground, 1.5)
|
||||
@@ -53,6 +54,7 @@ BarWidget {
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
textFormat: Text.PlainText
|
||||
text: root.title + (root.artist ? " · " + root.artist : "")
|
||||
color: root.bar.barForeground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -148,6 +150,7 @@ BarWidget {
|
||||
width: parent.width - Style.space(74)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.title || "Nothing playing"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -158,6 +161,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.artist
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -168,6 +172,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -255,6 +260,7 @@ BarWidget {
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.player && sourceRow.player.isPlaying ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -270,6 +276,7 @@ BarWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.sourceTitle
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -280,6 +287,7 @@ BarWidget {
|
||||
}
|
||||
|
||||
Text {
|
||||
textFormat: Text.PlainText
|
||||
text: sourceRow.sourceDetail
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
|
||||
@@ -64,6 +64,72 @@ verify_services() {
|
||||
pass "user audio services are running"
|
||||
}
|
||||
|
||||
verify_printing_security() {
|
||||
local cups_browsed_pid lpinfo_output printer_name printer_process printer_tmp
|
||||
|
||||
! pacman -Q cups-pdf >/dev/null 2>&1 || fail "CUPS-PDF is absent"
|
||||
pass "the root CUPS-PDF backend is not installed"
|
||||
|
||||
getent passwd cups-browsed >/dev/null || fail "the cups-browsed service account exists"
|
||||
[[ $(systemctl show -P User cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service account"
|
||||
[[ $(systemctl show -P Group cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service group"
|
||||
systemctl is-active --quiet cups-browsed.service || fail "cups-browsed is running"
|
||||
|
||||
cups_browsed_pid=$(systemctl show -P MainPID cups-browsed.service)
|
||||
[[ -r /proc/$cups_browsed_pid/status ]] || fail "cups-browsed has a readable process status"
|
||||
[[ $(awk '/^Uid:/{print $2}' "/proc/$cups_browsed_pid/status") != 0 ]] ||
|
||||
fail "cups-browsed does not run with root UID"
|
||||
[[ $(awk '/^CapEff:/{print $2}' "/proc/$cups_browsed_pid/status") == "0000000000000000" ]] ||
|
||||
fail "cups-browsed has no effective Linux capabilities"
|
||||
|
||||
[[ $(stat -c '%a %U:%G' /var/cache/cups-browsed) == "750 cups-browsed:cups-browsed" ]] ||
|
||||
fail "cups-browsed has an isolated cache" "$(stat -c '%a %U:%G' /var/cache/cups-browsed)"
|
||||
[[ " $(id -nG cups-browsed) " != *" cups "* ]] ||
|
||||
fail "cups-browsed is separate from the print-filter group"
|
||||
|
||||
if lpinfo_output=$(LC_ALL=C timeout 10 lpinfo -v </dev/null 2>&1); then
|
||||
fail "the desktop user cannot administer CUPS without authentication"
|
||||
elif [[ $lpinfo_output != *"Forbidden"* ]]; then
|
||||
fail "CUPS explicitly denies unauthenticated desktop administration" "$lpinfo_output"
|
||||
fi
|
||||
|
||||
pass "CUPS discovery is isolated from root, filters, and passwordless desktop administration"
|
||||
|
||||
# A live driverless printer proves the non-root daemon can still discover and
|
||||
# create queues without the CAP_NET_BIND_SERVICE Ubuntu carries downstream.
|
||||
printer_name="OmarchyAcceptancePrinter"
|
||||
printer_tmp=$(mktemp -d)
|
||||
printf '#!/bin/bash\nexit 0\n' >"$printer_tmp/command"
|
||||
chmod 0700 "$printer_tmp/command"
|
||||
mkdir -m 0700 "$printer_tmp/spool"
|
||||
|
||||
ippeveprinter -p 18631 -d "$printer_tmp/spool" -c "$printer_tmp/command" "$printer_name" \
|
||||
>"$printer_tmp/ippeveprinter.log" 2>&1 &
|
||||
printer_process=$!
|
||||
|
||||
printing_test_cleanup() {
|
||||
kill "$printer_process" >/dev/null 2>&1 || true
|
||||
wait "$printer_process" >/dev/null 2>&1 || true
|
||||
rm -rf "$printer_tmp"
|
||||
}
|
||||
trap printing_test_cleanup EXIT
|
||||
|
||||
for _ in {1..30}; do
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" ||
|
||||
fail "non-root cups-browsed discovers a driverless IPP printer" "$(<"$printer_tmp/ippeveprinter.log")"
|
||||
|
||||
printing_test_cleanup
|
||||
trap - EXIT
|
||||
|
||||
pass "non-root cups-browsed still creates driverless IPP queues without capabilities"
|
||||
}
|
||||
|
||||
verify_runtime_tools() {
|
||||
# Docker access is intentionally NOT granted to the desktop user: the docker
|
||||
# group is root-equivalent, so a rogue process running as the user could
|
||||
@@ -107,7 +173,7 @@ verify_user_setup() {
|
||||
pass "Omarchy user state and shell configuration exist"
|
||||
}
|
||||
|
||||
for check in verify_core_packages verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
for check in verify_core_packages verify_defaults verify_services verify_printing_security verify_runtime_tools verify_user_setup; do
|
||||
if ! ("$check"); then
|
||||
status=1
|
||||
fi
|
||||
|
||||
@@ -13,6 +13,10 @@ mkdir -p "$TEST_HOME/.codex/sessions/$(date +%Y/%m/%d)" "$TEST_HOME/bin"
|
||||
cat >"$TEST_HOME/bin/codex" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -n ${CODEX_ARGS_FILE:-} ]]; then
|
||||
printf '%s\0' "$@" >"$CODEX_ARGS_FILE"
|
||||
fi
|
||||
|
||||
while read -r request; do
|
||||
id=$(jq -r '.id // empty' <<<"$request")
|
||||
method=$(jq -r '.method // empty' <<<"$request")
|
||||
@@ -40,9 +44,18 @@ cat >"$session" <<EOF
|
||||
{"timestamp":"$timestamp","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":180,"cached_input_tokens":110,"output_tokens":30,"reasoning_output_tokens":8,"total_tokens":210},"last_token_usage":{"input_tokens":80,"cached_input_tokens":50,"output_tokens":10,"reasoning_output_tokens":3,"total_tokens":90}}}}
|
||||
EOF
|
||||
|
||||
result=$(HOME="$TEST_HOME" CODEX_HOME="$TEST_HOME/.codex" XDG_DATA_HOME="$TEST_HOME/.local/share" PATH="$TEST_HOME/bin:$PATH" \
|
||||
result=$(HOME="$TEST_HOME" CODEX_HOME="$TEST_HOME/.codex" CODEX_ARGS_FILE="$TEST_HOME/codex-args" XDG_DATA_HOME="$TEST_HOME/.local/share" PATH="$TEST_HOME/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-agent-usage-codex")
|
||||
|
||||
# NUL-separated, so the assertion sees argument boundaries: a single "-a on-request"
|
||||
# would flatten to the same text as two arguments but is not a policy codex accepts.
|
||||
expected_args=(-s read-only -a on-request app-server)
|
||||
mapfile -d '' -t codex_args <"$TEST_HOME/codex-args"
|
||||
|
||||
[[ ${codex_args[*]@Q} == "${expected_args[*]@Q}" ]] ||
|
||||
fail "Codex collector uses the supported approval policy" "${codex_args[*]@Q}"
|
||||
pass "Codex collector uses the supported approval policy"
|
||||
|
||||
[[ $(jq -r '.todayTotalTokens' <<<"$result") == "210" ]] ||
|
||||
fail "Codex collector counts each turn once" "$result"
|
||||
pass "Codex collector counts each turn once"
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
# The /tmp-fallback case (below) must place its decoy at exactly the fixed path the
|
||||
# old wrapper would have formed, so it cannot use a random mktemp name. Track whether
|
||||
# we created it and remove it on exit only then -- never touch a path we did not create.
|
||||
tmp_cache="/tmp/omarchy-brightness-display-apple.device"
|
||||
created_tmp_cache=0
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMPDIR"
|
||||
# Remove the /tmp decoy only if this test is the one that created it.
|
||||
if (( created_tmp_cache )); then
|
||||
rm -f "$tmp_cache"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Stubs on PATH: drop sudo so asdcontrol runs directly, record every asdcontrol
|
||||
# invocation, make detection deterministic by having --detect report no device,
|
||||
# and no-op the OSD. On a host without any /dev/*hiddev* node the wrapper's
|
||||
# detect_apple_display_device returns before it ever runs asdcontrol, so the
|
||||
# reject cases assert on the negative: a refused cache value is never handed to
|
||||
# `asdcontrol <dev> -- <step>`. Blind-trust validation would hand it over and be
|
||||
# caught here.
|
||||
stub_dir="$TMPDIR/stubs"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
asd_log="$TMPDIR/asdcontrol.log"
|
||||
|
||||
cat >"$stub_dir/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
chmod +x "$stub_dir/sudo"
|
||||
|
||||
cat >"$stub_dir/asdcontrol" <<STUB
|
||||
#!/bin/bash
|
||||
printf '%s\n' "\$*" >>"$asd_log"
|
||||
# --detect reports nothing, so detection never yields a device.
|
||||
if [[ \$1 == "--detect" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
# A brightness read (a lone device arg) returns a plausible value; a set
|
||||
# (<device> -- <step>) just succeeds.
|
||||
if [[ \$# -eq 1 ]]; then
|
||||
printf '%s: BRIGHTNESS=30000\n' "\$1"
|
||||
fi
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$stub_dir/asdcontrol"
|
||||
|
||||
cat >"$stub_dir/omarchy-osd" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$stub_dir/omarchy-osd"
|
||||
|
||||
run_wrapper() {
|
||||
# $1: value for XDG_RUNTIME_DIR ("" means unset); remaining args go to the wrapper.
|
||||
local xdg="$1"
|
||||
shift
|
||||
: >"$asd_log"
|
||||
if [[ -n $xdg ]]; then
|
||||
XDG_RUNTIME_DIR="$xdg" PATH="$stub_dir:$ROOT/bin:$PATH" \
|
||||
omarchy-brightness-display-apple "$@" 2>&1 || true
|
||||
else
|
||||
env -u XDG_RUNTIME_DIR PATH="$stub_dir:$ROOT/bin:$PATH" \
|
||||
omarchy-brightness-display-apple "$@" 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# --- A cache value that is not a hiddev character device is rejected ----------
|
||||
xdg_dir="$TMPDIR/xdg"
|
||||
mkdir -p "$xdg_dir"
|
||||
cache_file="$xdg_dir/omarchy-brightness-display-apple.device"
|
||||
|
||||
regular_file="$TMPDIR/not-a-device"
|
||||
: >"$regular_file"
|
||||
|
||||
poisons=("/dev/null" "$regular_file" "/tmp/omarchy-evil")
|
||||
|
||||
# The cases above all fail on the pathname prefix, so none of them reaches the -c
|
||||
# test -- drop `&& -c $cached` from the wrapper and they all still pass. A path
|
||||
# that matches the hiddev glob but is not a character device is what -c is for,
|
||||
# and it is the realistic stale cache: the display replugs, the interface
|
||||
# renumbers, and the cached node is simply gone. Add it only when the host really
|
||||
# has no such node, so a machine with the display attached cannot fail here.
|
||||
if [[ ! -e /dev/hiddev999 ]]; then
|
||||
poisons+=("/dev/hiddev999")
|
||||
fi
|
||||
|
||||
for poison in "${poisons[@]}"; do
|
||||
printf '%s\n' "$poison" >"$cache_file"
|
||||
output=$(run_wrapper "$xdg_dir" "+5%")
|
||||
if grep -qF -- "$poison -- +5%" "$asd_log"; then
|
||||
fail "wrapper handed a non-hiddev cache value to asdcontrol: $poison" "$output"
|
||||
fi
|
||||
done
|
||||
pass "wrapper rejects a cached path that is not a hiddev character device"
|
||||
|
||||
# NOTE: the /dev/hiddev999 case above covers the -c test for a glob-matching path
|
||||
# that does not exist. The remaining arm -- a path under /dev that exists, matches
|
||||
# the glob, and is not a character device -- cannot be built without root, since
|
||||
# only real device nodes live there.
|
||||
|
||||
# --- A legitimate cached hiddev node is trusted (only where HW is present) ----
|
||||
real_hiddev=""
|
||||
for candidate in /dev/usb/hiddev* /dev/hiddev*; do
|
||||
if [[ -c $candidate ]]; then
|
||||
real_hiddev="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n $real_hiddev ]]; then
|
||||
printf '%s\n' "$real_hiddev" >"$cache_file"
|
||||
run_wrapper "$xdg_dir" "+5%" >/dev/null
|
||||
grep -qF -- "$real_hiddev -- +5%" "$asd_log" ||
|
||||
fail "wrapper did not trust a valid cached hiddev node: $real_hiddev"
|
||||
pass "wrapper trusts a cached hiddev character device without re-detecting"
|
||||
else
|
||||
pass "no /dev/hiddev* character device present; skipping the valid-cache case"
|
||||
fi
|
||||
|
||||
# --- With no XDG_RUNTIME_DIR, the predictable /tmp cache is not consulted ------
|
||||
# Assert on the open, not on the contents. A decoy holding a rejectable path proves
|
||||
# nothing: the validation above refuses it whether or not the /tmp fallback is still
|
||||
# there, so that assertion passes against both wrappers. A FIFO with no writer blocks
|
||||
# whoever opens it, so a wrapper that consults the path hangs and one that ignores it
|
||||
# exits -- which separates the two. mkfifo is atomic and fails outright if the path is
|
||||
# taken, so it neither overwrites a file nor follows a symlink; the fixed path is
|
||||
# required, being exactly the path the old code would have formed. Clear the flag as
|
||||
# soon as the decoy is gone, so a concurrent run's decoy cannot be removed by this
|
||||
# run's EXIT trap.
|
||||
if mkfifo "$tmp_cache" 2>/dev/null; then
|
||||
created_tmp_cache=1
|
||||
status=0
|
||||
env -u XDG_RUNTIME_DIR PATH="$stub_dir:$ROOT/bin:$PATH" \
|
||||
timeout 5 omarchy-brightness-display-apple "+5%" >/dev/null 2>&1 || status=$?
|
||||
rm -f "$tmp_cache"
|
||||
created_tmp_cache=0
|
||||
(( status != 124 )) ||
|
||||
fail "wrapper consulted the world-writable /tmp cache with no XDG_RUNTIME_DIR" \
|
||||
"it blocked reading the FIFO decoy at $tmp_cache"
|
||||
pass "wrapper ignores the /tmp cache path when XDG_RUNTIME_DIR is unset"
|
||||
else
|
||||
pass "$tmp_cache already present or not safely creatable; skipping the /tmp-fallback case"
|
||||
fi
|
||||
Executable
+330
@@ -0,0 +1,330 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
export OMARCHY_PROVISIONING_DIR="$test_tmp/provisioning"
|
||||
|
||||
source "$ROOT/install/helpers/browser-policy.sh"
|
||||
|
||||
# Temp dirs are user-owned; drop -o/-g so install(1) can run unprivileged.
|
||||
unprivileged_as_root() {
|
||||
if [[ $1 == "install" ]]; then
|
||||
shift
|
||||
local args=()
|
||||
local skip=0
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
if (( skip )); then
|
||||
skip=0
|
||||
continue
|
||||
fi
|
||||
case $arg in
|
||||
-o|-g) skip=1 ;;
|
||||
*) args+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
command install "${args[@]}"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
write_dir=$test_tmp/writable
|
||||
mkdir -p "$write_dir"
|
||||
browser_policy_install_color "$write_dir" "#aabbcc" ||
|
||||
fail "theme colour writes into a writable policy directory"
|
||||
grep -F '"BrowserThemeColor": "#aabbcc"' "$write_dir/color.json" >/dev/null ||
|
||||
fail "theme colour writes BrowserThemeColor"
|
||||
mode=$(stat -c '%a' "$write_dir/color.json")
|
||||
[[ $mode == "644" ]] || fail "theme colour creates a root-mode policy file" "mode=$mode"
|
||||
pass "theme colour writes a 0644 color.json"
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
pass "running as root; skipping the mktemp-failure check"
|
||||
else
|
||||
chmod u+w "$write_dir"
|
||||
export TMPDIR=$test_tmp/missing-tmp
|
||||
if browser_policy_install_color "$write_dir" "#dead00" 2>/dev/null; then
|
||||
fail "theme colour fails when mktemp cannot create a file"
|
||||
fi
|
||||
unset TMPDIR
|
||||
grep -F '"BrowserThemeColor": "#aabbcc"' "$write_dir/color.json" >/dev/null ||
|
||||
fail "a failed mktemp leaves an existing color.json intact"
|
||||
pass "a failed mktemp does not truncate color.json"
|
||||
fi
|
||||
|
||||
printf 'original\n' >"$test_tmp/pwn"
|
||||
rm -f "$write_dir/color.json"
|
||||
ln -s "$test_tmp/pwn" "$write_dir/color.json"
|
||||
browser_policy_install_color "$write_dir" "#aabbcc" ||
|
||||
fail "theme colour replaces a planted color.json symlink"
|
||||
[[ -f $write_dir/color.json && ! -L $write_dir/color.json ]] ||
|
||||
fail "theme colour unlinks a planted color.json symlink instead of writing through it"
|
||||
grep -Fxq 'original' "$test_tmp/pwn" || fail "theme colour leaves the symlink target unchanged"
|
||||
pass "theme colour does not follow a planted color.json symlink"
|
||||
|
||||
plant_write=$test_tmp/plant-dir
|
||||
mkdir -p "$plant_write/color.json/nested"
|
||||
printf 'inside\n' >"$plant_write/color.json/nested/x"
|
||||
browser_policy_install_color "$plant_write" "#aabbcc" ||
|
||||
fail "theme colour replaces a planted color.json directory"
|
||||
[[ -f $plant_write/color.json && ! -d $plant_write/color.json ]] ||
|
||||
fail "theme colour does not write into a planted color.json directory"
|
||||
pass "theme colour does not write into a planted color.json directory"
|
||||
|
||||
missing_dir=$test_tmp/missing
|
||||
browser_policy_install_color "$missing_dir" "#aabbcc" ||
|
||||
fail "theme colour skips a policy directory that does not exist"
|
||||
[[ ! -e $missing_dir ]] || fail "theme colour does not create a missing policy directory"
|
||||
pass "theme colour skips a missing policy directory"
|
||||
|
||||
if browser_policy_install_color "$write_dir" "aabbcc" 2>/dev/null; then
|
||||
fail "theme colour rejects hex without a leading #"
|
||||
fi
|
||||
if browser_policy_install_color "$write_dir" "#AABBCC" 2>/dev/null; then
|
||||
fail "theme colour rejects uppercase hex"
|
||||
fi
|
||||
pass "theme colour accepts only # plus six lowercase hex digits"
|
||||
|
||||
planted_dir=$test_tmp/planted
|
||||
mkdir -p "$planted_dir/evil"
|
||||
printf 'evil\n' >"$planted_dir/evil/f"
|
||||
printf 'old\n' >"$planted_dir/color.json"
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
browser_policy_setup_dir "$planted_dir"
|
||||
[[ ! -e $planted_dir/evil ]] || fail "policy setup drops a non-empty non-root subdirectory"
|
||||
[[ ! -e $planted_dir/color.json ]] || fail "policy setup drops a non-root color.json"
|
||||
[[ -d $planted_dir ]] || fail "policy setup leaves the managed directory in place"
|
||||
mode=$(stat -c '%a' "$planted_dir")
|
||||
[[ $mode == "755" ]] || fail "policy setup leaves the managed directory 0755" "mode=$mode"
|
||||
pass "policy setup drops non-root files and non-empty subdirectories"
|
||||
|
||||
owned=$test_tmp/not-root
|
||||
mkdir -p "$owned"
|
||||
chmod 755 "$owned"
|
||||
if browser_policy_dir_hardened "$owned"; then
|
||||
fail "a user-owned 0755 directory is not treated as hardened"
|
||||
fi
|
||||
pass "a hardened directory must be root-owned"
|
||||
|
||||
saved_parent_dirs=("${BROWSER_POLICY_PARENT_DIRS[@]}")
|
||||
parent_root=$test_tmp/parents
|
||||
mkdir -p "$parent_root/etc/chromium/policies/managed/keep"
|
||||
printf 'keep\n' >"$parent_root/etc/chromium/policies/managed/keep/x"
|
||||
chmod 0777 "$parent_root/etc/chromium" "$parent_root/etc/chromium/policies"
|
||||
chmod 755 "$parent_root/etc/chromium/policies/managed"
|
||||
BROWSER_POLICY_PARENT_DIRS=(
|
||||
"$parent_root/etc/chromium"
|
||||
"$parent_root/etc/chromium/policies"
|
||||
)
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
if browser_policy_parents_hardened "$parent_root/etc/chromium/policies/managed"; then
|
||||
fail "a world-writable policy parent is not treated as hardened"
|
||||
fi
|
||||
browser_policy_setup_parents_for "$parent_root/etc/chromium/policies/managed"
|
||||
mode=$(stat -c '%a' "$parent_root/etc/chromium")
|
||||
[[ $mode == "755" ]] || fail "setup tightens /etc/chromium" "mode=$mode"
|
||||
mode=$(stat -c '%a' "$parent_root/etc/chromium/policies")
|
||||
[[ $mode == "755" ]] || fail "setup tightens /etc/chromium/policies" "mode=$mode"
|
||||
[[ -d $parent_root/etc/chromium/policies/managed/keep ]] ||
|
||||
fail "parent repair does not purge the managed directory"
|
||||
pass "policy parent directories are tightened to 0755 without purging the leaf"
|
||||
|
||||
symlink_root=$test_tmp/symlink-parents
|
||||
mkdir -p "$symlink_root/etc" "$symlink_root/attacker/policies/managed"
|
||||
printf 'planted\n' >"$symlink_root/attacker/policies/managed/evil.json"
|
||||
ln -s "$symlink_root/attacker" "$symlink_root/etc/chromium"
|
||||
BROWSER_POLICY_PARENT_DIRS=(
|
||||
"$symlink_root/etc/chromium"
|
||||
"$symlink_root/etc/chromium/policies"
|
||||
)
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
browser_policy_setup_dir "$symlink_root/etc/chromium/policies/managed"
|
||||
[[ ! -L $symlink_root/etc/chromium ]] || fail "setup replaces a planted /etc/chromium symlink"
|
||||
[[ -d $symlink_root/etc/chromium && ! -L $symlink_root/etc/chromium ]] ||
|
||||
fail "setup recreates /etc/chromium as a real directory"
|
||||
[[ -d $symlink_root/etc/chromium/policies && ! -L $symlink_root/etc/chromium/policies ]] ||
|
||||
fail "setup recreates /etc/chromium/policies as a real directory"
|
||||
[[ ! -e $symlink_root/etc/chromium/policies/managed/evil.json ]] ||
|
||||
fail "setup does not keep policy that lived behind a planted parent symlink"
|
||||
grep -Fxq 'planted' "$symlink_root/attacker/policies/managed/evil.json" ||
|
||||
fail "replacing a parent symlink does not delete the symlink target"
|
||||
BROWSER_POLICY_PARENT_DIRS=("${saved_parent_dirs[@]}")
|
||||
pass "policy setup does not follow a planted parent symlink"
|
||||
|
||||
leaf_link_root=$test_tmp/leaf-link
|
||||
mkdir -p "$leaf_link_root/etc/chromium/policies" "$leaf_link_root/attacker"
|
||||
printf 'planted\n' >"$leaf_link_root/attacker/evil.json"
|
||||
chmod 755 "$leaf_link_root/etc/chromium" "$leaf_link_root/etc/chromium/policies"
|
||||
ln -s "$leaf_link_root/attacker" "$leaf_link_root/etc/chromium/policies/managed"
|
||||
BROWSER_POLICY_PARENT_DIRS=(
|
||||
"$leaf_link_root/etc/chromium"
|
||||
"$leaf_link_root/etc/chromium/policies"
|
||||
)
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
if browser_policy_dir_hardened "$leaf_link_root/etc/chromium/policies/managed"; then
|
||||
fail "a planted managed symlink is not treated as hardened"
|
||||
fi
|
||||
browser_policy_setup_dir "$leaf_link_root/etc/chromium/policies/managed"
|
||||
[[ ! -L $leaf_link_root/etc/chromium/policies/managed ]] ||
|
||||
fail "setup replaces a planted managed symlink"
|
||||
[[ -d $leaf_link_root/etc/chromium/policies/managed && ! -L $leaf_link_root/etc/chromium/policies/managed ]] ||
|
||||
fail "setup recreates managed as a real directory"
|
||||
[[ ! -e $leaf_link_root/etc/chromium/policies/managed/evil.json ]] ||
|
||||
fail "setup does not keep policy that lived behind a planted managed symlink"
|
||||
grep -Fxq 'planted' "$leaf_link_root/attacker/evil.json" ||
|
||||
fail "replacing a managed symlink does not delete the symlink target"
|
||||
BROWSER_POLICY_PARENT_DIRS=("${saved_parent_dirs[@]}")
|
||||
pass "policy setup does not follow a planted managed symlink"
|
||||
|
||||
fx_link_root=$test_tmp/fx-link
|
||||
mkdir -p "$fx_link_root/attacker" "$fx_link_root/opt"
|
||||
printf 'planted\n' >"$fx_link_root/attacker/policies.json"
|
||||
ln -s "$fx_link_root/attacker" "$fx_link_root/opt/zen"
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
if browser_policy_firefox_hardened "$fx_link_root/opt/zen"; then
|
||||
fail "a planted Firefox distribution symlink is not treated as hardened"
|
||||
fi
|
||||
browser_policy_setup_firefox_distribution "$fx_link_root/opt/zen" ||
|
||||
fail "Firefox setup replaces a planted distribution symlink"
|
||||
[[ ! -L $fx_link_root/opt/zen ]] || fail "Firefox setup unlinks a planted distribution symlink"
|
||||
[[ -d $fx_link_root/opt/zen && ! -L $fx_link_root/opt/zen ]] ||
|
||||
fail "Firefox setup recreates the distribution directory"
|
||||
[[ -f $fx_link_root/opt/zen/policies.json && ! -L $fx_link_root/opt/zen/policies.json ]] ||
|
||||
fail "Firefox setup writes policies.json into the recreated directory"
|
||||
grep -Fxq 'planted' "$fx_link_root/attacker/policies.json" ||
|
||||
fail "replacing a Firefox distribution symlink does not delete the symlink target"
|
||||
pass "Firefox setup does not follow a planted distribution symlink"
|
||||
|
||||
[[ $(browser_policy_theme_hex "242,240,229") == "#f2f0e5" ]] ||
|
||||
fail "theme colour converts an RGB triple to hex"
|
||||
[[ $(browser_policy_theme_hex $'14,31,41\n') == "#0e1f29" ]] ||
|
||||
fail "theme colour accepts a trailing newline"
|
||||
[[ $(browser_policy_theme_hex "0,0,0") == "#000000" ]] ||
|
||||
fail "theme colour pads single-digit components"
|
||||
[[ $(browser_policy_theme_hex " 12 , 11 , 12 ") == "#0c0b0c" ]] ||
|
||||
fail "theme colour tolerates surrounding whitespace"
|
||||
[[ $(browser_policy_theme_hex "08,09,10") == "#08090a" ]] ||
|
||||
fail "theme colour treats leading zeros as decimal"
|
||||
for malformed in "" "not,a,color" "1,2" "1,2,3,4" "256,0,0" "999,999,999" "-1,0,0" \
|
||||
"1,2,3;id" '1,2,$(id)' "0x10,0,0" "1,2,3 4,5,6"; do
|
||||
[[ $(browser_policy_theme_hex "$malformed") == "#1c2027" ]] ||
|
||||
fail "theme colour falls back to the stock grey for '$malformed'"
|
||||
done
|
||||
pass "theme colour is six hex digits or the stock grey"
|
||||
|
||||
for theme in "$ROOT"/themes/*/chromium.theme; do
|
||||
[[ -f $theme ]] || continue
|
||||
rgb=$(<$theme)
|
||||
hex=$(browser_policy_theme_hex "$rgb")
|
||||
[[ $hex =~ ^#[0-9a-f]{6}$ ]] ||
|
||||
fail "shipped $(basename "$(dirname "$theme")") chromium.theme parses as hex" "got: $hex from $(printf %q "$rgb")"
|
||||
if [[ $hex == "#1c2027" && ! $rgb =~ ^[[:space:]]*28[[:space:]]*,[[:space:]]*32[[:space:]]*,[[:space:]]*39[[:space:]]*$ ]]; then
|
||||
fail "shipped $(basename "$(dirname "$theme")") chromium.theme is a valid RGB triple" "got: $(printf %q "$rgb")"
|
||||
fi
|
||||
done
|
||||
pass "shipped chromium.theme files parse as RGB triples"
|
||||
|
||||
grep -F 'browser_policy_theme_hex' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser parses chromium.theme through browser_policy_theme_hex"
|
||||
grep -F 'omarchy-theme-set-browser-policy' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser writes colour through omarchy-theme-set-browser-policy"
|
||||
if grep -E 'printf.*THEME_RGB_COLOR' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null; then
|
||||
fail "omarchy-theme-set-browser does not hand unvetted theme words to printf"
|
||||
fi
|
||||
pass "omarchy-theme-set-browser validates the theme colour"
|
||||
|
||||
fx_policy=$test_tmp/policies.json
|
||||
printf '%s\n' '{"policies":{}}' >"$fx_policy"
|
||||
chmod 644 "$fx_policy"
|
||||
if browser_policy_firefox_policy_file_ok "$fx_policy"; then
|
||||
fail "a user-owned policies.json is not treated as hardened"
|
||||
fi
|
||||
ln -sf "$fx_policy" "$test_tmp/policies-link.json"
|
||||
if browser_policy_firefox_policy_file_ok "$test_tmp/policies-link.json"; then
|
||||
fail "a policies.json symlink is not treated as hardened"
|
||||
fi
|
||||
pass "Firefox policy files must be root-owned regular files without group or other write"
|
||||
|
||||
dist=$test_tmp/distribution
|
||||
mkdir -p "$dist"
|
||||
printf 'original\n' >"$test_tmp/firefox-pwn"
|
||||
ln -s "$test_tmp/firefox-pwn" "$dist/policies.json"
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
browser_policy_install_firefox_policies "$dist" ||
|
||||
fail "Firefox policy install replaces a planted policies.json symlink"
|
||||
[[ -f $dist/policies.json && ! -L $dist/policies.json ]] ||
|
||||
fail "Firefox policy install unlinks a planted policies.json symlink instead of writing through it"
|
||||
grep -Fxq 'original' "$test_tmp/firefox-pwn" || fail "Firefox policy install leaves the symlink target unchanged"
|
||||
grep -q '"policies"' "$dist/policies.json" || fail "Firefox policy install writes the stock policies"
|
||||
pass "Firefox policy install does not follow a planted policies.json symlink"
|
||||
|
||||
dir_dist=$test_tmp/distribution-dir
|
||||
mkdir -p "$dir_dist"
|
||||
mkdir "$dir_dist/policies.json"
|
||||
as_root() { unprivileged_as_root "$@"; }
|
||||
if browser_policy_install_firefox_policies "$dir_dist" 2>/dev/null; then
|
||||
fail "Firefox policy install refuses a planted policies.json directory"
|
||||
fi
|
||||
[[ -d $dir_dist/policies.json ]] || fail "Firefox policy install leaves a planted policies.json directory in place"
|
||||
pass "Firefox policy install does not write into a planted policies.json directory"
|
||||
|
||||
grep -F 'exit "$failed"' "$ROOT/bin/omarchy-theme-set-browser" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser exits non-zero when a policy write fails"
|
||||
pass "omarchy-theme-set-browser exits non-zero when a policy write fails"
|
||||
|
||||
# Bash 5.3 adopts the EXIT trap's last status as the script's exit status, so a
|
||||
# handler ending on a false test turns a clean run into a failure and aborts the
|
||||
# migration that calls this through omarchy-theme-set-browser.
|
||||
policy_cleanup=$(sed -n '/^cleanup() {/,/^}/p' "$ROOT/bin/omarchy-theme-set-browser-policy")
|
||||
[[ -n $policy_cleanup ]] || fail "omarchy-theme-set-browser-policy defines an EXIT cleanup handler"
|
||||
eval "$policy_cleanup"
|
||||
staged=""
|
||||
cleanup || fail "omarchy-theme-set-browser-policy's EXIT trap succeeds with nothing staged"
|
||||
staged=$test_tmp/staged-policy
|
||||
: >"$staged"
|
||||
cleanup || fail "omarchy-theme-set-browser-policy's EXIT trap succeeds with a staged file"
|
||||
[[ ! -e $staged ]] || fail "omarchy-theme-set-browser-policy's EXIT trap removes the staged file"
|
||||
unset -f cleanup
|
||||
pass "omarchy-theme-set-browser-policy's EXIT trap never leaks a failure status"
|
||||
|
||||
grep -F 'omarchy-theme-set-browser || true' "$ROOT/migrations/1787515927.sh" >/dev/null ||
|
||||
fail "the policy-directory migration hardens Firefox even when the theme refresh fails"
|
||||
pass "the policy-directory migration does not abort on a failed theme refresh"
|
||||
|
||||
policy_files=(
|
||||
"$ROOT/bin/omarchy-install-browser"
|
||||
"$ROOT/bin/omarchy-provision-owner"
|
||||
"$ROOT/bin/omarchy-theme-set-browser"
|
||||
"$ROOT/bin/omarchy-theme-set-browser-policy"
|
||||
"$ROOT/bin/omarchy-upgrade-to-quattro"
|
||||
"$ROOT/install/config/theme-system.sh"
|
||||
"$ROOT/install/config/browser-policy.sh"
|
||||
"$ROOT/install/helpers/browser-policy.sh"
|
||||
"$ROOT/migrations/1787515927.sh"
|
||||
)
|
||||
if grep -nE 'chmod a\+rwx\b|chmod a\+rw\b|chmod a\+w\b|chmod o\+w|chmod ugo\+w|chmod 2775\b|chmod 2777\b|chmod 0777\b|chmod 777\b|install -d -m 0?[27]?777|omarchy-browser-policy' "${policy_files[@]}" >/dev/null; then
|
||||
fail "browser policy setup is not world-writable and does not use omarchy-browser-policy"
|
||||
fi
|
||||
pass "browser policy setup is not world-writable"
|
||||
|
||||
mapfile -t migrations < <(rg -l 'Stop world-writable Chromium and Firefox policy directories' "$ROOT/migrations")
|
||||
(( ${#migrations[@]} == 1 )) || fail "exactly one migration locks existing policy directories" "${migrations[*]}"
|
||||
grep -F 'browser_policy_setup_dir' "${migrations[0]}" >/dev/null ||
|
||||
fail "the policy-directory migration repairs managed directories"
|
||||
if grep -F 'browser_policy_grant_user' "${migrations[0]}" >/dev/null; then
|
||||
fail "the policy-directory migration does not grant a browser-policy group"
|
||||
fi
|
||||
grep -F 'BROWSER_POLICY_FIREFOX_DIRS' "${migrations[0]}" >/dev/null ||
|
||||
fail "the policy-directory migration covers Firefox and Zen"
|
||||
grep -F 'browser_policy_firefox_policy_file_ok' "${migrations[0]}" >/dev/null ||
|
||||
fail "the policy-directory migration keeps a trusted Firefox policies.json"
|
||||
grep -F '/opt/zen-browser/distribution' "$ROOT/install/helpers/browser-policy.sh" >/dev/null ||
|
||||
fail "the shared helper names the Zen distribution directory"
|
||||
pass "a migration locks existing policy directories"
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
helper="$ROOT/bin/omarchy-theme-set-browser-policy"
|
||||
setter="$ROOT/bin/omarchy-theme-set-browser"
|
||||
sudoers_file="$ROOT/etc/sudoers.d/omarchy-theme-browser"
|
||||
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]'
|
||||
|
||||
# Exactly one rule, matched whole. Dropping the argument -- which sudoers reads
|
||||
# as "any arguments" -- or widening the glob to `*` would let the grant carry
|
||||
# something other than a color while leaving this line looking right.
|
||||
rules=$(grep -vE '^[[:space:]]*(#|$)' "$sudoers_file")
|
||||
[[ $rules == "$rule" ]] ||
|
||||
fail "browser policy sudoers file carries exactly the six-hex-digit rule and nothing else" "got: $rules"
|
||||
|
||||
if command -v visudo >/dev/null; then
|
||||
visudo -cf "$sudoers_file" >/dev/null || fail "browser policy sudoers rule parses"
|
||||
fi
|
||||
|
||||
grep -Fx 'PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy' "$helper" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser-policy elevates the path the sudoers rule names"
|
||||
|
||||
grep -E 'sudo -n -l -l' "$helper" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser-policy reads the grant from the long sudo listing"
|
||||
|
||||
grep -Eq '^\s*export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin' "$helper" ||
|
||||
fail "omarchy-theme-set-browser-policy pins PATH to trusted system directories when it holds root"
|
||||
gated=$(grep -A1 -E '^if \(\( EUID == 0 \)\); then$' "$helper" || true)
|
||||
[[ $gated == *"export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin"* ]] ||
|
||||
fail "omarchy-theme-set-browser-policy gates the trusted-PATH pin on holding root"
|
||||
|
||||
pass "browser policy sudoers rule is scoped to a single color argument"
|
||||
|
||||
for dir in /etc/chromium/policies/managed /etc/opt/chrome/policies/managed \
|
||||
/etc/opt/edge/policies/managed /etc/brave/policies/managed; do
|
||||
grep -Fx " $dir" "$helper" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser-policy names $dir in its fixed policy directory list"
|
||||
done
|
||||
|
||||
policy_dir_count=$(sed -n '/^POLICY_DIRS=(/,/^)/p' "$helper" | grep -c '^ /')
|
||||
((policy_dir_count == 4)) ||
|
||||
fail "omarchy-theme-set-browser-policy writes only the four known policy directories" \
|
||||
"got: $policy_dir_count"
|
||||
|
||||
grep -F 'install -m 0644 -o root -g root -T' "$helper" >/dev/null ||
|
||||
fail "omarchy-theme-set-browser-policy installs color.json with install -T"
|
||||
if grep -E 'mv -f' "$helper" >/dev/null; then
|
||||
fail "omarchy-theme-set-browser-policy does not mv into a planted color.json directory"
|
||||
fi
|
||||
|
||||
pass "browser policy helper writes a fixed set of policy directories"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/pkexec" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'pkexec %s\n' "$*" >"$ELEVATION_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/pkexec"
|
||||
|
||||
# STUB_GRANTED empty stands for an install whose omarchy-settings predates the
|
||||
# sudoers file. The default is granted, matching a current Omarchy.
|
||||
cat >"$stub_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $1 == -n && $2 == -l ]]; then
|
||||
if [[ ${STUB_GRANTED-granted} == "granted" ]]; then
|
||||
echo " Options: !authenticate"
|
||||
else
|
||||
echo " Matched: ${!#}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
printf 'sudo %s\n' "$*" >"$ELEVATION_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/sudo"
|
||||
|
||||
if ((EUID == 0)); then
|
||||
pass "running as root; skipping the elevation checks, which would rewrite this machine's browser policy"
|
||||
else
|
||||
elevation_for() {
|
||||
: >"$test_tmp/elevation"
|
||||
ELEVATION_LOG="$test_tmp/elevation" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
bash "$helper" "$@" </dev/null >/dev/null 2>&1 || true
|
||||
cat "$test_tmp/elevation"
|
||||
}
|
||||
|
||||
elevation=$(elevation_for 1c2027)
|
||||
[[ $elevation == "sudo /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] ||
|
||||
fail "omarchy-theme-set-browser-policy takes the passwordless sudo grant without a terminal" \
|
||||
"got: $elevation"
|
||||
|
||||
dev_linked=$(OMARCHY_PATH="$test_tmp/checkout" elevation_for 1c2027)
|
||||
[[ $dev_linked == "sudo /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] ||
|
||||
fail "omarchy-theme-set-browser-policy elevates the system install wherever OMARCHY_PATH points" \
|
||||
"got: $dev_linked"
|
||||
|
||||
pass "browser policy helper elevates a valid color through the sudo grant"
|
||||
|
||||
ungranted=$(STUB_GRANTED="" elevation_for 1c2027)
|
||||
[[ $ungranted == "pkexec /usr/bin/omarchy-theme-set-browser-policy 1c2027" ]] ||
|
||||
fail "omarchy-theme-set-browser-policy falls back to polkit where the grant does not reach" \
|
||||
"got: $ungranted"
|
||||
|
||||
pass "browser policy helper falls back to polkit wherever the grant does not reach"
|
||||
|
||||
for bad in "" "1C2027" "abc12" "abc1234" "1c202g" "../../etc/passwd" "1c2027 1c2027" \
|
||||
'$(id)' "1c2027;id" "#1c2027"; do
|
||||
if PATH="$stub_bin:$PATH" ELEVATION_LOG="$test_tmp/elevation" \
|
||||
bash "$helper" "$bad" </dev/null >/dev/null 2>&1; then
|
||||
fail "omarchy-theme-set-browser-policy rejects '$bad'"
|
||||
fi
|
||||
|
||||
rejected=$(elevation_for "$bad")
|
||||
[[ -z $rejected ]] ||
|
||||
fail "omarchy-theme-set-browser-policy rejects '$bad' before elevating" "got: $rejected"
|
||||
done
|
||||
|
||||
if PATH="$stub_bin:$PATH" bash "$helper" 1c2027 ffffff </dev/null >/dev/null 2>&1; then
|
||||
fail "omarchy-theme-set-browser-policy rejects more than one argument"
|
||||
fi
|
||||
|
||||
pass "browser policy helper accepts nothing but six lowercase hex digits"
|
||||
fi
|
||||
|
||||
setter_bin="$test_tmp/setter-bin"
|
||||
mkdir -p "$setter_bin"
|
||||
|
||||
cat >"$setter_bin/omarchy-theme-set-browser-policy" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >"$COLOR_LOG"
|
||||
SH
|
||||
chmod +x "$setter_bin/omarchy-theme-set-browser-policy"
|
||||
|
||||
cat >"$setter_bin/omarchy-cmd-present" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$setter_bin/omarchy-cmd-present"
|
||||
|
||||
setter_home="$test_tmp/home"
|
||||
theme_dir="$setter_home/.local/state/omarchy/current/theme"
|
||||
mkdir -p "$theme_dir"
|
||||
|
||||
color_for_theme() {
|
||||
: >"$test_tmp/color"
|
||||
if [[ $# -gt 0 ]]; then
|
||||
printf '%s' "$1" >"$theme_dir/chromium.theme"
|
||||
else
|
||||
rm -f "$theme_dir/chromium.theme"
|
||||
fi
|
||||
|
||||
HOME="$setter_home" COLOR_LOG="$test_tmp/color" PATH="$setter_bin:$stub_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" bash "$setter" </dev/null >/dev/null 2>&1 || true
|
||||
cat "$test_tmp/color"
|
||||
}
|
||||
|
||||
[[ $(color_for_theme "242,240,229") == "f2f0e5" ]] ||
|
||||
fail "omarchy-theme-set-browser converts an RGB triple to six hex digits"
|
||||
[[ $(color_for_theme $'14,31,41\n') == "0e1f29" ]] ||
|
||||
fail "omarchy-theme-set-browser accepts a trailing newline"
|
||||
[[ $(color_for_theme "0,0,0") == "000000" ]] ||
|
||||
fail "omarchy-theme-set-browser pads single-digit components"
|
||||
[[ $(color_for_theme " 12 , 11 , 12 ") == "0c0b0c" ]] ||
|
||||
fail "omarchy-theme-set-browser tolerates surrounding whitespace"
|
||||
|
||||
for malformed in "" "not,a,color" "1,2" "1,2,3,4" "256,0,0" "999,999,999" "-1,0,0" \
|
||||
"1,2,3;id" '1,2,$(id)' "0x10,0,0" "1,2,3 4,5,6"; do
|
||||
color=$(color_for_theme "$malformed")
|
||||
[[ $color == "1c2027" ]] ||
|
||||
fail "omarchy-theme-set-browser falls back to the stock colour for '$malformed'" "got: $color"
|
||||
done
|
||||
|
||||
[[ $(color_for_theme) == "1c2027" ]] ||
|
||||
fail "omarchy-theme-set-browser falls back to the stock colour with no theme file"
|
||||
|
||||
pass "browser theme color is derived as six hex digits or falls back to the stock grey"
|
||||
@@ -28,8 +28,18 @@ write_stale_preferences() {
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
REAL_PYTHON=$(command -v python3)
|
||||
cat >"$stub_bin/python3" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 127
|
||||
STUB
|
||||
chmod +x "$stub_bin/python3"
|
||||
|
||||
# Test stubs must delegate to the system interpreter, not a user shim that can
|
||||
# route python3 back through the stubs and recurse.
|
||||
REAL_PYTHON=$(PATH="$stub_bin:$PATH" command -p -v python3)
|
||||
[[ $REAL_PYTHON != "$stub_bin/python3" ]] || fail "real Python resolution bypasses user shims"
|
||||
export REAL_PYTHON
|
||||
rm -f "$stub_bin/python3"
|
||||
|
||||
run_migration() {
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1
|
||||
|
||||
@@ -54,6 +54,334 @@ grep -F 'omarchy-crash-watch.service' "$ROOT/install/user/first-run/enable-user-
|
||||
fail "crash capture is no longer on by default for new installs"
|
||||
pass "crash capture is on by default"
|
||||
|
||||
require_command jq
|
||||
|
||||
# The per-program mute, driven through the real watcher with a stubbed journal:
|
||||
# these prove what a person sees -- a toast arriving or not -- where asserting
|
||||
# that a flag file was read would prove only that a flag file was read.
|
||||
watch_bin="$TMPDIR/watch-bin"
|
||||
watch_home="$TMPDIR/watch-home"
|
||||
NOTIFY_LOG="$TMPDIR/notify-log"
|
||||
JOURNAL_ENTRIES="$TMPDIR/journal-entries"
|
||||
|
||||
mkdir -p "$watch_bin" "$watch_home"
|
||||
|
||||
cat >"$watch_bin/journalctl" <<'SH'
|
||||
#!/bin/bash
|
||||
cat "$JOURNAL_ENTRIES"
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-default-agent" <<'SH'
|
||||
#!/bin/bash
|
||||
echo claude
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-notification-wait" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$NOTIFY_LOG"
|
||||
SH
|
||||
|
||||
chmod +x "$watch_bin/journalctl" "$watch_bin/omarchy-default-agent" \
|
||||
"$watch_bin/omarchy-notification-wait" "$watch_bin/omarchy-notification-send"
|
||||
|
||||
reset_entries() {
|
||||
: >"$JOURNAL_ENTRIES"
|
||||
}
|
||||
|
||||
# One core dump as systemd-coredump journals it. The UID must be this user's, or
|
||||
# the watcher discards it as somebody else's crash before anything under test.
|
||||
crash_entry() {
|
||||
local comm="$1" exe="$2"
|
||||
|
||||
jq -cn --arg uid "$UID" --arg comm "$comm" --arg exe "$exe" \
|
||||
'{_UID: $uid, COREDUMP_COMM: $comm, COREDUMP_PID: "4242",
|
||||
COREDUMP_EXE: $exe, COREDUMP_SIGNAL_NAME: "SIGSEGV"}' >>"$JOURNAL_ENTRIES"
|
||||
}
|
||||
|
||||
# The stubbed journalctl ends after the entries, so the watcher's loop ends too.
|
||||
# Its exit status is asserted rather than discarded: a watcher that dies on a
|
||||
# muted crash notifies about nothing afterwards, which every assertion below
|
||||
# that expects silence would otherwise read as success.
|
||||
run_watch() {
|
||||
local status=0
|
||||
|
||||
: >"$NOTIFY_LOG"
|
||||
|
||||
PATH="$watch_bin:$ROOT/bin:$PATH" \
|
||||
JOURNAL_ENTRIES="$JOURNAL_ENTRIES" \
|
||||
NOTIFY_LOG="$NOTIFY_LOG" \
|
||||
HOME="$watch_home" \
|
||||
"$ROOT/bin/omarchy-crash-watch" || status=$?
|
||||
|
||||
(( status == 0 )) ||
|
||||
fail "the watcher exited $status rather than carrying on, so a mute takes the service down with it"
|
||||
}
|
||||
|
||||
# Through the real command rather than writing the flag by hand: these assertions
|
||||
# are then the guard that the thing the diagnosis runs and the thing the watcher
|
||||
# reads have not drifted apart.
|
||||
mute() {
|
||||
HOME="$watch_home" PATH="$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" "$1" "$2" >/dev/null
|
||||
}
|
||||
|
||||
announced() {
|
||||
grep -Fq "Process crashed: $1" "$NOTIFY_LOG"
|
||||
}
|
||||
|
||||
reset_entries
|
||||
crash_entry hyprland /usr/bin/hyprland
|
||||
run_watch
|
||||
announced hyprland ||
|
||||
fail "a crash nobody muted still announces itself"
|
||||
pass "a crash nobody muted still announces itself"
|
||||
|
||||
mute hyprland on
|
||||
run_watch
|
||||
! announced hyprland ||
|
||||
fail "muting a program stops the crash notifications the diagnosis offered to stop"
|
||||
pass "muting a program stops its crash notifications"
|
||||
|
||||
reset_entries
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced nautilus ||
|
||||
fail "muting one program silences every other program, which is the global toggle's job and not this one's"
|
||||
pass "muting one program leaves every other program announcing"
|
||||
|
||||
mute hyprland off
|
||||
reset_entries
|
||||
crash_entry hyprland /usr/bin/hyprland
|
||||
run_watch
|
||||
announced hyprland ||
|
||||
fail "un-muting a program brings its crash notifications back"
|
||||
pass "un-muting a program brings its crash notifications back"
|
||||
|
||||
# The diagnosis tells the user to mute the name the toast showed them, so the
|
||||
# toast has to show the name the watcher checks. COMM is truncated to 15
|
||||
# characters and the executable's basename is not, and announcing the truncated
|
||||
# one would leave a dutifully-followed mute matching nothing forever.
|
||||
reset_entries
|
||||
crash_entry chromium-browse /usr/lib/chromium/chromium-browser
|
||||
run_watch
|
||||
announced chromium-browser ||
|
||||
fail "the toast announces a name the mute cannot be keyed on, so following the diagnosis mutes nothing"
|
||||
pass "the toast announces the name the mute is keyed on"
|
||||
|
||||
mute chromium-browser on
|
||||
run_watch
|
||||
! announced chromium-browser ||
|
||||
fail "the mute is keyed on the name the notification announced, not on the truncated COMM"
|
||||
pass "muting the announced name silences a program whose COMM was truncated"
|
||||
|
||||
# A muted crash must not end the watcher. Restart=always would paper over it
|
||||
# with a five-second gap, and the watcher restarts on `journalctl -n 0`, which
|
||||
# never replays the crashes it missed while it was away.
|
||||
reset_entries
|
||||
crash_entry chromium-browse /usr/lib/chromium/chromium-browser
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced nautilus ||
|
||||
fail "a muted crash stops the watcher reading the journal, losing every crash after it"
|
||||
pass "a muted crash does not stop the watcher reading the next one"
|
||||
|
||||
# A process can set its own comm to anything prctl takes, slashes included, and
|
||||
# a crash with no recorded executable falls back to it. A name that climbed out
|
||||
# of crash-ignore/ would let a crashing program silence itself against an
|
||||
# unrelated flag -- and have the diagnosis write one there on the user's behalf.
|
||||
# The fixture carries two slashes so that dropping only the first is not mistaken
|
||||
# for dropping all of them.
|
||||
reset_entries
|
||||
crash_entry a/../bar-off -
|
||||
sibling_flag="$watch_home/.local/state/omarchy/toggles/bar-off"
|
||||
touch "$sibling_flag"
|
||||
run_watch
|
||||
announced bar-off ||
|
||||
fail "a comm that climbs out of crash-ignore/ reads an unrelated toggle, letting a crash suppress its own notification"
|
||||
pass "a comm that climbs out of crash-ignore/ cannot reach an unrelated toggle"
|
||||
rm -f "$sibling_flag"
|
||||
|
||||
# Stripping to the last component does not always leave a component. An empty
|
||||
# name is no kind of array subscript and no kind of toast, and a dot component
|
||||
# names a directory the mute would touch and then never match.
|
||||
for empty_comm in / a/ . ..; do
|
||||
reset_entries
|
||||
crash_entry "$empty_comm" -
|
||||
run_watch
|
||||
announced unknown ||
|
||||
fail "a comm of '$empty_comm' leaves no usable name, so the toast cannot say what crashed and the mute has nothing to key on"
|
||||
done
|
||||
pass "a comm that strips down to nothing or a dot still announces under a name a mute can use"
|
||||
|
||||
# An empty comm is not a missing entry. Tab is IFS whitespace, so an empty field
|
||||
# collapses and every field after it shifts along one -- the pid becomes a path,
|
||||
# the crash reads as somebody else's, and it is dropped without a word.
|
||||
reset_entries
|
||||
crash_entry "" -
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced unknown ||
|
||||
fail "a crash whose comm is empty is dropped instead of announced, because the empty field shifted every field after it"
|
||||
announced nautilus ||
|
||||
fail "an empty comm derails the rest of the journal entry"
|
||||
pass "an empty comm is announced rather than parsed into the next field"
|
||||
|
||||
# Only "." and ".." are special. A leading dot is an ordinary filename, and
|
||||
# folding those into the fallback would have one program's mute silence another.
|
||||
for dotted_comm in .hidden ...; do
|
||||
reset_entries
|
||||
crash_entry "$dotted_comm" -
|
||||
run_watch
|
||||
announced "$dotted_comm" ||
|
||||
fail "'$dotted_comm' is an ordinary name, but it lands in the fallback, so muting it would silence unrelated crashes"
|
||||
done
|
||||
pass "a leading dot is an ordinary name rather than a special component"
|
||||
|
||||
# And the name it settles on is mutable like any other.
|
||||
mute unknown on
|
||||
reset_entries
|
||||
crash_entry / -
|
||||
run_watch
|
||||
! announced unknown ||
|
||||
fail "the fallback name cannot be muted, so the one crash most likely to repeat is the one that cannot be silenced"
|
||||
pass "the fallback name can be muted like any other"
|
||||
mute unknown off
|
||||
|
||||
# What omarchy-crash-mute does on its own. That it agrees with the watcher is
|
||||
# already covered above, which drives it for every mute it makes.
|
||||
mute_home="$TMPDIR/mute-home"
|
||||
mkdir -p "$mute_home"
|
||||
|
||||
crash_mute() {
|
||||
HOME="$mute_home" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-crash-mute" "$@"
|
||||
}
|
||||
|
||||
mute_flag() {
|
||||
[[ $1 == "--" ]] && shift
|
||||
printf '%s' "$mute_home/.local/state/omarchy/toggles/crash-ignore/$1"
|
||||
}
|
||||
|
||||
crash_mute | grep -Fq "No programs muted" ||
|
||||
fail "an empty mute list prints nothing, so a user cannot tell it from a broken command"
|
||||
pass "the command says so when nothing is muted"
|
||||
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute | grep -Fqx hyprland ||
|
||||
fail "a muted program is missing from the list, so a mute cannot be found again to lift it"
|
||||
pass "the command lists what it muted"
|
||||
|
||||
# The watcher keys on the basename, so the command has to take the path a crash
|
||||
# recorded and land on the same flag the watcher will look for.
|
||||
crash_mute /usr/lib/chromium/chromium-browser >/dev/null
|
||||
[[ -f $(mute_flag chromium-browser) ]] ||
|
||||
fail "a binary's path is muted verbatim rather than by name, so the watcher never sees that flag"
|
||||
pass "the command reduces a path to the name the watcher checks"
|
||||
|
||||
crash_mute hyprland off >/dev/null
|
||||
[[ ! -f $(mute_flag hyprland) ]] ||
|
||||
fail "off leaves the program muted, making the mute a one-way door"
|
||||
pass "the command un-mutes"
|
||||
|
||||
# Muting is not flipping. The diagnosis offers this on a program the user may
|
||||
# already have muted, and asking for a mute twice has to leave it muted.
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute hyprland >/dev/null
|
||||
[[ -f $(mute_flag hyprland) ]] ||
|
||||
fail "muting an already-muted program un-mutes it, so offering the mute a second time turns it back on"
|
||||
pass "asking to mute twice leaves it muted"
|
||||
|
||||
# A program may legitimately be called .hidden, and a mute nobody can see is a
|
||||
# mute nobody can lift.
|
||||
crash_mute .hidden >/dev/null
|
||||
crash_mute | grep -Fqx .hidden ||
|
||||
fail "a mute on a dotted name is missing from the list, so it can never be found and lifted"
|
||||
pass "the list shows a name that begins with a dot"
|
||||
|
||||
# It turns what it is given into a path, so it has to refuse whatever is not one
|
||||
# component of one.
|
||||
for bad_name in . .. /; do
|
||||
! crash_mute "$bad_name" >/dev/null 2>&1 ||
|
||||
fail "'$bad_name' is taken as a program name, and the flag that writes is not one the watcher will ever read"
|
||||
done
|
||||
pass "the command refuses a name that is not a name"
|
||||
|
||||
! crash_mute hyprland sideways >/dev/null 2>&1 ||
|
||||
fail "an action it does not know is treated as a mute, so a typo silences a program"
|
||||
pass "the command refuses an action it does not know"
|
||||
|
||||
# And says what it refused, or the user retypes the same thing. Captured rather
|
||||
# than piped: the command exits non-zero here, which pipefail would surface as
|
||||
# the pipeline's status and read as a failed assertion.
|
||||
refusal=$(crash_mute hyprland sideways 2>&1) || true
|
||||
grep -Fq "Not an action" <<<"$refusal" ||
|
||||
fail "an unknown action is refused without naming it, leaving the user nothing to correct"
|
||||
pass "the command names the action it refused"
|
||||
|
||||
crash_mute ../bar-off >/dev/null
|
||||
[[ ! -e "$mute_home/.local/state/omarchy/toggles/bar-off" ]] ||
|
||||
fail "a name that climbs out writes a sibling toggle, so muting a crash could turn off the bar instead"
|
||||
pass "the command cannot be talked into writing outside crash-ignore/"
|
||||
|
||||
# A program may be called -h, and the router answers that with its own help
|
||||
# before the command runs. A leading -- is the way through, so it has to be
|
||||
# consumed rather than taken for the program name.
|
||||
crash_mute -- -h >/dev/null 2>&1 ||
|
||||
fail "a leading -- is refused rather than consumed, so a program named like a flag cannot be muted at all"
|
||||
[[ -f $(mute_flag -- -h) ]] ||
|
||||
fail "a leading -- is taken for the program name, so muting -h mutes something else"
|
||||
pass "a leading -- lets a program named like a flag be muted"
|
||||
|
||||
# toggle is advertised, so it has to flip both ways rather than quietly mute.
|
||||
crash_mute toggler off >/dev/null
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle does not mute an un-muted program"
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ ! -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle mutes but never un-mutes, so the advertised action only goes one way"
|
||||
pass "toggle flips a mute both ways"
|
||||
|
||||
# The listing means what the watcher means, and the watcher honours a regular
|
||||
# file. Anything else in there is not a mute, however much it looks like one.
|
||||
mkdir -p "$(mute_flag notactuallymuted)"
|
||||
! crash_mute | grep -Fqx notactuallymuted ||
|
||||
fail "a directory is reported as muted while that program's crashes keep arriving"
|
||||
pass "the listing counts only the flags the watcher honours"
|
||||
rmdir "$(mute_flag notactuallymuted)"
|
||||
|
||||
# A mute that could not be written must not be reported as one. Without this the
|
||||
# command can print success for a flag that was never created.
|
||||
failing_bin="$TMPDIR/failing-bin"
|
||||
mkdir -p "$failing_bin"
|
||||
cat >"$failing_bin/omarchy-toggle" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$failing_bin/omarchy-toggle"
|
||||
|
||||
status=0
|
||||
refusal=$(HOME="$mute_home" PATH="$failing_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" hyprland 2>&1) || status=$?
|
||||
(( status != 0 )) ||
|
||||
fail "a mute that could not be written exits zero, so nothing downstream learns it failed"
|
||||
! grep -Fq "Muted crash notifications" <<<"$refusal" ||
|
||||
fail "a mute that could not be written still reports success, so the user believes a program is silenced when it is not"
|
||||
pass "a mute that could not be written is not reported as one"
|
||||
|
||||
skill="$ROOT/default/agents/skills/diagnose-crash/SKILL.md"
|
||||
grep -Fq 'omarchy-crash-mute' "$skill" ||
|
||||
fail "the diagnosis no longer names the command that mutes, so the offer it makes cannot be carried out"
|
||||
pass "the diagnosis names the command that mutes"
|
||||
|
||||
grep -Fq 'GROUP_DESCRIPTIONS[crash]' "$ROOT/bin/omarchy" ||
|
||||
fail "the crash group has no description, so the router lists a group it cannot describe"
|
||||
pass "the crash group is described in the router"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
packages="$ROOT/install/omarchy-base.packages"
|
||||
cups_browsed_conf="$ROOT/etc/cups/cups-browsed.conf"
|
||||
cups_files_conf="$ROOT/etc/cups/cups-files.conf"
|
||||
sysusers_conf="$ROOT/etc/sysusers.d/omarchy-cups-browsed.conf"
|
||||
service_dropin="$ROOT/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf"
|
||||
|
||||
grep -qxF cups-browsed "$packages" || fail "cups-browsed remains in the base package set"
|
||||
grep -qxF cups-pk-helper "$packages" || fail "Polkit printer administration is installed"
|
||||
! grep -qxF cups-pdf "$packages" || fail "the root CUPS-PDF backend is removed"
|
||||
|
||||
pass "the base install keeps discovery and replaces CUPS-PDF with Polkit administration"
|
||||
|
||||
grep -qxF 'CacheDir /var/cache/cups-browsed' "$cups_browsed_conf" ||
|
||||
fail "cups-browsed keeps state outside the print-filter cache"
|
||||
grep -qxF 'CreateIPPPrinterQueues Driverless' "$cups_browsed_conf" ||
|
||||
fail "automatic queues are limited to driverless IPP printers"
|
||||
grep -qxF 'CreateRemoteCUPSPrinterQueues No' "$cups_browsed_conf" ||
|
||||
fail "remote CUPS queues are not created automatically"
|
||||
! grep -q 'CreateRemotePrinters' "$cups_browsed_conf" ||
|
||||
fail "the unsupported CreateRemotePrinters directive is gone"
|
||||
|
||||
pass "cups-browsed uses explicit supported discovery policy and an isolated cache"
|
||||
|
||||
grep -qxF 'SystemGroup cups-browsed sys root' "$cups_files_conf" ||
|
||||
fail "only the printer discovery account receives passwordless CUPS administration"
|
||||
grep -qxF 'PeerCred on' "$cups_files_conf" ||
|
||||
fail "the packaged CUPS policy enables peer credentials"
|
||||
[[ $(grep -ciE '^[[:space:]]*SystemGroup[[:space:]]' "$cups_files_conf") == 1 ]] ||
|
||||
fail "the packaged CUPS policy has one SystemGroup directive"
|
||||
[[ $(grep -ciE '^[[:space:]]*PeerCred[[:space:]]' "$cups_files_conf") == 1 ]] ||
|
||||
fail "the packaged CUPS policy has one PeerCred directive"
|
||||
[[ ! -e $ROOT/install/config/printing.sh ]] ||
|
||||
fail "printing policy is not rewritten by an install script"
|
||||
! grep -q 'config/printing.sh' "$ROOT/install/config/all.sh" "$ROOT/migrations/1787815267.sh" ||
|
||||
fail "neither install nor update invokes a printing rewrite script"
|
||||
|
||||
pass "CUPS authorization ships as a canonical package override"
|
||||
|
||||
grep -qxF 'u cups-browsed - "CUPS printer discovery" / -' "$sysusers_conf" ||
|
||||
fail "a locked cups-browsed system account is declared"
|
||||
|
||||
for setting in \
|
||||
'User=cups-browsed' \
|
||||
'Group=cups-browsed' \
|
||||
'CacheDirectory=cups-browsed' \
|
||||
'CacheDirectoryMode=0750' \
|
||||
'UMask=0027' \
|
||||
'NoNewPrivileges=yes' \
|
||||
'ProtectSystem=strict' \
|
||||
'ProtectHome=yes' \
|
||||
'PrivateTmp=yes' \
|
||||
'RestrictSUIDSGID=yes'; do
|
||||
grep -qxF "$setting" "$service_dropin" ||
|
||||
fail "cups-browsed service hardening includes $setting"
|
||||
done
|
||||
|
||||
! grep -q '^\(Ambient\|CapabilityBoundingSet\).*CAP_NET_BIND_SERVICE' "$service_dropin" ||
|
||||
fail "cups-browsed is not granted an unverified network capability"
|
||||
|
||||
pass "cups-browsed runs as its confined service account without added capabilities"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
mkdir -p "$mock_bin" "$test_tmp/var/lib/omarchy/migrations"
|
||||
|
||||
passwd_db="$test_tmp/passwd"
|
||||
group_db="$test_tmp/group"
|
||||
touch "$passwd_db" "$group_db"
|
||||
|
||||
cat >"$mock_bin/getent" <<'SH'
|
||||
#!/bin/bash
|
||||
case "$1" in
|
||||
passwd) database="$OMARCHY_CUPS_TEST_PASSWD" ;;
|
||||
group) database="$OMARCHY_CUPS_TEST_GROUP" ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
|
||||
if (($# == 1)); then
|
||||
cat "$database"
|
||||
else
|
||||
awk -F: -v name="$2" '$1 == name { print; found = 1 } END { exit !found }' "$database"
|
||||
fi
|
||||
SH
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == "cups" || $1 == "cups-browsed" ]]
|
||||
SH
|
||||
for command in omarchy-pkg-add omarchy-pkg-drop; do
|
||||
cat >"$mock_bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\t%s\n' "${0##*/}" "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
SH
|
||||
done
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
exit 0
|
||||
SH
|
||||
cat >"$mock_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'sudo\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
exec "$@"
|
||||
SH
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
log="$test_tmp/actions.log"
|
||||
touch "$log"
|
||||
export OMARCHY_CUPS_TEST_LOG="$log"
|
||||
export OMARCHY_CUPS_TEST_PASSWD="$passwd_db"
|
||||
export OMARCHY_CUPS_TEST_GROUP="$group_db"
|
||||
|
||||
printf 'cups-browsed:x:1000:1000:Desktop user:/home/cups-browsed:/usr/bin/bash\n' >"$passwd_db"
|
||||
printf 'cups-browsed:x:1000:\n' >"$group_db"
|
||||
if PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/desktop-collision-marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then
|
||||
fail "the migration accepts an existing desktop user named cups-browsed"
|
||||
fi
|
||||
[[ ! -s $log ]] || fail "an account collision stops the migration before changing the system"
|
||||
|
||||
printf 'alice:x:1000:947:Desktop user:/home/alice:/usr/bin/bash\n' >"$passwd_db"
|
||||
printf 'cups-browsed:x:947:alice\n' >"$group_db"
|
||||
if PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/group-collision-marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then
|
||||
fail "the migration accepts an existing cups-browsed group with members"
|
||||
fi
|
||||
[[ ! -s $log ]] || fail "a group collision stops the migration before changing the system"
|
||||
|
||||
printf 'cups-browsed:x:947:947:CUPS printer discovery:/:/usr/bin/nologin\n' >"$passwd_db"
|
||||
printf 'cups-browsed:x:947:\n' >"$group_db"
|
||||
|
||||
pass "the migration rejects account and group collisions before changing printing"
|
||||
|
||||
marker="$test_tmp/var/lib/omarchy/migrations/1787815267"
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
|
||||
grep -qxF $'omarchy-pkg-drop\tcups-pdf' "$log" ||
|
||||
fail "the migration removes CUPS-PDF"
|
||||
grep -qxF $'omarchy-pkg-add\tcups-pk-helper' "$log" ||
|
||||
fail "the migration installs authenticated printer administration"
|
||||
grep -qxF $'systemctl\tstop cups-browsed.service' "$log" ||
|
||||
fail "the migration stops the root cups-browsed process before reconfiguration"
|
||||
grep -qxF $'systemctl\tdaemon-reload' "$log" ||
|
||||
fail "the migration reloads the hardened service"
|
||||
grep -qxF $'systemctl\ttry-reload-or-restart cups.service' "$log" ||
|
||||
fail "the migration reloads the packaged CUPS authorization"
|
||||
grep -qxF $'systemctl\trestart cups-browsed.service' "$log" ||
|
||||
fail "the migration resumes an active cups-browsed service"
|
||||
[[ -f $marker ]] || fail "the migration records machine-wide completion"
|
||||
|
||||
actions_after_first_run=$(wc -l <"$log")
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
[[ $(wc -l <"$log") == "$actions_after_first_run" ]] ||
|
||||
fail "the machine-wide migration repeats privileged work"
|
||||
|
||||
pass "the migration safely converts an active existing installation once"
|
||||
|
||||
# An interrupted earlier run leaves cups-browsed stopped. A retry still needs
|
||||
# to resume an enabled service before recording completion.
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
[[ $1 == "is-active" ]] && exit 1
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin/systemctl"
|
||||
|
||||
retry_log="$test_tmp/retry.log"
|
||||
retry_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-retry"
|
||||
|
||||
OMARCHY_CUPS_TEST_LOG="$retry_log" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$retry_marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
|
||||
grep -qxF $'systemctl\trestart cups-browsed.service' "$retry_log" ||
|
||||
fail "the retry resumes cups-browsed after an interrupted earlier run"
|
||||
|
||||
pass "a run following an interrupted one still resumes printer discovery"
|
||||
|
||||
# A masked or disabled unit is deliberately left alone.
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
[[ $1 == "is-active" || $1 == "is-enabled" ]] && exit 1
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin/systemctl"
|
||||
|
||||
masked_log="$test_tmp/masked.log"
|
||||
masked_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-masked"
|
||||
|
||||
OMARCHY_CUPS_TEST_LOG="$masked_log" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$masked_marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
|
||||
! grep -qxF $'systemctl\trestart cups-browsed.service' "$masked_log" ||
|
||||
fail "the migration leaves a masked or disabled cups-browsed alone"
|
||||
[[ -f $masked_marker ]] || fail "the migration completes with cups-browsed masked"
|
||||
|
||||
pass "a masked or disabled cups-browsed is left alone and does not fail the migration"
|
||||
@@ -457,7 +457,7 @@ assert_bypass() {
|
||||
assert_launch pi pi "Review this project"
|
||||
assert_launch omp omp --auto-approve -- "Review this project"
|
||||
assert_launch opencode opencode --auto --prompt "Review this project"
|
||||
assert_launch ori ori code --prompt "Review this project"
|
||||
assert_launch ori ori code --interactive --prompt "Review this project"
|
||||
assert_launch claude claude --permission-mode auto -- "Review this project"
|
||||
assert_launch codex codex --approve-for-me -- "Review this project"
|
||||
assert_launch crush crush run "Review this project"
|
||||
|
||||
@@ -61,11 +61,13 @@ if [[ $installer == "omarchy-install-browser" && ${OMARCHY_TEST_REAL_BROWSER_INS
|
||||
fi
|
||||
|
||||
case $installer in
|
||||
omarchy-pkg-add)
|
||||
omarchy-pkg-add|omarchy-pkg-aur-add)
|
||||
package=$1
|
||||
printf 'pkg:%s\n' "$package" >>"$OMARCHY_TEST_INSTALL_LOG"
|
||||
case $package in
|
||||
chromium) command=chromium ;;
|
||||
firefox) command=firefox ;;
|
||||
zen-browser-bin) command=zen-browser ;;
|
||||
cursor-bin) command=cursor ;;
|
||||
sublime-text-4) command=sublime_text ;;
|
||||
vim) command=vim ;;
|
||||
@@ -107,6 +109,7 @@ SH
|
||||
|
||||
for installer in \
|
||||
omarchy-pkg-add \
|
||||
omarchy-pkg-aur-add \
|
||||
omarchy-install-browser \
|
||||
omarchy-install-terminal \
|
||||
omarchy-install-editor-vscode \
|
||||
@@ -205,10 +208,17 @@ OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install chromiu
|
||||
[[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium becomes the default after its full installer succeeds"
|
||||
cmp -s "$ROOT/config/chromium-flags.conf" "$test_home/.config/chromium-flags.conf" ||
|
||||
fail "Chromium browser installer copies the default flags"
|
||||
grep -Fxq 'sudo:mkdir -p /etc/chromium/policies/managed' "$setup_log" ||
|
||||
fail "Chromium browser installer creates its policy directory"
|
||||
grep -Fxq 'sudo:chmod a+rw /etc/chromium/policies/managed' "$setup_log" ||
|
||||
fail "Chromium browser installer makes its policy directory writable"
|
||||
grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium' "$setup_log" ||
|
||||
fail "Chromium browser installer creates a root-owned Chromium policy parent"
|
||||
grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium/policies' "$setup_log" ||
|
||||
fail "Chromium browser installer creates a root-owned Chromium policies parent"
|
||||
grep -Fxq 'sudo:install -d -m 0755 -o root -g root /etc/chromium/policies/managed' "$setup_log" ||
|
||||
fail "Chromium browser installer creates a root-owned managed policy directory"
|
||||
grep -Fxq 'sudo:find /etc/chromium/policies/managed -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" ||
|
||||
fail "Chromium browser installer drops non-root files from its policy directory"
|
||||
if grep -E 'groupadd|usermod|omarchy-browser-policy' "$setup_log" >/dev/null; then
|
||||
fail "Chromium browser installer does not create a browser-policy group" "$(cat "$setup_log")"
|
||||
fi
|
||||
grep -Fxq 'omarchy-install-chromium-copy-url:' "$setup_log" ||
|
||||
fail "Chromium browser installer registers the Copy URL host"
|
||||
grep -Fxq 'omarchy-install-chromium-ytdlp:' "$setup_log" ||
|
||||
@@ -217,6 +227,36 @@ grep -Fxq 'omarchy-theme-set-browser:' "$setup_log" ||
|
||||
fail "Chromium browser installer applies the current theme"
|
||||
pass "Chromium browser installer restores the complete Omarchy setup"
|
||||
|
||||
: >"$install_log"
|
||||
: >"$setup_log"
|
||||
rm -f "$installed_dir/firefox"
|
||||
OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install firefox >/dev/null
|
||||
[[ $(<"$install_log") == "pkg:firefox" ]] || fail "Firefox browser installer installs the package"
|
||||
[[ $(omarchy-default-browser) == "firefox" ]] || fail "Firefox becomes the default after its full installer succeeds"
|
||||
grep -Fxq 'sudo:install -d -m 0755 -o root -g root /usr/lib/firefox/distribution' "$setup_log" ||
|
||||
fail "Firefox browser installer creates its distribution directory"
|
||||
grep -Fxq 'sudo:find /usr/lib/firefox/distribution -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" ||
|
||||
fail "Firefox browser installer drops non-root files from its distribution directory"
|
||||
grep -Fxq "sudo:install -m 644 -o root -g root -T $ROOT/default/firefox/policies.json /usr/lib/firefox/distribution/policies.json" "$setup_log" ||
|
||||
fail "Firefox browser installer copies policies.json without following a destination symlink"
|
||||
[[ -e $installed_dir/firefox ]] || fail "Firefox browser installer marks firefox installed"
|
||||
pass "Firefox browser installer restores the complete Omarchy setup"
|
||||
|
||||
: >"$install_log"
|
||||
: >"$setup_log"
|
||||
rm -f "$installed_dir/zen-browser"
|
||||
OMARCHY_TEST_REAL_BROWSER_INSTALL=true omarchy-default-browser --install zen >/dev/null
|
||||
[[ $(<"$install_log") == "pkg:zen-browser-bin" ]] || fail "Zen browser installer installs the package"
|
||||
[[ $(omarchy-default-browser) == "zen" ]] || fail "Zen becomes the default after its full installer succeeds"
|
||||
grep -Fxq 'sudo:install -d -m 0755 -o root -g root /opt/zen-browser/distribution' "$setup_log" ||
|
||||
fail "Zen browser installer creates its distribution directory"
|
||||
grep -Fxq 'sudo:find /opt/zen-browser/distribution -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +' "$setup_log" ||
|
||||
fail "Zen browser installer drops non-root files from its distribution directory"
|
||||
grep -Fxq "sudo:install -m 644 -o root -g root -T $ROOT/default/firefox/policies.json /opt/zen-browser/distribution/policies.json" "$setup_log" ||
|
||||
fail "Zen browser installer copies policies.json without following a destination symlink"
|
||||
[[ -e $installed_dir/zen-browser ]] || fail "Zen browser installer marks zen-browser installed"
|
||||
pass "Zen browser installer restores the complete Omarchy setup"
|
||||
|
||||
omarchy-default-browser zen
|
||||
rm -f "$installed_dir/chromium"
|
||||
if OMARCHY_TEST_REAL_BROWSER_INSTALL=true OMARCHY_TEST_INSTALL_FAIL=true \
|
||||
|
||||
@@ -18,6 +18,151 @@ assertEqual(
|
||||
'notifications strip inline image tags'
|
||||
)
|
||||
|
||||
// The body renders as StyledText, which fetches <img src> over the network. The
|
||||
// invariant that matters is not a particular output string but that no tag Qt
|
||||
// would honour as an image survives, so assert that directly. Tags are bounded
|
||||
// the conservative way the stripper bounds them: a `<` opens a tag that runs to
|
||||
// the next `>`. Qt's own bound can be longer, since a `>` inside a quoted
|
||||
// attribute value does not close a tag there — which only ever splits one Qt
|
||||
// tag into several here, so a name this helper reads is a name Qt reads too.
|
||||
function survivingTagNames(text) {
|
||||
const names = []
|
||||
let i = 0
|
||||
while (i < text.length) {
|
||||
const open = text.indexOf('<', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexOf('>', open)
|
||||
const tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
|
||||
// Read the name the way Qt does, skipping anything that is not part of it.
|
||||
// Matching the separator with \s instead would give this helper the same
|
||||
// blind spot as the code it is checking — Qt skips U+0085 and \s does not —
|
||||
// and an assertion that shares the implementation's bug proves nothing.
|
||||
const name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
|
||||
if (name) names.push(name[1].toLowerCase())
|
||||
i = close === -1 ? text.length : close + 1
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Assert on styledBody, not sanitizeBody: styledBody is the string the card
|
||||
// binds to the StyledText, so it is the only one Qt ever parses. Checking the
|
||||
// sanitizer's output instead would pass a body whose surviving tag the newline
|
||||
// rewrite later splits open.
|
||||
function assertNoImageSurvives(body, description) {
|
||||
const out = notifications.styledBody(body, 'Slack', '')
|
||||
const names = survivingTagNames(out)
|
||||
assert(
|
||||
!names.includes('img'),
|
||||
description,
|
||||
`input: ${body}\noutput: ${out}\ntags: ${JSON.stringify(names)}`
|
||||
)
|
||||
}
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<img src="http://host/plain.png">',
|
||||
'notifications leave no image tag for a plain payload'
|
||||
)
|
||||
|
||||
// A payload spliced inside the literal "<img" prefix. Qt reads ONE malformed
|
||||
// tag named `im` here and renders nothing; a stripper that deleted the inner
|
||||
// match would close the halves up into a live <img> the input never had.
|
||||
assertNoImageSurvives(
|
||||
'<im<img src="http://host/decoy.png">g src="http://host/beacon.png">',
|
||||
'notifications leave no image tag when a payload is spliced inside <img'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<im<im<img src=a>g src=b>g src="http://host/deep.png">',
|
||||
'notifications leave no image tag for a doubly nested payload'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<img<img src="http://host/twin.png">',
|
||||
'notifications leave no image tag when the outer tag is itself named img'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'< img src="http://host/spaced.png">',
|
||||
'notifications leave no image tag when whitespace follows the angle bracket'
|
||||
)
|
||||
|
||||
// Qt skips the separator between `<` and the tag name with QChar::isSpace(),
|
||||
// which counts U+0085 NEL. JavaScript's \s does not. Reading the name with \s
|
||||
// finds none here, keeps the tag, and Qt then reads `img` and fetches it —
|
||||
// measured against Qt 6.11.2, where this exact body makes a StyledText Text
|
||||
// issue an outbound GET. Asserted on the whole output rather than through
|
||||
// assertNoImageSurvives so it holds even if that helper is ever loosened.
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<\u0085img src="http://host/nel.png">after', 'Slack', ''),
|
||||
'after',
|
||||
'notifications strip an image tag whose separator is U+0085, which Qt skips but \\s does not'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<\u0085img src="http://host/nel2.png">',
|
||||
'notifications leave no image tag when U+0085 follows the angle bracket'
|
||||
)
|
||||
|
||||
// The card rewrites newlines to <br/> for the StyledText, which puts tag syntax
|
||||
// inside a tag the stripper kept: `<x`, newline, `<img …>` is one tag named `x`
|
||||
// to both the stripper and Qt, and the rewrite splits it into `<x<br/>` and a
|
||||
// live image tag. Measured against Qt 6.11.2 — the rewritten form issues the GET
|
||||
// and the original does not — so the strip has to run after the rewrite, which
|
||||
// is what styledBody() does.
|
||||
assertNoImageSurvives(
|
||||
'<x\n<img src="http://host/split.png">',
|
||||
'notifications leave no image tag when a newline rewrite splits a kept tag'
|
||||
)
|
||||
|
||||
assertNoImageSurvives(
|
||||
'<x\r\n<img src="http://host/split-crlf.png">',
|
||||
'notifications leave no image tag when a CRLF rewrite splits a kept tag'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.styledBody('<x\n<img src="http://host/split.png">', 'Slack', ''),
|
||||
'<x<br/>',
|
||||
'notifications drop the image half of a tag the newline rewrite splits'
|
||||
)
|
||||
|
||||
// The rewrite itself still happens, and body markup other than images survives it.
|
||||
assertEqual(
|
||||
notifications.styledBody('<b>bold</b>\nsecond line', 'Slack', ''),
|
||||
'<b>bold</b><br/>second line',
|
||||
'notifications keep body markup and the line break the card renders'
|
||||
)
|
||||
|
||||
// The order above is only worth anything if the card actually renders it, and no
|
||||
// JavaScript assertion can see a QML binding. Pin the binding itself: the rewrite
|
||||
// belongs in the logic module, where the strip runs after it.
|
||||
const cardQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/components/NotificationCard.qml'), 'utf8')
|
||||
assert(
|
||||
/readonly property string styledBody: NotificationLogic\.styledBody\(body, app, appIcon\)/.test(cardQml),
|
||||
'the notification card renders the body that was stripped after the newline rewrite'
|
||||
)
|
||||
assert(
|
||||
!/<br\/>/.test(cardQml),
|
||||
'the notification card does not rewrite newlines itself, which would leave tag syntax unchecked'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('trailing <img src="http://host/z.png"', 'Slack', ''),
|
||||
'trailing ',
|
||||
'notifications strip an unterminated image tag the renderer would close itself'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<IMG SRC="http://host/u.png">shout', 'Slack', ''),
|
||||
'shout',
|
||||
'notifications strip image tags regardless of case'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<b>bold</b> and <a href="http://host">link</a>', 'Slack', ''),
|
||||
'<b>bold</b> and <a href="http://host">link</a>',
|
||||
'notifications keep the body markup the body-markup capability advertises'
|
||||
)
|
||||
|
||||
assertEqual(
|
||||
notifications.sanitizeBody('<a href="https://example.com">example.com</a> Message body', 'Chromium', ''),
|
||||
'Message body',
|
||||
|
||||
@@ -41,3 +41,103 @@ grep -Fq 'sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"' "$ROOT/bin/omarc
|
||||
fail "omarchy-plymouth-set copies the staged logo to SDDM rather than rereading the caller's path as root"
|
||||
|
||||
pass "a themed logo cannot republish a file it merely points at"
|
||||
|
||||
# Style > Unlock picks a theme by name and hands the answer to
|
||||
# omarchy-launch-floating-terminal-with-presentation, which joins its arguments
|
||||
# into a script and runs that with `bash -c`. So the name is shell source
|
||||
# unless the action quotes it -- and the name is a directory name under
|
||||
# ~/.config/omarchy/themes, which a theme installed from a git repo gets from
|
||||
# the repo URL. `a';id;'b` is a legal directory name.
|
||||
require_command node
|
||||
|
||||
unlock_action=$(node -e '
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const menu = require(path.join(process.env.ROOT, "shell/plugins/menu/MenuModel.js"))
|
||||
const items = menu.parseMenuJsonc(fs.readFileSync(path.join(process.env.ROOT, "default/omarchy/omarchy-menu.jsonc"), "utf8"))
|
||||
process.stdout.write(items.find(item => item.id === "style.unlock").action)
|
||||
')
|
||||
|
||||
[[ -n $unlock_action ]] || fail "the shipped menu still carries a style.unlock action"
|
||||
|
||||
stub_dir="$test_tmp/stubs"
|
||||
mkdir -p "$stub_dir"
|
||||
|
||||
canary="$test_tmp/canary"
|
||||
set_args="$test_tmp/set-args"
|
||||
reset_marker="$test_tmp/reset-ran"
|
||||
|
||||
# What a name that got reparsed would reach. It is a command rather than a
|
||||
# `touch` so that no quoting of the test's own paths is involved.
|
||||
cat >"$stub_dir/omarchy-test-canary" <<STUB
|
||||
#!/bin/bash
|
||||
printf 'ran\n' >"$canary"
|
||||
STUB
|
||||
|
||||
cat >"$stub_dir/omarchy-plymouth-switcher" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$OMARCHY_TEST_UNLOCK_NAME"
|
||||
STUB
|
||||
|
||||
# Stands in for the real wrapper, which is a shell-string API: it interpolates
|
||||
# "$*" into a script and hands that to `bash -c`. The grep below is what keeps
|
||||
# this stub honest if the wrapper ever stops working that way.
|
||||
cat >"$stub_dir/omarchy-launch-floating-terminal-with-presentation" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec bash -c "omarchy-show-logo; $*; omarchy-show-done"
|
||||
STUB
|
||||
|
||||
grep -Fq 'bash -c "$presentation_script"' "$ROOT/bin/omarchy-launch-floating-terminal-with-presentation" ||
|
||||
fail "the presentation wrapper still runs its argument as a shell string, as the stub above assumes"
|
||||
|
||||
# Records what actually arrived, so a name that survived as data is told apart
|
||||
# from one that arrived split or partly eaten.
|
||||
cat >"$stub_dir/omarchy-plymouth-set-by-theme" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$#" "$@" >"$OMARCHY_TEST_SET_ARGS"
|
||||
STUB
|
||||
|
||||
cat >"$stub_dir/omarchy-plymouth-reset" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'ran\n' >"$OMARCHY_TEST_RESET_MARKER"
|
||||
STUB
|
||||
|
||||
for command in omarchy-show-logo omarchy-show-done; do
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_dir/$command"
|
||||
done
|
||||
|
||||
chmod +x "$stub_dir"/*
|
||||
|
||||
run_unlock_action() {
|
||||
rm -f "$canary" "$set_args" "$reset_marker"
|
||||
|
||||
PATH="$stub_dir:$PATH" \
|
||||
OMARCHY_TEST_UNLOCK_NAME="$1" \
|
||||
OMARCHY_TEST_SET_ARGS="$set_args" \
|
||||
OMARCHY_TEST_RESET_MARKER="$reset_marker" \
|
||||
bash -c "$unlock_action" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A directory name cannot hold a slash or a NUL, and everything else is fair
|
||||
# game -- these are the shapes that would run on the way to the picker.
|
||||
for name in "a';omarchy-test-canary;'b" 'a$(omarchy-test-canary)b' 'a`omarchy-test-canary`b' 'a b' '-a'; do
|
||||
run_unlock_action "$name"
|
||||
|
||||
[[ ! -e $canary ]] || fail "a theme name reaches the unlock screen as data, not as shell" "ran for: $name"
|
||||
[[ $(cat "$set_args" 2>/dev/null) == $'1\n'"$name" ]] ||
|
||||
fail "the unlock screen gets the theme name whole" "$name: $(cat "$set_args" 2>/dev/null)"
|
||||
done
|
||||
|
||||
pass "a theme name cannot carry a command into the unlock screen"
|
||||
|
||||
# The two ordinary paths still work: a named theme is applied, and `default`
|
||||
# resets rather than being looked up as a theme.
|
||||
run_unlock_action "tokyo-night"
|
||||
[[ $(cat "$set_args" 2>/dev/null) == $'1\ntokyo-night' ]] ||
|
||||
fail "an ordinary theme name still reaches omarchy-plymouth-set-by-theme" "$(cat "$set_args" 2>/dev/null)"
|
||||
|
||||
run_unlock_action "default"
|
||||
[[ -e $reset_marker ]] || fail "picking default still resets the unlock screen"
|
||||
[[ ! -e $set_args ]] || fail "picking default does not look up a theme named default" "$(cat "$set_args")"
|
||||
|
||||
pass "the unlock picker still applies a theme and still resets on default"
|
||||
|
||||
@@ -27,16 +27,41 @@ cat >"$TMPDIR/bin/usermod" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/usermod.calls"
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin/getent" "$TMPDIR/bin/usermod"
|
||||
cat >"$TMPDIR/bin/groupadd" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/groupadd.calls"
|
||||
STUB
|
||||
cat >"$TMPDIR/bin/install" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/install.calls"
|
||||
STUB
|
||||
cat >"$TMPDIR/bin/find" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/find.calls"
|
||||
STUB
|
||||
cat >"$TMPDIR/bin/sudo" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/sudo.calls"
|
||||
exec "\$@"
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin"/{getent,usermod,groupadd,install,find,sudo}
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
|
||||
# No install user (deferred-provisioning install): input recorded, usermod not called.
|
||||
# No install user (deferred-provisioning install): groups recorded, usermod not called.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
|
||||
[[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user"
|
||||
grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded"
|
||||
! grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" ||
|
||||
fail "browser-policy group must not be recorded"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user"
|
||||
[[ ! -f $TMPDIR/groupadd.calls ]] || ! grep -F omarchy-browser-policy "$TMPDIR/groupadd.calls" >/dev/null ||
|
||||
fail "browser-policy group is not created"
|
||||
grep -F -- '-d -m 0755 -o root -g root /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null ||
|
||||
fail "browser-policy directory is created root-owned"
|
||||
pass "deferred provisioning records groups without calling usermod"
|
||||
|
||||
# The docker group is root-equivalent and must never be granted automatically.
|
||||
@@ -45,17 +70,22 @@ pass "docker group is not recorded at install"
|
||||
|
||||
# Missing user (defensive): no usermod either.
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called for a missing user"
|
||||
pass "missing install user defers group grants"
|
||||
|
||||
# Re-running never duplicates entries.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
[[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
pass "group recording is idempotent"
|
||||
|
||||
# Existing user: usermod applies the recorded groups, and docker is never among them.
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user"
|
||||
! grep -q -- "omarchy-browser-policy" "$TMPDIR/usermod.calls" ||
|
||||
fail "usermod must not grant browser-policy to the install user"
|
||||
! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user"
|
||||
pass "existing install user gets input but never docker"
|
||||
pass "existing install user gets input but never docker or browser-policy"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user