Merge remote-tracking branch 'origin/quattro' into quattro
# Conflicts: # default/pacman/pacman-stable.conf
This commit is contained in:
@@ -41,6 +41,7 @@ GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
|||||||
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
||||||
GROUP_DESCRIPTIONS[cn]="OmarchyCN China environment integration"
|
GROUP_DESCRIPTIONS[cn]="OmarchyCN China environment integration"
|
||||||
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
||||||
|
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
|
||||||
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
||||||
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||||
|
|||||||
+3
-1
@@ -92,8 +92,10 @@ omp)
|
|||||||
;;
|
;;
|
||||||
ori)
|
ori)
|
||||||
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
|
# 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)
|
command=(ori code)
|
||||||
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
|
||||||
;;
|
;;
|
||||||
pi)
|
pi)
|
||||||
command=(pi)
|
command=(pi)
|
||||||
|
|||||||
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
|
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
# omarchy:args=[--no-osd] [+N%|N%-|N%]
|
# omarchy:args=[--no-osd] [+N%|N%-|N%]
|
||||||
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
|
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
|
||||||
|
|
||||||
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device"
|
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
|
||||||
|
# caching (detect every run) rather than fall back to a predictable, world-writable
|
||||||
|
# /tmp path another user could pre-create.
|
||||||
|
device_cache=""
|
||||||
|
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
|
||||||
|
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
|
||||||
|
fi
|
||||||
no_osd=0
|
no_osd=0
|
||||||
if [[ ${1:-} == "--no-osd" ]]; then
|
if [[ ${1:-} == "--no-osd" ]]; then
|
||||||
no_osd=1
|
no_osd=1
|
||||||
@@ -28,9 +34,14 @@ find_apple_display_device() {
|
|||||||
local cached=""
|
local cached=""
|
||||||
local device=""
|
local device=""
|
||||||
|
|
||||||
if [[ -r $device_cache ]]; then
|
if [[ -n $device_cache && -r $device_cache ]]; then
|
||||||
read -r cached <"$device_cache" || true
|
read -r cached <"$device_cache" || true
|
||||||
if [[ -n $cached && -e $cached ]]; then
|
# Trust a cached value only if it still names a hiddev character device. A
|
||||||
|
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
|
||||||
|
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
|
||||||
|
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
|
||||||
|
# and quoting them would turn the match into a literal string comparison.
|
||||||
|
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
|
||||||
printf '%s\n' "$cached"
|
printf '%s\n' "$cached"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -39,7 +50,9 @@ find_apple_display_device() {
|
|||||||
device="$(detect_apple_display_device)" || return 1
|
device="$(detect_apple_display_device)" || return 1
|
||||||
[[ -n $device ]] || return 1
|
[[ -n $device ]] || return 1
|
||||||
|
|
||||||
printf '%s\n' "$device" >"$device_cache"
|
if [[ -n $device_cache ]]; then
|
||||||
|
printf '%s\n' "$device" >"$device_cache"
|
||||||
|
fi
|
||||||
printf '%s\n' "$device"
|
printf '%s\n' "$device"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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.
|
# -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 |
|
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||||
while IFS= read -r entry; do
|
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 < <(
|
IFS=$'\t' read -r uid comm pid exe signal < <(
|
||||||
jq -r '[(._UID // "-"),
|
jq -r 'def field: if . == null or . == "" then "-" else . end;
|
||||||
(.COREDUMP_COMM // "-"),
|
[(._UID | field),
|
||||||
(.COREDUMP_PID // "-"),
|
(.COREDUMP_COMM | field),
|
||||||
(.COREDUMP_EXE // "-"),
|
(.COREDUMP_PID | field),
|
||||||
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
(.COREDUMP_EXE | field),
|
||||||
|
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
|
||||||
)
|
)
|
||||||
|
|
||||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||||
@@ -71,11 +76,29 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
|||||||
name=$comm
|
name=$comm
|
||||||
[[ $exe == /* ]] && name=${exe##*/}
|
[[ $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
|
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
|
||||||
|
|
||||||
# Never announce our own machinery, or it notifies about itself.
|
# Never announce our own machinery, or it notifies about itself.
|
||||||
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
|
[[ $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=$EPOCHSECONDS
|
||||||
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,18 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
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 (dirname, install, tee, rm, nmcli, systemctl, awk) 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
|
||||||
|
|
||||||
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
||||||
|
|
||||||
provider_from_arg() {
|
provider_from_arg() {
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Check that a git URL names a repository, not a transport helper
|
||||||
|
# omarchy:args=<git-url>
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
|
||||||
|
# in exactly two shapes, and no others: `<helper>::<address>`, and
|
||||||
|
# `<scheme>://<address>` for any scheme git does not handle itself. A single
|
||||||
|
# colon is always scp-style ssh, and a bare path is always a path; neither can
|
||||||
|
# reach a helper. So constraining those two shapes covers the whole surface.
|
||||||
|
#
|
||||||
|
# The `::` shape is refused outright, because no helper reachable that way is one
|
||||||
|
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
|
||||||
|
# The `://` shape cannot be refused the same way, since it is also how every
|
||||||
|
# legitimate URL arrives -- so it is allowlisted instead. The list is the
|
||||||
|
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
|
||||||
|
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
|
||||||
|
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
|
||||||
|
# command the URL carries.
|
||||||
|
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "omarchy-git-url-check: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
url="${1-}"
|
||||||
|
|
||||||
|
if [[ -z $url ]]; then
|
||||||
|
fail "a git URL is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||||
|
fail "'$url' names a git option or transport helper, not a repository."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
|
||||||
|
scheme="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
for transport in "${TRANSPORTS[@]}"; do
|
||||||
|
if [[ $scheme == "$transport" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
|
||||||
|
fi
|
||||||
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,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
|
|||||||
|
|
||||||
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
||||||
|
|
||||||
|
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
|
||||||
|
# name that is not a plain connector string could execute on the next reload.
|
||||||
|
# Names come from hyprctl; a user-created headless output can carry anything.
|
||||||
|
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
echo "Refusing unsafe internal monitor name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
valid_scale() {
|
valid_scale() {
|
||||||
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ off() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The name is written into generated Lua below, so only a plain connector
|
||||||
|
# name may pass; anything else could execute on the next reload.
|
||||||
|
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! omarchy-hyprland-monitor-external-active; then
|
if ! omarchy-hyprland-monitor-external-active; then
|
||||||
omarchy-notification-send -g "Can't disable the only active display"
|
omarchy-notification-send -g "Can't disable the only active display"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ on() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Both names are written into generated Lua below, so only plain connector
|
||||||
|
# names may pass; a user-created headless output can carry any name.
|
||||||
|
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||||
|
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ set_scale() {
|
|||||||
local width="$(echo "$monitor_info" | jq -r '.width')"
|
local width="$(echo "$monitor_info" | jq -r '.width')"
|
||||||
local height="$(echo "$monitor_info" | jq -r '.height')"
|
local height="$(echo "$monitor_info" | jq -r '.height')"
|
||||||
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
||||||
|
|
||||||
|
# active_monitor is written into the Lua string eval'd below, so only a plain
|
||||||
|
# connector name may pass; a hostile output name could execute otherwise.
|
||||||
|
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
echo "Refusing unsafe monitor name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
||||||
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
||||||
# factor even when the monitor scale itself is fractional.
|
# factor even when the monitor scale itself is fractional.
|
||||||
|
|||||||
+11
-17
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
setup_policy_directory() {
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
sudo mkdir -p "$1"
|
|
||||||
sudo chmod a+rw "$1"
|
setup_chromium_policy_directory() {
|
||||||
|
browser_policy_setup_dir "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
announce_browser_installed() {
|
announce_browser_installed() {
|
||||||
@@ -23,13 +24,6 @@ copy_chromium_flags() {
|
|||||||
omarchy-install-chromium-ytdlp
|
omarchy-install-chromium-ytdlp
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_firefox_preferences() {
|
|
||||||
local distribution_dir="$1"
|
|
||||||
|
|
||||||
setup_policy_directory "$distribution_dir"
|
|
||||||
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_firefox_wayland() {
|
setup_firefox_wayland() {
|
||||||
mkdir -p ~/.config/environment.d
|
mkdir -p ~/.config/environment.d
|
||||||
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
|
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
|
||||||
@@ -40,7 +34,7 @@ chromium)
|
|||||||
echo "Installing Chromium..."
|
echo "Installing Chromium..."
|
||||||
omarchy-pkg-add 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
|
copy_chromium_flags ~/.config/chromium-flags.conf
|
||||||
omarchy-theme-set-browser
|
omarchy-theme-set-browser
|
||||||
announce_browser_installed "Chromium"
|
announce_browser_installed "Chromium"
|
||||||
@@ -49,7 +43,7 @@ chrome)
|
|||||||
echo "Installing Chrome..."
|
echo "Installing Chrome..."
|
||||||
omarchy-pkg-aur-add google-chrome || exit 1
|
omarchy-pkg-aur-add google-chrome || exit 1
|
||||||
|
|
||||||
setup_policy_directory /etc/opt/chrome/policies/managed
|
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
|
||||||
copy_chromium_flags ~/.config/chrome-flags.conf
|
copy_chromium_flags ~/.config/chrome-flags.conf
|
||||||
omarchy-theme-set-browser
|
omarchy-theme-set-browser
|
||||||
announce_browser_installed "Chrome"
|
announce_browser_installed "Chrome"
|
||||||
@@ -58,7 +52,7 @@ edge)
|
|||||||
echo "Installing Edge..."
|
echo "Installing Edge..."
|
||||||
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
|
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
|
||||||
|
|
||||||
setup_policy_directory /etc/opt/edge/policies/managed
|
setup_chromium_policy_directory /etc/opt/edge/policies/managed
|
||||||
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
|
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
|
||||||
omarchy-theme-set-browser
|
omarchy-theme-set-browser
|
||||||
announce_browser_installed "Edge"
|
announce_browser_installed "Edge"
|
||||||
@@ -67,7 +61,7 @@ brave)
|
|||||||
echo "Installing Brave..."
|
echo "Installing Brave..."
|
||||||
omarchy-pkg-aur-add brave-bin || exit 1
|
omarchy-pkg-aur-add brave-bin || exit 1
|
||||||
|
|
||||||
setup_policy_directory /etc/brave/policies/managed
|
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||||
copy_chromium_flags ~/.config/brave-flags.conf
|
copy_chromium_flags ~/.config/brave-flags.conf
|
||||||
omarchy-theme-set-browser
|
omarchy-theme-set-browser
|
||||||
announce_browser_installed "Brave"
|
announce_browser_installed "Brave"
|
||||||
@@ -76,7 +70,7 @@ brave-origin)
|
|||||||
echo "Installing Brave Origin..."
|
echo "Installing Brave Origin..."
|
||||||
omarchy-pkg-aur-add brave-origin-bin || exit 1
|
omarchy-pkg-aur-add brave-origin-bin || exit 1
|
||||||
|
|
||||||
setup_policy_directory /etc/brave/policies/managed
|
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||||
copy_chromium_flags ~/.config/brave-origin-flags.conf
|
copy_chromium_flags ~/.config/brave-origin-flags.conf
|
||||||
omarchy-theme-set-browser
|
omarchy-theme-set-browser
|
||||||
announce_browser_installed "Brave Origin"
|
announce_browser_installed "Brave Origin"
|
||||||
@@ -85,7 +79,7 @@ firefox)
|
|||||||
echo "Installing Firefox..."
|
echo "Installing Firefox..."
|
||||||
omarchy-pkg-add firefox || exit 1
|
omarchy-pkg-add firefox || exit 1
|
||||||
|
|
||||||
setup_firefox_preferences /usr/lib/firefox/distribution
|
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
|
||||||
setup_firefox_wayland
|
setup_firefox_wayland
|
||||||
announce_browser_installed "Firefox"
|
announce_browser_installed "Firefox"
|
||||||
;;
|
;;
|
||||||
@@ -93,7 +87,7 @@ zen)
|
|||||||
echo "Installing Zen..."
|
echo "Installing Zen..."
|
||||||
omarchy-pkg-aur-add zen-browser-bin || exit 1
|
omarchy-pkg-aur-add zen-browser-bin || exit 1
|
||||||
|
|
||||||
setup_firefox_preferences /opt/zen-browser/distribution
|
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
|
||||||
setup_firefox_wayland
|
setup_firefox_wayland
|
||||||
announce_browser_installed "Zen"
|
announce_browser_installed "Zen"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
|
|||||||
sudo systemctl enable --now once-background.service
|
sudo systemctl enable --now once-background.service
|
||||||
|
|
||||||
echo -e "\nLaunching ONCE..."
|
echo -e "\nLaunching ONCE..."
|
||||||
once
|
sudo once
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ if [[ -z $url ]]; then
|
|||||||
[[ -n $url ]] || fail "a git URL is required"
|
[[ -n $url ]] || fail "a git URL is required"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Refuse a URL that names a git option or a transport helper before cloning, so
|
||||||
|
# an untrusted URL cannot run a command before the plugin is validated or
|
||||||
|
# enabled. The check is shared with omarchy-theme-install and explains itself; a
|
||||||
|
# missing checker leaves this non-zero, which refuses the URL rather than
|
||||||
|
# cloning it.
|
||||||
|
omarchy-git-url-check "$url" || exit 1
|
||||||
|
|
||||||
if (( ! ASSUME_YES )); then
|
if (( ! ASSUME_YES )); then
|
||||||
cat >&2 <<WARN
|
cat >&2 <<WARN
|
||||||
|
|
||||||
|
|||||||
@@ -742,6 +742,12 @@ create_user() {
|
|||||||
# for specific commands), and a duplicate grant is harmless.
|
# for specific commands), and a duplicate grant is harmless.
|
||||||
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
||||||
chmod 440 /etc/sudoers.d/00-omarchy-wheel
|
chmod 440 /etc/sudoers.d/00-omarchy-wheel
|
||||||
|
|
||||||
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
|
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||||
|
[[ -d $dir || -L $dir ]] || continue
|
||||||
|
browser_policy_setup_dir "$dir"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
install_authorized_keys() {
|
install_authorized_keys() {
|
||||||
|
|||||||
@@ -24,9 +24,14 @@ echo -e "\e[32mRemoving FIDO2 device from authentication.\n\e[0m"
|
|||||||
|
|
||||||
remove_pam_config
|
remove_pam_config
|
||||||
|
|
||||||
if [[ -d /etc/fido2 ]]; then
|
authdir=/etc/fido2
|
||||||
|
|
||||||
|
# -d follows symlinks, so a dangling link at /etc/fido2 would survive this and
|
||||||
|
# a later setup would install the authfile through it. rm -rf on a symlink
|
||||||
|
# removes the link itself, never the directory it points at.
|
||||||
|
if [[ -e $authdir || -L $authdir ]]; then
|
||||||
echo "Removing FIDO2 configuration..."
|
echo "Removing FIDO2 configuration..."
|
||||||
sudo rm -rf /etc/fido2
|
sudo rm -rf "$authdir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Removing FIDO2 packages..."
|
echo "Removing FIDO2 packages..."
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
|
||||||
check_fido2_hardware() {
|
check_fido2_hardware() {
|
||||||
@@ -50,13 +51,79 @@ if ! check_fido2_hardware; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the pamu2fcfg file
|
# Create the pamu2fcfg file
|
||||||
if [[ ! -f /etc/fido2/fido2 ]]; then
|
authdir=/etc/fido2
|
||||||
sudo mkdir -p /etc/fido2
|
authfile=/etc/fido2/fido2
|
||||||
|
|
||||||
|
# install -d follows a symlink here and applies the mode and ownership to
|
||||||
|
# whatever it points at, so the credential would be staged and published inside
|
||||||
|
# the link target and that directory reopened to root:root 755. This is the
|
||||||
|
# threat omarchy-remove-security-fido2 already names on its side.
|
||||||
|
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
|
||||||
|
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
|
||||||
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -f follows symlinks, so the already-registered check below reads a symlinked
|
||||||
|
# authfile as a registration and leaves it in place, and is false for a
|
||||||
|
# directory, so it tries to register over one. Only a regular file is a valid
|
||||||
|
# pam_u2f authfile.
|
||||||
|
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
|
||||||
|
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
|
||||||
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $authfile ]]; then
|
||||||
|
sudo install -d -m 755 -o root -g root "$authdir"
|
||||||
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
||||||
echo -e "Touch your FIDO2 key when it lights up...\n"
|
echo -e "Touch your FIDO2 key when it lights up...\n"
|
||||||
|
|
||||||
if pamu2fcfg >/tmp/fido2; then
|
# A unique sibling created by root cannot be replaced by another process
|
||||||
sudo mv /tmp/fido2 /etc/fido2/fido2
|
# running as this user. Stream pamu2fcfg into it instead of asking root to
|
||||||
|
# reopen a caller-owned path: an observed temporary name could otherwise be
|
||||||
|
# replaced with a symlink before the privileged copy. The final rename is
|
||||||
|
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
|
||||||
|
# root-owned global authfile readable when pam_u2f uses openasuser; only root
|
||||||
|
# can still rewrite it.
|
||||||
|
stage=""
|
||||||
|
|
||||||
|
# mktemp's output is an operand for four privileged commands below, one of
|
||||||
|
# them an rm. Take only the name this script asked for rather than whatever
|
||||||
|
# came back on stdout.
|
||||||
|
safe_stage_path() {
|
||||||
|
local candidate=$1
|
||||||
|
local prefix="$authfile.new."
|
||||||
|
local suffix
|
||||||
|
|
||||||
|
[[ $candidate == "$prefix"* ]] || return 1
|
||||||
|
suffix=${candidate#"$prefix"}
|
||||||
|
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stage() {
|
||||||
|
local status=$?
|
||||||
|
|
||||||
|
if safe_stage_path "$stage"; then
|
||||||
|
sudo rm -f -- "$stage" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_stage EXIT
|
||||||
|
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
||||||
|
|
||||||
|
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
||||||
|
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
|
||||||
|
sudo chmod 644 "$stage"
|
||||||
|
sudo mv -Tf "$stage" "$authfile"
|
||||||
|
stage=""
|
||||||
|
trap - EXIT
|
||||||
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
||||||
else
|
else
|
||||||
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
||||||
|
|||||||
@@ -16,14 +16,10 @@ if [[ -z $REPO_URL ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# git reads a leading dash as an option, and `<helper>::<address>` as a remote
|
# Refuse a URL that names a git option or a transport helper before cloning. The
|
||||||
# helper to run. The helper name is a bare word at the very start, which is what
|
# check is shared with omarchy-plugin-add and explains itself; a missing checker
|
||||||
# this matches; an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git
|
# leaves this non-zero, which refuses the URL rather than cloning it.
|
||||||
# carries `::` too and must still clone.
|
omarchy-git-url-check "$REPO_URL" || exit 1
|
||||||
if [[ $REPO_URL == -* || $REPO_URL =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
|
||||||
echo "Error: '$REPO_URL' names a git option or transport helper, not a repository."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,15 @@
|
|||||||
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
|
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
|
||||||
# omarchy:hidden=true
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
|
|
||||||
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
|
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
|
||||||
|
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
|
||||||
|
|
||||||
if [[ -f $CHROMIUM_THEME ]]; then
|
if [[ -f $CHROMIUM_THEME ]]; then
|
||||||
THEME_RGB_COLOR=$(<$CHROMIUM_THEME)
|
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
|
||||||
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
|
|
||||||
else
|
|
||||||
# Use a default, neutral grey if theme doesn't have a color
|
|
||||||
THEME_HEX_COLOR="#1c2027"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_browser_policy() {
|
|
||||||
local policy_dir="$1"
|
|
||||||
|
|
||||||
[[ -d $policy_dir ]] || return
|
|
||||||
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh_running_browser() {
|
refresh_running_browser() {
|
||||||
local process="$1"
|
local process="$1"
|
||||||
local command="$2"
|
local command="$2"
|
||||||
@@ -30,17 +22,15 @@ refresh_running_browser() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
set_browser_policy /etc/chromium/policies/managed
|
failed=0
|
||||||
|
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
|
||||||
|
|
||||||
refresh_running_browser chromium chromium
|
refresh_running_browser chromium chromium
|
||||||
|
|
||||||
set_browser_policy /etc/opt/chrome/policies/managed
|
|
||||||
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
|
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
|
||||||
|
|
||||||
set_browser_policy /etc/opt/edge/policies/managed
|
|
||||||
refresh_running_browser msedge microsoft-edge-stable
|
refresh_running_browser msedge microsoft-edge-stable
|
||||||
|
|
||||||
set_browser_policy /etc/brave/policies/managed
|
|
||||||
refresh_running_browser brave brave
|
refresh_running_browser brave brave
|
||||||
# Match on the binary path: the running process is named plain "brave", and a
|
# Match on the binary path: the running process is named plain "brave", and a
|
||||||
# bare -f brave-origin pattern would also match the installer's own terminal.
|
# bare -f brave-origin pattern would also match the installer's own terminal.
|
||||||
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
|
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
|
||||||
|
|
||||||
|
exit "$failed"
|
||||||
|
|||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Write the current theme color into the browser policy directories
|
||||||
|
# omarchy:args=<rrggbb>
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Whenever this runs as root — invoked directly through the passwordless
|
||||||
|
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||||
|
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||||
|
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||||
|
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
|
||||||
|
# never an omarchy-* command, so pin PATH to trusted system directories and keep
|
||||||
|
# root from resolving one out of that checkout. The unprivileged wrapper phase
|
||||||
|
# keeps the caller's PATH so it can still find sudo/pkexec.
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enterprise policy trust roots. The list is fixed here rather than taken from
|
||||||
|
# the caller: the caller chooses a color, never a path.
|
||||||
|
POLICY_DIRS=(
|
||||||
|
/etc/chromium/policies/managed
|
||||||
|
/etc/opt/chrome/policies/managed
|
||||||
|
/etc/opt/edge/policies/managed
|
||||||
|
/etc/brave/policies/managed
|
||||||
|
)
|
||||||
|
|
||||||
|
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
|
||||||
|
# runs from there rather than from whichever copy was invoked, so the rule
|
||||||
|
# matches even where $OMARCHY_PATH points at a checkout.
|
||||||
|
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( $# != 1 )); then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
color="$1"
|
||||||
|
|
||||||
|
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
|
||||||
|
# is added when the JSON is written rather than passed in: "#" opens a comment
|
||||||
|
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
|
||||||
|
# argument as a plain six-character glob.
|
||||||
|
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
|
||||||
|
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# True when sudo would run this exact command without stopping for a password.
|
||||||
|
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||||
|
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||||
|
# entry's tags, so !authenticate is the grant in
|
||||||
|
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
|
||||||
|
# and, under -n, prompts for nothing.
|
||||||
|
sudo_grants_passwordless() {
|
||||||
|
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
return
|
||||||
|
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||||
|
exec sudo "$PACKAGED_PATH" "$@"
|
||||||
|
else
|
||||||
|
exec pkexec "$PACKAGED_PATH" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root "$color"
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
staged=""
|
||||||
|
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
|
||||||
|
# so this handler must not end on a false test. Every successful run clears
|
||||||
|
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n $staged ]]; then
|
||||||
|
rm -f "$staged"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
for policy_dir in "${POLICY_DIRS[@]}"; do
|
||||||
|
# Only browsers Omarchy has installed have a policy directory. Creating one
|
||||||
|
# here would hand a browser a managed-policy root it does not otherwise have.
|
||||||
|
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
|
||||||
|
|
||||||
|
dest=$policy_dir/color.json
|
||||||
|
staged=$(mktemp) || {
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
|
||||||
|
|
||||||
|
if [[ -L $dest || -d $dest ]]; then
|
||||||
|
if ! rm -rf -- "$dest"; then
|
||||||
|
rm -f "$staged"
|
||||||
|
staged=""
|
||||||
|
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
|
||||||
|
rm -f "$staged"
|
||||||
|
staged=""
|
||||||
|
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$staged"
|
||||||
|
staged=""
|
||||||
|
done
|
||||||
|
|
||||||
|
exit "$failed"
|
||||||
@@ -7,44 +7,70 @@
|
|||||||
KIND="${1:-}"
|
KIND="${1:-}"
|
||||||
ACTION="${2:-toggle}"
|
ACTION="${2:-toggle}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||||
|
}
|
||||||
|
|
||||||
case "$KIND" in
|
case "$KIND" in
|
||||||
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
||||||
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Hyprland sources this directory on reload, so the disabled state survives restarts
|
# The persisted disable is the device name stored as plain data; on every
|
||||||
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
|
# reload default/hypr/disabled-input-device.lua reads it back and disables the
|
||||||
|
# device. Names come from USB descriptors and must not be interpolated into
|
||||||
|
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
|
||||||
|
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
|
||||||
|
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
|
||||||
|
|
||||||
device="$("omarchy-hw-$KIND")"
|
device="$("omarchy-hw-$KIND")"
|
||||||
|
|
||||||
if [[ -z $device ]]; then
|
require_device() {
|
||||||
echo "No $KIND device found" >&2
|
if [[ -z $device ]]; then
|
||||||
exit 1
|
echo "No $KIND device found" >&2
|
||||||
fi
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $device == *[[:cntrl:]]* ]]; then
|
||||||
|
echo "Invalid $KIND device name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_device() {
|
||||||
|
local enabled=$1
|
||||||
|
local quoted=${device//\\/\\\\}
|
||||||
|
quoted=${quoted//\"/\\\"}
|
||||||
|
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
|
# Clear the persisted state before requiring a usable device, so a device
|
||||||
rm -f "$STATE_FILE"
|
# that stops reporting a valid name can never wedge the disable in place.
|
||||||
|
rm -f "$NAME_FILE"
|
||||||
|
require_device
|
||||||
|
apply_device true
|
||||||
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
|
require_device
|
||||||
mkdir -p "$(dirname "$STATE_FILE")"
|
apply_device false
|
||||||
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
|
mkdir -p "$(dirname "$NAME_FILE")"
|
||||||
|
printf '%s\n' "$device" >"$NAME_FILE"
|
||||||
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$ACTION" in
|
case "$ACTION" in
|
||||||
on) enable ;;
|
on) enable ;;
|
||||||
off) disable ;;
|
off) disable ;;
|
||||||
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
|
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1312,7 +1312,21 @@ apply_system_transition() {
|
|||||||
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
|
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
|
||||||
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
|
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
|
||||||
|
|
||||||
as_root install -d -m 0777 /etc/chromium/policies/managed
|
local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
|
||||||
|
if ! as_root test -f "$browser_policy_helper"; then
|
||||||
|
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
|
||||||
|
else
|
||||||
|
as_root env OMARCHY_PATH=/usr/share/omarchy \
|
||||||
|
bash -euo pipefail -c '
|
||||||
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
|
browser_policy_setup_dir /etc/chromium/policies/managed
|
||||||
|
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||||
|
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
|
||||||
|
[[ -d $dir || -L $dir ]] || continue
|
||||||
|
browser_policy_setup_dir "$dir"
|
||||||
|
done
|
||||||
|
'
|
||||||
|
fi
|
||||||
as_root install -d -m 0755 /usr/lib/chromium
|
as_root install -d -m 0755 /usr/lib/chromium
|
||||||
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||||
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
||||||
@@ -2302,6 +2316,11 @@ refresh_current_theme_after_upgrade() {
|
|||||||
# hooks because one of them runs `hyprctl reload`. Still poke terminal
|
# hooks because one of them runs `hyprctl reload`. Still poke terminal
|
||||||
# emulators so the active upgrade terminal picks up generated theme files.
|
# emulators so the active upgrade terminal picks up generated theme files.
|
||||||
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
|
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# apply_system_transition purged user-owned color.json. Headless theme-set
|
||||||
|
# skipped omarchy-theme-set-browser, so rewrite the colour here.
|
||||||
|
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
|
||||||
|
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Everything below mutates the system, so a non-zero exit from here on leaves a
|
# Everything below mutates the system, so a non-zero exit from here on leaves a
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ safe_icon_name() {
|
|||||||
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
|
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_plain_name() {
|
||||||
|
# The name becomes a filename. A slash would turn it into directory levels, so
|
||||||
|
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
|
||||||
|
# is stuck in the launcher; a leading ../ leaves the applications directory
|
||||||
|
# altogether. Refuse rather than silently renaming what the user typed -- most
|
||||||
|
# often it is a URL entered in the name field.
|
||||||
|
if [[ $1 == */* ]]; then
|
||||||
|
echo "App name cannot contain '/': $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
icon_name_from_ref() {
|
icon_name_from_ref() {
|
||||||
local ref="$1"
|
local ref="$1"
|
||||||
local name
|
local name
|
||||||
@@ -68,6 +80,7 @@ fetch_site_icon() {
|
|||||||
if (( $# < 3 )); then
|
if (( $# < 3 )); then
|
||||||
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
|
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
|
||||||
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
|
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
|
||||||
|
require_plain_name "$APP_NAME"
|
||||||
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
||||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||||
APP_URL="https://$APP_URL"
|
APP_URL="https://$APP_URL"
|
||||||
@@ -104,6 +117,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
require_plain_name "$APP_NAME"
|
||||||
|
|
||||||
if [[ -z $ICON_REF ]]; then
|
if [[ -z $ICON_REF ]]; then
|
||||||
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
||||||
mkdir -p "$ICON_DIR"
|
mkdir -p "$ICON_DIR"
|
||||||
@@ -132,8 +147,9 @@ fi
|
|||||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||||
|
|
||||||
# Create application .desktop file
|
# Create application .desktop file
|
||||||
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
|
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||||
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
|
||||||
|
mkdir -p "$DESKTOP_DIR"
|
||||||
|
|
||||||
cat >"$DESKTOP_FILE" <<EOF
|
cat >"$DESKTOP_FILE" <<EOF
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
|
|||||||
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
|
|||||||
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
|
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
|
||||||
DESKTOP_DIR="$HOME/.local/share/applications/"
|
DESKTOP_DIR="$HOME/.local/share/applications/"
|
||||||
|
|
||||||
if (( $# == 0 )); then
|
# Always index the launchers, so removal deletes the file that was found rather
|
||||||
# Find all web apps
|
# than a path rebuilt from the displayed name. Installs predating the name
|
||||||
while IFS= read -r -d '' file; do
|
# validation could nest the launcher inside directories, and those are exactly
|
||||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
# the ones a reconstructed path cannot reach.
|
||||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
WEB_APP_PATHS=()
|
||||||
fi
|
while IFS= read -r -d '' file; do
|
||||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
|
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||||
|
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||||
|
WEB_APP_PATHS+=("$file")
|
||||||
|
fi
|
||||||
|
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
|
||||||
|
|
||||||
|
# The launcher matching a chosen name, or empty when nothing was indexed under
|
||||||
|
# it (an app removed between the scan and the pick, say).
|
||||||
|
path_for_web_app() {
|
||||||
|
local wanted="$1" i
|
||||||
|
for i in "${!WEB_APPS[@]}"; do
|
||||||
|
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
|
||||||
|
printf '%s\n' "${WEB_APP_PATHS[$i]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( $# == 0 )); then
|
||||||
if ((${#WEB_APPS[@]})); then
|
if ((${#WEB_APPS[@]})); then
|
||||||
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
|
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
|
||||||
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
|
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
|
||||||
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
|
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
|
||||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
desktop_file=$(path_for_web_app "$APP_NAME")
|
||||||
|
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
||||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||||
|
|
||||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||||
|
|||||||
@@ -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
|
**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
|
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
|
## If it is an Omarchy bug
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
o.window(".*[Rr]esolve.*", {
|
o.window(".*[Rr]esolve.*", {
|
||||||
float = true,
|
float = true,
|
||||||
stay_focused = true,
|
stay_focused = true,
|
||||||
|
-- Prevent modal dialog pointer warps when focus follows the mouse.
|
||||||
|
no_follow_mouse = true,
|
||||||
tag = "-default-opacity",
|
tag = "-default-opacity",
|
||||||
opacity = "1 1",
|
opacity = "1 1",
|
||||||
})
|
})
|
||||||
|
|
||||||
o.window({ class = ".*[Rr]esolve.*", title = "^DaVinci Resolve( Studio)? - .+$" }, { fullscreen = true })
|
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,21 @@
|
|||||||
|
-- Disable a Hyprland input device whose name was stored as data, not Lua.
|
||||||
|
-- Device names come from USB descriptors and must never be loaded as code.
|
||||||
|
|
||||||
|
local paths = require("default.hypr.paths")
|
||||||
|
|
||||||
|
return function(kind)
|
||||||
|
-- Hardcoded to ~/.local/state to match omarchy-toggle-input-device and the
|
||||||
|
-- sibling bash toggle tools, which all write there regardless of
|
||||||
|
-- XDG_STATE_HOME.
|
||||||
|
local file = io.open(paths.home .. "/.local/state/omarchy/toggles/hypr/" .. kind .. "-disabled-name", "r")
|
||||||
|
if not file then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local name = file:read("*l")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
if name and name ~= "" then
|
||||||
|
hl.device({ name = name, enabled = false })
|
||||||
|
end
|
||||||
|
end
|
||||||
+13
-3
@@ -4,9 +4,19 @@
|
|||||||
|
|
||||||
local home = os.getenv("HOME")
|
local home = os.getenv("HOME")
|
||||||
|
|
||||||
|
-- A variable that is set but empty means "unset" (XDG Base Directory spec);
|
||||||
|
-- bash's ${VAR:-fallback} in the sibling tools treats it the same way.
|
||||||
|
local function env_or(name, fallback)
|
||||||
|
local value = os.getenv(name)
|
||||||
|
if value == nil or value == "" then
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
home = home,
|
home = home,
|
||||||
config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config"),
|
config_home = env_or("XDG_CONFIG_HOME", home .. "/.config"),
|
||||||
state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state"),
|
state_home = env_or("XDG_STATE_HOME", home .. "/.local/state"),
|
||||||
omarchy_path = os.getenv("OMARCHY_PATH") or "/usr/share/omarchy",
|
omarchy_path = env_or("OMARCHY_PATH", "/usr/share/omarchy"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
-- Pass a module prefix for normal package.path modules, e.g.
|
-- Pass a module prefix for normal package.path modules, e.g.
|
||||||
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
|
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
|
||||||
-- Pass nil as the prefix when the directory itself has been added to package.path.
|
-- Pass nil as the prefix when the directory itself has been added to package.path.
|
||||||
|
-- Pass options.exclude as a set of base names (without ".lua") to skip; a legacy
|
||||||
|
-- file that must never be loaded as code stays on disk for a migration to remove.
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -12,19 +14,23 @@ local function shell_quote(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.files(dir, module_prefix, options)
|
function M.files(dir, module_prefix, options)
|
||||||
|
local exclude = options and options.exclude or {}
|
||||||
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
|
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
|
||||||
if handle then
|
if handle then
|
||||||
for filename in handle:lines() do
|
for filename in handle:lines() do
|
||||||
local module = filename:gsub("%.lua$", "")
|
local name = filename:gsub("%.lua$", "")
|
||||||
if module_prefix then
|
if not exclude[name] then
|
||||||
module = module_prefix .. "." .. module
|
local module = name
|
||||||
end
|
if module_prefix then
|
||||||
|
module = module_prefix .. "." .. module
|
||||||
|
end
|
||||||
|
|
||||||
if options and options.reload then
|
if options and options.reload then
|
||||||
package.loaded[module] = nil
|
package.loaded[module] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
require(module)
|
require(module)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
handle:close()
|
handle:close()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ local require_all = require("default.hypr.require_all")
|
|||||||
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
|
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
|
||||||
package.path = toggles_dir .. "/?.lua;" .. package.path
|
package.path = toggles_dir .. "/?.lua;" .. package.path
|
||||||
|
|
||||||
require_all.files(toggles_dir, nil, { reload = true })
|
-- touchpad-disabled.lua / touchscreen-disabled.lua were generated Lua in older
|
||||||
|
-- versions and could carry an injected USB device name. They must never be loaded
|
||||||
|
-- as code again: exclude them so a not-yet-migrated install cannot execute a
|
||||||
|
-- leftover payload on reload. The migration recovers the name and deletes them.
|
||||||
|
require_all.files(toggles_dir, nil, {
|
||||||
|
reload = true,
|
||||||
|
exclude = {
|
||||||
|
["touchpad-disabled"] = true,
|
||||||
|
["touchscreen-disabled"] = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local disabled_input_device = require("default.hypr.disabled-input-device")
|
||||||
|
disabled_input_device("touchpad")
|
||||||
|
disabled_input_device("touchscreen")
|
||||||
|
|
||||||
require("default.hypr.workspace-layouts")
|
require("default.hypr.workspace-layouts")
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
|
|||||||
Include = /etc/pacman.d/mirrorlist
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
[omarchy]
|
[omarchy]
|
||||||
SigLevel = Optional TrustAll
|
|
||||||
Server = https://pkgs.omarchy.org/edge/$arch
|
Server = https://pkgs.omarchy.org/edge/$arch
|
||||||
|
|
||||||
# Repositories for debug symbol packages.
|
# Repositories for debug symbol packages.
|
||||||
|
|||||||
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
|||||||
Include = /etc/pacman.d/mirrorlist
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
[omarchy]
|
[omarchy]
|
||||||
SigLevel = Optional TrustAll
|
|
||||||
Server = https://pkgs.omarchy.org/edge/$arch
|
Server = https://pkgs.omarchy.org/edge/$arch
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
|
|||||||
Include = /etc/pacman.d/mirrorlist
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
[omarchy]
|
[omarchy]
|
||||||
SigLevel = Optional TrustAll
|
# OmarchyCN mirror first (registry-signed db and packages, key via cn/keys), upstream fallback
|
||||||
# OmarchyCN mirror first (registry-signed db, key via cn/keys), upstream fallback
|
|
||||||
Server = https://git.zacharyzhang.com/api/packages/ZacharyZhang-NY/arch/omarchy/x86_64
|
Server = https://git.zacharyzhang.com/api/packages/ZacharyZhang-NY/arch/omarchy/x86_64
|
||||||
Server = https://pkgs.omarchy.org/stable/$arch
|
Server = https://pkgs.omarchy.org/stable/$arch
|
||||||
|
|||||||
@@ -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_+.-]*)*$
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
|
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
|
||||||
|
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
|
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
|
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
|
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||||
|
browser_policy_setup_dir /etc/chromium/policies/managed
|
||||||
@@ -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
|
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
|
||||||
gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true
|
gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true
|
||||||
|
|
||||||
# Chromium policy directory for theme
|
|
||||||
mkdir -p /etc/chromium/policies/managed
|
|
||||||
chmod a+rw /etc/chromium/policies/managed
|
|
||||||
|
|
||||||
# Default Chromium to follow system appearance ("device") instead of dark
|
# Default Chromium to follow system appearance ("device") instead of dark
|
||||||
mkdir -p /usr/lib/chromium
|
mkdir -p /usr/lib/chromium
|
||||||
echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
|
echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
|
||||||
|
|||||||
@@ -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/fix-wifi7-eht.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.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-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-display.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-touchpad.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"
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ linux-firmware-marvell
|
|||||||
|
|
||||||
# Dell laptop support packages
|
# Dell laptop support packages
|
||||||
dell-xps-touchpad-haptics
|
dell-xps-touchpad-haptics
|
||||||
|
dell-xps13-sidecar-amps
|
||||||
|
|
||||||
# Speaker tunings (LV2 limiter every tuning ends in)
|
# Speaker tunings (LV2 limiter every tuning ends in)
|
||||||
lsp-plugins-lv2
|
lsp-plugins-lv2
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy
|
|||||||
| Suspend | — | `omarchy toggle suspend` |
|
| Suspend | — | `omarchy toggle suspend` |
|
||||||
| Hybrid GPU | — | `omarchy toggle hybrid gpu` |
|
| Hybrid GPU | — | `omarchy toggle hybrid gpu` |
|
||||||
|
|
||||||
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled state is written back out as a small Lua file that Hyprland sources on startup.
|
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled device's name is saved to a small state file that Hyprland reads on startup to disable it again.
|
||||||
|
|
||||||
The Toggle menu also carries a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, workspace layout (`Super + L`), window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
|
The Toggle menu also carries a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, workspace layout (`Super + L`), window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### Apple M1/M2 chips
|
### 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
|
### Apple Virtual Machine
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
echo "Take ownership of the FIDO2 authfile so it cannot be rewritten without root"
|
||||||
|
|
||||||
|
authfile="/etc/fido2/fido2"
|
||||||
|
|
||||||
|
# omarchy-migrate records this migration as complete whenever it exits zero, so
|
||||||
|
# a line printed here scrolls past once in the update terminal and is never
|
||||||
|
# shown again. The states below cannot be repaired without deciding what to do
|
||||||
|
# with a file we do not own, and they are exactly the ones where the authfile
|
||||||
|
# may already be under someone else's control, so say so where it outlives the
|
||||||
|
# scrollback as well.
|
||||||
|
report_unrepairable() {
|
||||||
|
echo " $1"
|
||||||
|
echo " $2"
|
||||||
|
omarchy-notification-send -u critical -g "FIDO2 authfile needs attention" "$1 $2" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nothing to repair on any machine that never set FIDO2 up, which is almost all
|
||||||
|
# of them. Checked before any sudo so those machines never see a password
|
||||||
|
# prompt. -L as well as -e: a dangling symlink is invisible to -e.
|
||||||
|
if [[ ! -L $authfile && ! -e $authfile ]]; then
|
||||||
|
# Absence and "cannot look" are the same answer to the tests above. The old
|
||||||
|
# setup created /etc/fido2 with `sudo mkdir -p`, which took the union of the
|
||||||
|
# caller's umask and sudoers' 0022, so anyone registering under `umask 077`
|
||||||
|
# left it mode 0700 with the user-owned authfile still inside. Escalate for
|
||||||
|
# that case alone -- a machine that never set FIDO2 up has no directory here
|
||||||
|
# and still reaches exit 0 without a password prompt. Not through a symlink:
|
||||||
|
# chmod would act on whatever it points at.
|
||||||
|
authdir=${authfile%/*}
|
||||||
|
|
||||||
|
if [[ -L $authdir || ! -d $authdir || -x $authdir ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask root whether a registration is behind it before touching the directory
|
||||||
|
# itself. An aborted setup that left an empty 0700 directory, or one an
|
||||||
|
# administrator deliberately keeps private, must not have its mode widened
|
||||||
|
# and its group and special bits discarded for a repair it does not need.
|
||||||
|
if ! sudo test -e "$authfile" && ! sudo test -L "$authfile"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo chmod 755 "$authdir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The old privileged move could install a symlink here if its fixed staging path
|
||||||
|
# was redirected. Reported, not repaired: chown follows symlinks and would take
|
||||||
|
# ownership of the target instead, and removing it would strip sudo and polkit
|
||||||
|
# from anyone whose only credential is the token.
|
||||||
|
if [[ -L $authfile ]]; then
|
||||||
|
report_unrepairable "$authfile is a symlink, not a regular file." \
|
||||||
|
"Leaving it alone. If you did not create it, remove it and re-run Setup > Security > Fido2."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A directory or a device here is no more ours to rewrite than a symlink is,
|
||||||
|
# and changing a directory's mode would alter an object we do not own.
|
||||||
|
if [[ ! -f $authfile ]]; then
|
||||||
|
report_unrepairable "$authfile is not a regular file." \
|
||||||
|
"Leaving it alone. Remove it and re-run Setup > Security > Fido2."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Migration state is per-user, so every account re-runs this. The file's own
|
||||||
|
# ownership is the state check: the second account finds the repair already
|
||||||
|
# done and exits without escalating.
|
||||||
|
owner=$(stat -c %U "$authfile" 2>/dev/null) || owner=""
|
||||||
|
group=$(stat -c %G "$authfile" 2>/dev/null) || group=""
|
||||||
|
mode=$(stat -c %a "$authfile" 2>/dev/null) || mode=""
|
||||||
|
if [[ $owner == "root" && $group == "root" && $mode == "644" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup used to `mv` this in from /tmp, which carried the invoking user's
|
||||||
|
# ownership into /etc. Root ownership stops that user from rewriting their own
|
||||||
|
# PAM credential without root. Mode 644 keeps the public credential mapping
|
||||||
|
# readable when pam_u2f opens an absolute authfile as the authenticating user.
|
||||||
|
#
|
||||||
|
# Rename a fresh copy over the path rather than chowning in place. A descriptor
|
||||||
|
# opened while the file was still the user's own stays writable on that inode
|
||||||
|
# through any later chmod or chown, since permission is checked at open(2), and
|
||||||
|
# pam_u2f resolving the path would keep landing on it. Replacing the inode
|
||||||
|
# leaves that descriptor writing to a file nothing reads.
|
||||||
|
stage=""
|
||||||
|
|
||||||
|
safe_stage_path() {
|
||||||
|
local candidate=$1
|
||||||
|
local prefix="$authfile.new."
|
||||||
|
local suffix
|
||||||
|
|
||||||
|
[[ $candidate == "$prefix"* ]] || return 1
|
||||||
|
suffix=${candidate#"$prefix"}
|
||||||
|
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stage() {
|
||||||
|
local status=$?
|
||||||
|
|
||||||
|
if safe_stage_path "$stage"; then
|
||||||
|
sudo rm -f -- "$stage" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_stage EXIT
|
||||||
|
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
||||||
|
|
||||||
|
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
||||||
|
echo " Could not create a safe staging file beside $authfile."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo install -T -m 644 -o root -g root "$authfile" "$stage"
|
||||||
|
sudo mv -Tf "$stage" "$authfile"
|
||||||
|
stage=""
|
||||||
|
trap - EXIT
|
||||||
@@ -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,39 @@
|
|||||||
|
echo "Store Hyprland input-device names as data instead of generated Lua"
|
||||||
|
|
||||||
|
# omarchy-toggle-input-device used to interpolate hyprctl device names into
|
||||||
|
# hyprctl eval and a generated Lua file. Those names come from USB descriptors,
|
||||||
|
# so recover the plain device name as data and delete the generated Lua. A name
|
||||||
|
# that could have broken out of the old Lua string literal is discarded, not
|
||||||
|
# trusted. The old script wrote to ~/.local/state regardless of XDG_STATE_HOME.
|
||||||
|
toggles_dir="$HOME/.local/state/omarchy/toggles/hypr"
|
||||||
|
|
||||||
|
reapply=0
|
||||||
|
|
||||||
|
for kind in touchpad touchscreen; do
|
||||||
|
state_file="$toggles_dir/$kind-disabled.lua"
|
||||||
|
name_file="$toggles_dir/$kind-disabled-name"
|
||||||
|
|
||||||
|
[[ -f $state_file ]] || continue
|
||||||
|
|
||||||
|
if [[ ! -f $name_file && -r $state_file ]]; then
|
||||||
|
old=$(<"$state_file")
|
||||||
|
pattern='^hl\.device\(\{ name = "([^"\\[:cntrl:]]+)", enabled = false \}\)$'
|
||||||
|
if [[ $old =~ $pattern ]]; then
|
||||||
|
printf '%s\n' "${BASH_REMATCH[1]}" >"$name_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$state_file"
|
||||||
|
|
||||||
|
if [[ -f $name_file ]]; then
|
||||||
|
reapply=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# The package hook reloads Hyprland before migrations run, so this session has
|
||||||
|
# already dropped the disable: the generated Lua is no longer loaded and the
|
||||||
|
# name file did not exist yet to replace it. Reload once more now that it does,
|
||||||
|
# or the device the user switched off stays on until their next login.
|
||||||
|
if (( reapply )); then
|
||||||
|
hyprctl reload >/dev/null 2>&1 || true
|
||||||
|
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
|
||||||
@@ -138,6 +138,7 @@ BorderSurface {
|
|||||||
radius: 0
|
radius: 0
|
||||||
}
|
}
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.tooltipText
|
text: root.tooltipText
|
||||||
color: root.tooltipForeground
|
color: root.tooltipForeground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -158,6 +159,7 @@ BorderSurface {
|
|||||||
spacing: Style.spacing.controlGap
|
spacing: Style.spacing.controlGap
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.iconText !== ""
|
visible: root.iconText !== ""
|
||||||
text: root.iconText
|
text: root.iconText
|
||||||
color: root.selected ? root._selectedColor : root.foreground
|
color: root.selected ? root._selectedColor : root.foreground
|
||||||
@@ -177,6 +179,7 @@ BorderSurface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.text !== ""
|
visible: root.text !== ""
|
||||||
text: root.text
|
text: root.text
|
||||||
color: root.selected ? root._selectedColor : root.foreground
|
color: root.selected ? root._selectedColor : root.foreground
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: messageText
|
id: messageText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -105,6 +106,7 @@ Item {
|
|||||||
radius: 0
|
radius: 0
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: modelData
|
text: modelData
|
||||||
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
|
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ Item {
|
|||||||
spacing: Style.spacing.labelGap
|
spacing: Style.spacing.labelGap
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.showLabel && root.label !== ""
|
visible: root.showLabel && root.label !== ""
|
||||||
text: root.label
|
text: root.label
|
||||||
color: Qt.darker(root.foreground, 1.4)
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
@@ -110,6 +111,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: chevron.left
|
anchors.right: chevron.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -214,6 +216,7 @@ Item {
|
|||||||
: "transparent"
|
: "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ Item {
|
|||||||
spacing: Style.spacing.labelGap
|
spacing: Style.spacing.labelGap
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.showLabel && root.label !== ""
|
visible: root.showLabel && root.label !== ""
|
||||||
text: root.label
|
text: root.label
|
||||||
color: Qt.darker(root.foreground, 1.4)
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
@@ -298,6 +299,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: chevron.left
|
anchors.right: chevron.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -451,6 +453,7 @@ Item {
|
|||||||
: Border.controlSpec("normal", root.foreground, root.accent)
|
: Border.controlSpec("normal", root.foreground, root.accent)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.loadingOptions ? "" : ""
|
text: root.loadingOptions ? "" : ""
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -486,6 +489,7 @@ Item {
|
|||||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: resultList.count === 0
|
visible: resultList.count === 0
|
||||||
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
|
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
|
||||||
@@ -581,6 +585,7 @@ Item {
|
|||||||
spacing: Style.spacing.xxs
|
spacing: Style.spacing.xxs
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: modelData.label
|
text: modelData.label
|
||||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -589,6 +594,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
text: modelData.description
|
text: modelData.description
|
||||||
color: Qt.darker(root.foreground, 1.5)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Column {
|
|||||||
spacing: Style.spacing.md
|
spacing: Style.spacing.md
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.label !== ""
|
visible: root.label !== ""
|
||||||
text: root.label
|
text: root.label
|
||||||
color: Qt.darker(root.foreground, 1.4)
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: glyph
|
id: glyph
|
||||||
|
textFormat: Text.PlainText
|
||||||
// Keep the shared line box and baseline intact. Correcting only the
|
// Keep the shared line box and baseline intact. Correcting only the
|
||||||
// horizontal painted bounds avoids per-glyph vertical drift.
|
// horizontal painted bounds avoids per-glyph vertical drift.
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ BorderSurface {
|
|||||||
Behavior on color { ColorAnimation { duration: 60 } }
|
Behavior on color { ColorAnimation { duration: 60 } }
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.iconText
|
text: root.iconText
|
||||||
color: root.enabled
|
color: root.enabled
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.title !== ""
|
visible: root.title !== ""
|
||||||
text: root.title
|
text: root.title
|
||||||
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
|
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
|
||||||
@@ -75,6 +76,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: detailText
|
id: detailText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.detail
|
text: root.detail
|
||||||
color: root.dim
|
color: root.dim
|
||||||
@@ -87,6 +89,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: metaText
|
id: metaText
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.meta.toUpperCase()
|
text: root.meta.toUpperCase()
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ Text {
|
|||||||
property string fontFamily: Style.font.family
|
property string fontFamily: Style.font.family
|
||||||
property real fontSize: Style.font.caption
|
property real fontSize: Style.font.caption
|
||||||
|
|
||||||
|
// Callers bind `text` from outside this file, so the default has to be set
|
||||||
|
// here. AutoText would let a section title that happens to carry a device or
|
||||||
|
// network name promote itself to rich text.
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: Qt.darker(foreground, 1.4)
|
color: Qt.darker(foreground, 1.4)
|
||||||
font.family: fontFamily
|
font.family: fontFamily
|
||||||
font.pixelSize: fontSize
|
font.pixelSize: fontSize
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ ToolTip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.text
|
text: root.text
|
||||||
color: root.panelForeground
|
color: root.panelForeground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ Item {
|
|||||||
spacing: Style.spacing.labelGap
|
spacing: Style.spacing.labelGap
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.showLabel && root.label !== ""
|
visible: root.showLabel && root.label !== ""
|
||||||
text: root.label
|
text: root.label
|
||||||
color: Qt.darker(root.foreground, 1.4)
|
color: Qt.darker(root.foreground, 1.4)
|
||||||
@@ -132,6 +133,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: chevron.left
|
anchors.right: chevron.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -246,6 +248,7 @@ Item {
|
|||||||
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: resultList.count === 0
|
visible: resultList.count === 0
|
||||||
text: root.emptyText
|
text: root.emptyText
|
||||||
@@ -313,6 +316,7 @@ Item {
|
|||||||
spacing: Style.spacing.xxs
|
spacing: Style.spacing.xxs
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.optionLabel(modelData)
|
text: root.optionLabel(modelData)
|
||||||
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -321,6 +325,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
text: root.optionDescription(modelData)
|
text: root.optionDescription(modelData)
|
||||||
color: Qt.darker(root.foreground, 1.5)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ PanelWindow {
|
|||||||
spacing: Style.space(16)
|
spacing: Style.space(16)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.title !== ""
|
visible: root.title !== ""
|
||||||
text: root.title.toUpperCase()
|
text: root.title.toUpperCase()
|
||||||
color: root.onScrimDim
|
color: root.onScrimDim
|
||||||
@@ -182,6 +183,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.failed
|
visible: root.failed
|
||||||
text: root.error
|
text: root.error
|
||||||
color: root.onScrimUrgent
|
color: root.onScrimUrgent
|
||||||
@@ -368,6 +370,7 @@ PanelWindow {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
// Both branches go through the locale: a reading is a measurement, so
|
// Both branches go through the locale: a reading is a measurement, so
|
||||||
// its separators follow the system's number conventions rather than the
|
// its separators follow the system's number conventions rather than the
|
||||||
@@ -383,6 +386,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: root.unit
|
text: root.unit
|
||||||
color: root.onScrimDim
|
color: root.onScrimDim
|
||||||
@@ -394,6 +398,7 @@ PanelWindow {
|
|||||||
// The 90° gap at the bottom of the scale is where a cluster prints its
|
// The 90° gap at the bottom of the scale is where a cluster prints its
|
||||||
// unit; here it names the direction.
|
// unit; here it names the direction.
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
text: dial.label
|
text: dial.label
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ BorderSurface {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.label
|
text: root.label
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -79,6 +80,7 @@ BorderSurface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.description !== ""
|
visible: root.description !== ""
|
||||||
text: root.description
|
text: root.description
|
||||||
color: Qt.darker(root.foreground, 1.5)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: label
|
id: label
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.labelVisible
|
visible: root.labelVisible
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.text
|
text: root.text
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: heroMarkImage.status !== Image.Ready
|
visible: heroMarkImage.status !== Image.Ready
|
||||||
text: button.text
|
text: button.text
|
||||||
@@ -504,6 +505,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: statusText
|
id: statusText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -558,6 +560,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: balanceValue
|
id: balanceValue
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
|
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
|
||||||
color: root.balanceAlarming ? root.urgent : root.foreground
|
color: root.balanceAlarming ? root.urgent : root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -575,6 +578,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: root.balanceDetailText(root.balance)
|
text: root.balanceDetailText(root.balance)
|
||||||
@@ -680,6 +684,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
topPadding: Style.space(2)
|
topPadding: Style.space(2)
|
||||||
@@ -710,6 +715,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: limitLabel
|
id: limitLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
// A model-scoped window is titled after its model, and those names run
|
// A model-scoped window is titled after its model, and those names run
|
||||||
// long enough to reach the percentage, so the title gives way first.
|
// long enough to reach the percentage, so the title gives way first.
|
||||||
text: limitRow.window ? limitRow.window.title : ""
|
text: limitRow.window ? limitRow.window.title : ""
|
||||||
@@ -725,6 +731,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: limitValue
|
id: limitValue
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: limitRow.window && limitRow.window.percent >= 0
|
text: limitRow.window && limitRow.window.percent >= 0
|
||||||
? Math.round(limitRow.window.percent * 100) + "%"
|
? Math.round(limitRow.window.percent * 100) + "%"
|
||||||
: "—"
|
: "—"
|
||||||
@@ -744,6 +751,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: resetText
|
id: resetText
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: {
|
text: {
|
||||||
var remainingMs = root.resetMsFor(limitRow.window)
|
var remainingMs = root.resetMsFor(limitRow.window)
|
||||||
@@ -798,6 +806,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: dayLabel
|
id: dayLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
|
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
|
||||||
color: dayRow.today ? root.foreground : root.dim
|
color: dayRow.today ? root.foreground : root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -835,6 +844,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: dayValue
|
id: dayValue
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
|
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
|
||||||
color: dayRow.today ? root.foreground : root.dim
|
color: dayRow.today ? root.foreground : root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -890,6 +900,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: modelName
|
id: modelName
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: modelRow.row ? modelRow.row.name : ""
|
text: modelRow.row ? modelRow.row.name : ""
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -904,6 +915,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: modelTokens
|
id: modelTokens
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
|
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
|
||||||
color: root.dim
|
color: root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
|
|||||||
@@ -1090,6 +1090,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: tooltipLabel
|
id: tooltipLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.tooltipText
|
text: root.tooltipText
|
||||||
color: Color.tooltip.text
|
color: Color.tooltip.text
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ BarWidget {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: labelText
|
id: labelText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: rowIcon.right
|
anchors.left: rowIcon.right
|
||||||
anchors.leftMargin: Style.space(10)
|
anchors.leftMargin: Style.space(10)
|
||||||
@@ -577,6 +578,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Style.space(28)
|
anchors.leftMargin: Style.space(28)
|
||||||
@@ -681,6 +683,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -709,6 +712,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: !menuRow.modelData.isSeparator
|
visible: !menuRow.modelData.isSeparator
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ Item {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -500,6 +501,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
|
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
|
||||||
height: parent.height
|
height: parent.height
|
||||||
text: parent.parent.previewText
|
text: parent.parent.previewText
|
||||||
@@ -546,6 +548,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: parent.activeRow && !parent.activeRow.previewImage
|
visible: parent.activeRow && !parent.activeRow.previewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: root.contentMargin
|
anchors.leftMargin: root.contentMargin
|
||||||
@@ -593,6 +596,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
|
|||||||
@@ -519,12 +519,14 @@ Item {
|
|||||||
width: Style.space(140)
|
width: Style.space(140)
|
||||||
spacing: Style.space(1)
|
spacing: Style.space(1)
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: "Style.font." + modelData.key
|
text: "Style.font." + modelData.key
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: modelData.size + " px"
|
text: modelData.size + " px"
|
||||||
color: Qt.darker(root.foreground, 1.5)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -534,6 +536,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: sampleText
|
id: sampleText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: metaCol.right
|
anchors.left: metaCol.right
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -574,6 +577,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.font.family
|
text: Style.font.family
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -587,6 +591,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.font.resolvedFamily
|
text: Style.font.resolvedFamily
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -600,6 +605,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.font.baseSize + " px"
|
text: Style.font.baseSize + " px"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -613,6 +619,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.bar.sizeHorizontal + " px"
|
text: Style.bar.sizeHorizontal + " px"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -626,6 +633,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.bar.sizeVertical + " px"
|
text: Style.bar.sizeVertical + " px"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -639,6 +647,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.spacing.scale.toFixed(2)
|
text: Style.spacing.scale.toFixed(2)
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -652,6 +661,7 @@ Item {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Style.spacing.panelPadding + " px"
|
text: Style.spacing.panelPadding + " px"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -818,6 +828,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: csLabel
|
id: csLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -1273,6 +1284,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
|
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ Item {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -284,6 +285,7 @@ Item {
|
|||||||
color: hasCursor ? root.selectedBackground : "transparent"
|
color: hasCursor ? root.selectedBackground : "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: parent.emoji
|
text: parent.emoji
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.display
|
font.pixelSize: Style.font.display
|
||||||
@@ -326,6 +328,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: "No matches for “" + root.filterText + "”"
|
text: "No matches for “" + root.filterText + "”"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: selectedLabel
|
id: selectedLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.showLabels
|
visible: root.showLabels
|
||||||
anchors.top: carousel.bottom
|
anchors.top: carousel.bottom
|
||||||
anchors.topMargin: Style.space(16)
|
anchors.topMargin: Style.space(16)
|
||||||
@@ -561,6 +562,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.filterable && root.filterText
|
visible: root.filterable && root.filterText
|
||||||
anchors.top: selectedLabel.bottom
|
anchors.top: selectedLabel.bottom
|
||||||
anchors.topMargin: Style.space(8)
|
anchors.topMargin: Style.space(8)
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.fill: passwordInput
|
anchors.fill: passwordInput
|
||||||
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
||||||
visible: passwordInput.text.length === 0
|
visible: passwordInput.text.length === 0
|
||||||
|
|||||||
@@ -1199,6 +1199,7 @@ Item {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -1287,6 +1288,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: iconText
|
id: iconText
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: row.hasIcon && !row.isApp
|
visible: row.hasIcon && !row.isApp
|
||||||
text: row.icon
|
text: row.icon
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
color: row.hasCursor ? root.selectedText : root.foreground
|
||||||
@@ -1328,6 +1330,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: labelText
|
id: labelText
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: row.label
|
text: row.label
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
color: row.hasCursor ? root.selectedText : root.foreground
|
||||||
@@ -1338,6 +1341,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: row.detail
|
text: row.detail
|
||||||
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
|
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
|
||||||
@@ -1358,6 +1362,7 @@ Item {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: false
|
visible: false
|
||||||
text: row.childCount
|
text: row.childCount
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -1368,6 +1373,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: row.kind === "menu" || row.kind === "link" ? "›" : ""
|
text: row.kind === "menu" || row.kind === "link" ? "›" : ""
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
color: row.hasCursor ? root.selectedText : root.foreground
|
||||||
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
|
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
|
||||||
@@ -1452,6 +1458,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
|
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
|
|||||||
@@ -5,8 +5,92 @@ function isChromiumDerived(app, appIcon) {
|
|||||||
source.indexOf("opera") >= 0
|
source.indexOf("opera") >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when a `<...>` run is an image tag, so the name is read the way Qt's
|
||||||
|
// parser reads it: after the `<`, the leading run of letters and digits.
|
||||||
|
//
|
||||||
|
// Skip everything up to that run rather than matching the separator, because
|
||||||
|
// there is no JavaScript expression for what Qt skips. QQuickStyledText calls
|
||||||
|
// skipSpace(), which is QChar::isSpace(), and that set is not `\s`: Qt counts
|
||||||
|
// U+0085 NEL and `\s` does not, while `\s` counts U+FEFF and Qt does not. A
|
||||||
|
// name read with `\s` therefore misses a tag written as `<`, U+0085, `img`:
|
||||||
|
// Qt skips the NEL, reads `img` and issues the GET, while the regex finds no
|
||||||
|
// name at all and the tag is kept. Measured against Qt 6.11.2.
|
||||||
|
//
|
||||||
|
// Over-skipping is the safe direction. It can only classify more runs as
|
||||||
|
// images, and dropping a run never manufactures a tag: a dropped run joins two
|
||||||
|
// stretches of text that each contain no `<`.
|
||||||
|
function isImageTag(tag) {
|
||||||
|
var name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
|
||||||
|
return !!name && name[1].toLowerCase() === "img"
|
||||||
|
}
|
||||||
|
|
||||||
|
// The body renders as StyledText so notifications can use the markup the
|
||||||
|
// body-markup capability advertises (see Service.qml). StyledText honours
|
||||||
|
// <img src>, and a remote src makes the shell issue an unauthenticated GET
|
||||||
|
// with no user action, so image tags go before the renderer sees them.
|
||||||
|
//
|
||||||
|
// Work in whole tags, never in substrings of one. A `<` opens a tag that runs
|
||||||
|
// to the next `>`, nested `<` and all, and only a tag whose own name is `img`
|
||||||
|
// is dropped.
|
||||||
|
//
|
||||||
|
// That is the conservative bound, not Qt's exact one: Qt lets a `>` inside a
|
||||||
|
// quoted attribute value pass without closing the tag, so a Qt tag can be
|
||||||
|
// longer than the run taken here. Do not "correct" this to match Qt. Taking
|
||||||
|
// the shorter run only ever splits one Qt tag into several, and a split can
|
||||||
|
// only expose an `<img` to be dropped, never hide one — whereas honouring
|
||||||
|
// quotes would let `<b title="a>b"><img src="http://host/x.png">` through.
|
||||||
|
//
|
||||||
|
// Deleting a substring is what makes a naive `/<img[^>]*>/g` unsafe. Given
|
||||||
|
//
|
||||||
|
// <im<img src="http://a/decoy.png">g src="http://a/beacon.png">
|
||||||
|
//
|
||||||
|
// Qt reads ONE malformed tag named `im` and renders nothing, but removing the
|
||||||
|
// inner match closes the surviving halves up into `<img src=".../beacon.png">`
|
||||||
|
// — a live tag the input never contained. The stripper would be manufacturing
|
||||||
|
// the very thing it exists to remove.
|
||||||
|
//
|
||||||
|
// Because every `<` opens a tag, the text between tags never contains one, so
|
||||||
|
// dropping a tag cannot splice its neighbours into a new one. That makes a
|
||||||
|
// single pass sufficient, with no re-scanning and no input bound to police.
|
||||||
|
function stripImageTags(text) {
|
||||||
|
var out = ""
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
while (i < text.length) {
|
||||||
|
var open = text.indexOf("<", i)
|
||||||
|
if (open === -1) {
|
||||||
|
out += text.slice(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
out += text.slice(i, open)
|
||||||
|
|
||||||
|
// An unterminated tag at the end of the string still reaches the renderer,
|
||||||
|
// which closes it itself, so treat the remainder as one tag.
|
||||||
|
var close = text.indexOf(">", open)
|
||||||
|
var tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
|
||||||
|
|
||||||
|
if (!isImageTag(tag)) out += tag
|
||||||
|
i = close === -1 ? text.length : close + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the card renders, and the last thing to touch the string before Qt parses
|
||||||
|
// it. The newline rewrite belongs here rather than in the card because it inserts
|
||||||
|
// `<br/>` into text stripImageTags chose to KEEP, and a kept tag may hold a `<` of
|
||||||
|
// its own: `<x`, newline, `<img src="http://…">` is one tag named `x` to both the
|
||||||
|
// stripper and Qt, until the rewrite splits it into `<x<br/>` and a live image tag
|
||||||
|
// the input never contained. Measured against Qt 6.11.2 — the rewritten form
|
||||||
|
// fetches, the original does not. So strip again after, and what Qt parses is what
|
||||||
|
// was checked last.
|
||||||
|
function styledBody(body, app, appIcon) {
|
||||||
|
return stripImageTags(sanitizeBody(body, app, appIcon).replace(/\r\n|\r|\n/g, "<br/>"))
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeBody(body, app, appIcon) {
|
function sanitizeBody(body, app, appIcon) {
|
||||||
var text = String(body || "").replace(/<img[^>]*>/gi, "")
|
var text = stripImageTags(String(body || ""))
|
||||||
if (!isChromiumDerived(app, appIcon)) return text
|
if (!isChromiumDerived(app, appIcon)) return text
|
||||||
|
|
||||||
return text
|
return text
|
||||||
@@ -366,6 +450,7 @@ if (typeof module !== "undefined") {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
isChromiumDerived: isChromiumDerived,
|
isChromiumDerived: isChromiumDerived,
|
||||||
sanitizeBody: sanitizeBody,
|
sanitizeBody: sanitizeBody,
|
||||||
|
styledBody: styledBody,
|
||||||
summaryStartsWithGlyph: summaryStartsWithGlyph,
|
summaryStartsWithGlyph: summaryStartsWithGlyph,
|
||||||
shouldBypassDnd: shouldBypassDnd,
|
shouldBypassDnd: shouldBypassDnd,
|
||||||
isEphemeralApp: isEphemeralApp,
|
isEphemeralApp: isEphemeralApp,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ BorderSurface {
|
|||||||
readonly property bool singleLineToast: sanitizedBody.length === 0
|
readonly property bool singleLineToast: sanitizedBody.length === 0
|
||||||
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
|
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
|
||||||
readonly property string sanitizedBody: sanitizeBody(body)
|
readonly property string sanitizedBody: sanitizeBody(body)
|
||||||
readonly property string styledBody: sanitizedBody.replace(/\r\n|\r|\n/g, "<br/>")
|
readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon)
|
||||||
|
|
||||||
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
|
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
|
||||||
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
|
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
|
||||||
@@ -133,6 +133,7 @@ BorderSurface {
|
|||||||
// Glyph fallback (Nerd Font character) when no image icon is
|
// Glyph fallback (Nerd Font character) when no image icon is
|
||||||
// available. Used by omarchy-notification-send's `-g` flag.
|
// available. Used by omarchy-notification-send's `-g` flag.
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
|
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
|
||||||
text: root.glyph
|
text: root.glyph
|
||||||
@@ -143,6 +144,7 @@ BorderSurface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: root.compactGlyph
|
visible: root.compactGlyph
|
||||||
text: root.glyph
|
text: root.glyph
|
||||||
@@ -159,6 +161,11 @@ BorderSurface {
|
|||||||
spacing: Style.space(2)
|
spacing: Style.space(2)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
// The spec defines the summary as a single line of plain text, so
|
||||||
|
// AutoText could only ever promote a hostile string to rich text.
|
||||||
|
// The body below is StyledText on purpose — see Service.qml's
|
||||||
|
// bodyMarkupSupported — and is stripped in NotificationLogic.
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: root.summary.length > 0
|
visible: root.summary.length > 0
|
||||||
text: root.summary
|
text: root.summary
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ Item {
|
|||||||
width: root.iconWidth
|
width: root.iconWidth
|
||||||
height: parent.height
|
height: parent.height
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
// Sit the glyph's ink flush in the column, centered when the
|
// Sit the glyph's ink flush in the column, centered when the
|
||||||
// column is wider than this particular glyph.
|
// column is wider than this particular glyph.
|
||||||
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
||||||
@@ -186,6 +187,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.message !== ""
|
visible: root.message !== ""
|
||||||
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
||||||
// The readout hugs the card edge so a short percentage doesn't leave
|
// The readout hugs the card edge so a short percentage doesn't leave
|
||||||
|
|||||||
@@ -711,6 +711,7 @@ Panel {
|
|||||||
// Status only — the switch owns muting, mouse and keyboard alike.
|
// Status only — the switch owns muting, mouse and keyboard alike.
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.outputIcon()
|
text: root.outputIcon()
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -761,6 +762,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroLabel
|
id: heroLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.outputVolumeName(
|
text: root.outputVolumeName(
|
||||||
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
|
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
|
||||||
root.outputMuted
|
root.outputMuted
|
||||||
@@ -800,6 +802,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: outputPercent
|
id: outputPercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
|
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -886,6 +889,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: microphonePercent
|
id: microphonePercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
|
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1030,6 +1034,7 @@ Panel {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.sinkGlyph(sinkRow.node)
|
text: root.sinkGlyph(sinkRow.node)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1040,6 +1045,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.nodeLabel(sinkRow.node)
|
text: root.nodeLabel(sinkRow.node)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1089,6 +1095,7 @@ Panel {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.sourceGlyph(sourceRow.node)
|
text: root.sourceGlyph(sourceRow.node)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1099,6 +1106,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.nodeLabel(sourceRow.node)
|
text: root.nodeLabel(sourceRow.node)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1159,6 +1167,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: streamMuteIcon
|
id: streamMuteIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: streamRow.streamMuted ? "" : ""
|
text: streamRow.streamMuted ? "" : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1179,6 +1188,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.streamLabel(streamRow.node)
|
text: root.streamLabel(streamRow.node)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1191,6 +1201,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: streamPct
|
id: streamPct
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Math.round(streamRow.streamVolume * 100) + "%"
|
text: Math.round(streamRow.streamVolume * 100) + "%"
|
||||||
color: Qt.darker(root.bar.foreground, 1.5)
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
|
|||||||
@@ -698,6 +698,7 @@ Panel {
|
|||||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.icon
|
text: root.icon
|
||||||
@@ -748,6 +749,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroStatus
|
id: heroStatus
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.heroStatusText.toUpperCase()
|
text: root.heroStatusText.toUpperCase()
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -863,6 +865,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
|
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
|
||||||
text: !root.adapter ? "No Bluetooth adapter"
|
text: !root.adapter ? "No Bluetooth adapter"
|
||||||
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
|
||||||
@@ -971,6 +974,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: deviceIcon
|
id: deviceIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: row.isConnected ? "" : ""
|
text: row.isConnected ? "" : ""
|
||||||
color: row.statusColor
|
color: row.statusColor
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -989,6 +993,7 @@ Panel {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.deviceLabel(row.dev) || "Device"
|
text: root.deviceLabel(row.dev) || "Device"
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -997,6 +1002,7 @@ Panel {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: row.statusText !== ""
|
visible: row.statusText !== ""
|
||||||
text: row.statusText
|
text: row.statusText
|
||||||
color: row.statusColor
|
color: row.statusColor
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroDate
|
id: heroDate
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: Qt.formatDate(root.today, "MMMM d")
|
text: Qt.formatDate(root.today, "MMMM d")
|
||||||
color: heroMouse.containsMouse
|
color: heroMouse.containsMouse
|
||||||
@@ -413,6 +414,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: yearLabel
|
id: yearLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: !root.editingLife
|
visible: !root.editingLife
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -425,6 +427,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: yearPercent
|
id: yearPercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: !root.editingLife
|
visible: !root.editingLife
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -485,6 +488,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: lifePercent
|
id: lifePercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.lifeDonePercent + "%"
|
text: root.lifeDonePercent + "%"
|
||||||
@@ -608,6 +612,7 @@ Panel {
|
|||||||
model: root.weekdays
|
model: root.weekdays
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
required property var modelData
|
required property var modelData
|
||||||
width: root.cellWidth
|
width: root.cellWidth
|
||||||
height: Style.space(16)
|
height: Style.space(16)
|
||||||
@@ -631,6 +636,7 @@ Panel {
|
|||||||
spacing: root.cellSpacing
|
spacing: root.cellSpacing
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: root.weekColumnWidth
|
width: root.weekColumnWidth
|
||||||
height: root.cellHeight
|
height: root.cellHeight
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
@@ -662,6 +668,7 @@ Panel {
|
|||||||
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
|
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: modelData.day
|
text: modelData.day
|
||||||
color: modelData.inMonth
|
color: modelData.inMonth
|
||||||
@@ -707,6 +714,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: monthLabel
|
id: monthLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
// Fixed width so the chevrons hold still between a
|
// Fixed width so the chevrons hold still between a
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
||||||
@@ -421,6 +422,7 @@ Panel {
|
|||||||
spacing: Style.space(1)
|
spacing: Style.space(1)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -430,6 +432,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
||||||
color: root.dim
|
color: root.dim
|
||||||
@@ -478,6 +481,7 @@ Panel {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Model.fileGlyph(fileRow.fileName)
|
text: Model.fileGlyph(fileRow.fileName)
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -491,6 +495,7 @@ Panel {
|
|||||||
spacing: Style.space(1)
|
spacing: Style.space(1)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: fileRow.fileName
|
text: fileRow.fileName
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -500,6 +505,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: Model.fileMeta(fileRow.file)
|
text: Model.fileMeta(fileRow.file)
|
||||||
color: root.dim
|
color: root.dim
|
||||||
@@ -524,6 +530,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoLabel: Text {
|
component InfoLabel: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -531,6 +538,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoValue: Text {
|
component InfoValue: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.displays.length > 1 ? "" : ""
|
text: root.displays.length > 1 ? "" : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -559,6 +560,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroLabel
|
id: heroLabel
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: {
|
text: {
|
||||||
if (root.brightnessAvailable) {
|
if (root.brightnessAvailable) {
|
||||||
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
|
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
|
||||||
@@ -602,6 +604,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: brightnessPercent
|
id: brightnessPercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -674,6 +677,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: textSizePx
|
id: textSizePx
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: (textSizeSlider.dragging
|
text: (textSizeSlider.dragging
|
||||||
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
|
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
|
||||||
: root.displayedTextPx()) + "px"
|
: root.displayedTextPx()) + "px"
|
||||||
@@ -747,6 +751,7 @@ Panel {
|
|||||||
// focused one.
|
// focused one.
|
||||||
Text {
|
Text {
|
||||||
id: scaleMonitor
|
id: scaleMonitor
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.focusedMonitor
|
text: root.focusedMonitor
|
||||||
// Only worth naming when more than one display is in play.
|
// Only worth naming when more than one display is in play.
|
||||||
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
|
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
|
||||||
@@ -887,6 +892,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
|
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -897,6 +903,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: monitorRow.display.enabled ? "" : ""
|
text: monitorRow.display.enabled ? "" : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
|
|||||||
@@ -1090,6 +1090,7 @@ Panel {
|
|||||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.icon
|
text: root.icon
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1170,6 +1171,7 @@ Panel {
|
|||||||
// rather than in a pill, which crowded the on/off switch.
|
// rather than in a pill, which crowded the on/off switch.
|
||||||
Text {
|
Text {
|
||||||
id: heroSsid
|
id: heroSsid
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
readonly property string title: {
|
readonly property string title: {
|
||||||
@@ -1189,6 +1191,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroMeta
|
id: heroMeta
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: {
|
text: {
|
||||||
if (root.info.type === "wifi") {
|
if (root.info.type === "wifi") {
|
||||||
@@ -1711,6 +1714,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: networkIcon
|
id: networkIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: row.net ? root.wifiIconFor(row.net.signal) : ""
|
text: row.net ? root.wifiIconFor(row.net.signal) : ""
|
||||||
color: row.statusColor
|
color: row.statusColor
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1732,6 +1736,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: lockIndicator
|
id: lockIndicator
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: row.requiresCredentials || row.forgetVisible
|
visible: row.requiresCredentials || row.forgetVisible
|
||||||
width: parent.width
|
width: parent.width
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -1779,6 +1784,7 @@ Panel {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: row.net ? (row.net.ssid || "Hidden") : ""
|
text: row.net ? (row.net.ssid || "Hidden") : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1787,6 +1793,7 @@ Panel {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
// Signal strength is conveyed by the wifi-bars icon and the
|
// Signal strength is conveyed by the wifi-bars icon and the
|
||||||
// right-edge glyph/buttons carry protection or forget affordances,
|
// right-edge glyph/buttons carry protection or forget affordances,
|
||||||
// so the second line only carries action status (Connecting…,
|
// so the second line only carries action status (Connecting…,
|
||||||
@@ -1893,6 +1900,7 @@ Panel {
|
|||||||
radius: Style.cornerRadius
|
radius: Style.cornerRadius
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
@@ -1946,6 +1954,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoLabel: Text {
|
component InfoLabel: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -1953,6 +1962,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoValue: Text {
|
component InfoValue: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.batteryIcon()
|
text: root.batteryIcon()
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -356,6 +357,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroStatus
|
id: heroStatus
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.heroStatusText.toUpperCase()
|
text: root.heroStatusText.toUpperCase()
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -369,6 +371,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroPercent
|
id: heroPercent
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.batteryInfo.percentage || "—"
|
text: root.batteryInfo.percentage || "—"
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -517,6 +520,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoLabel: Text {
|
component InfoLabel: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -524,6 +528,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component InfoValue: Text {
|
component InfoValue: Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
|
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
|
||||||
@@ -841,6 +842,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: accountRow.accountText
|
text: accountRow.accountText
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -933,6 +935,7 @@ Panel {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: tailscale.osIcon(peer ? peer.OS : "")
|
text: tailscale.osIcon(peer ? peer.OS : "")
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -946,6 +949,7 @@ Panel {
|
|||||||
spacing: Style.space(1)
|
spacing: Style.space(1)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: peerRow.peerName
|
text: peerRow.peerName
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -955,6 +959,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: {
|
text: {
|
||||||
var parts = []
|
var parts = []
|
||||||
@@ -1087,6 +1092,7 @@ Panel {
|
|||||||
spacing: Style.space(10)
|
spacing: Style.space(10)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: copyChoice.label
|
text: copyChoice.label
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -1134,6 +1140,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: exitNodeGlyph
|
id: exitNodeGlyph
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "" : "")
|
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "" : "")
|
||||||
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
|
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -1154,6 +1161,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: exitNodeRow.peerName
|
text: exitNodeRow.peerName
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -1224,6 +1232,7 @@ Panel {
|
|||||||
spacing: Style.space(1)
|
spacing: Style.space(1)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: regionRow.regionName
|
text: regionRow.regionName
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
@@ -1234,6 +1243,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: regionRow.regionDetail
|
text: regionRow.regionDetail
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.verticalCenterOffset: 5
|
anchors.verticalCenterOffset: 5
|
||||||
text: root.label || "—"
|
text: root.label || "—"
|
||||||
@@ -547,6 +548,7 @@ Panel {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: tempBig
|
id: tempBig
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.reportTempNum || "—"
|
text: root.reportTempNum || "—"
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -556,6 +558,7 @@ Panel {
|
|||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.current ? root.tempUnit : ""
|
text: root.current ? root.tempUnit : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -593,6 +596,7 @@ Panel {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: (root.reportLocation || "").toUpperCase()
|
text: (root.reportLocation || "").toUpperCase()
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -643,6 +647,7 @@ Panel {
|
|||||||
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.savingLocation ? "" : "✕"
|
text: root.savingLocation ? "" : "✕"
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -683,6 +688,7 @@ Panel {
|
|||||||
font.letterSpacing: 1
|
font.letterSpacing: 1
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.reportFeels
|
text: root.reportFeels
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -700,6 +706,7 @@ Panel {
|
|||||||
font.letterSpacing: 1
|
font.letterSpacing: 1
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.reportWind
|
text: root.reportWind
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -717,6 +724,7 @@ Panel {
|
|||||||
font.letterSpacing: 1
|
font.letterSpacing: 1
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.reportHumidity
|
text: root.reportHumidity
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -752,12 +760,14 @@ Panel {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: modelData.name
|
text: modelData.name
|
||||||
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
|
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: Style.font.body
|
font.pixelSize: Style.font.body
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
text: modelData.description
|
text: modelData.description
|
||||||
color: Qt.darker(root.bar.foreground, 1.5)
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
@@ -817,6 +827,7 @@ Panel {
|
|||||||
spacing: Style.space(10)
|
spacing: Style.space(10)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.dayIcon(modelData)
|
text: root.dayIcon(modelData)
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
@@ -829,6 +840,7 @@ Panel {
|
|||||||
spacing: Style.space(2)
|
spacing: Style.space(2)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.dayName(modelData.date).toUpperCase()
|
text: root.dayName(modelData.date).toUpperCase()
|
||||||
color: Qt.darker(root.bar.foreground, 1.4)
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -840,12 +852,14 @@ Panel {
|
|||||||
spacing: Style.space(6)
|
spacing: Style.space(6)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.bareTempForDay(modelData, "max")
|
text: root.bareTempForDay(modelData, "max")
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: Style.font.body
|
font.pixelSize: Style.font.body
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.bareTempForDay(modelData, "min")
|
text: root.bareTempForDay(modelData, "min")
|
||||||
color: Qt.darker(root.bar.foreground, 1.5)
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ Item {
|
|||||||
spacing: Style.space(16)
|
spacing: Style.space(16)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: (root.ssid || "Wi-Fi").toUpperCase()
|
text: (root.ssid || "Wi-Fi").toUpperCase()
|
||||||
color: root.onScrimDim
|
color: root.onScrimDim
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -318,6 +319,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.error !== ""
|
visible: root.error !== ""
|
||||||
text: root.error
|
text: root.error
|
||||||
color: root.onScrimUrgent
|
color: root.onScrimUrgent
|
||||||
@@ -340,6 +342,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
visible: root.showingQr && root.secured
|
visible: root.showingQr && root.secured
|
||||||
text: root.passwordError !== "" ? root.passwordError
|
text: root.passwordError !== "" ? root.passwordError
|
||||||
: root.passwordVisible ? root.password
|
: root.passwordVisible ? root.password
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -374,6 +375,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: justificationText
|
id: justificationText
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Style.space(12)
|
anchors.leftMargin: Style.space(12)
|
||||||
anchors.rightMargin: Style.space(12)
|
anchors.rightMargin: Style.space(12)
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ Item {
|
|||||||
anchors.leftMargin: card.contentLeftInset
|
anchors.leftMargin: card.contentLeftInset
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ BarWidget {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: glyph
|
id: glyph
|
||||||
|
textFormat: Text.PlainText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.playIcon
|
text: root.playIcon
|
||||||
color: activePlayer && activePlayer.isPlaying ? root.bar.barForeground : Qt.darker(root.bar.barForeground, 1.5)
|
color: activePlayer && activePlayer.isPlaying ? root.bar.barForeground : Qt.darker(root.bar.barForeground, 1.5)
|
||||||
@@ -53,6 +54,7 @@ BarWidget {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: labelText
|
id: labelText
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.title + (root.artist ? " · " + root.artist : "")
|
text: root.title + (root.artist ? " · " + root.artist : "")
|
||||||
color: root.bar.barForeground
|
color: root.bar.barForeground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -148,6 +150,7 @@ BarWidget {
|
|||||||
width: parent.width - Style.space(74)
|
width: parent.width - Style.space(74)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.title || "Nothing playing"
|
text: root.title || "Nothing playing"
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -158,6 +161,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.artist
|
text: root.artist
|
||||||
color: Qt.darker(root.bar.foreground, 1.3)
|
color: Qt.darker(root.bar.foreground, 1.3)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -168,6 +172,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
|
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
|
||||||
color: Qt.darker(root.bar.foreground, 1.6)
|
color: Qt.darker(root.bar.foreground, 1.6)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -255,6 +260,7 @@ BarWidget {
|
|||||||
spacing: Style.space(8)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: sourceRow.player && sourceRow.player.isPlaying ? "" : ""
|
text: sourceRow.player && sourceRow.player.isPlaying ? "" : ""
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -270,6 +276,7 @@ BarWidget {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: sourceRow.sourceTitle
|
text: sourceRow.sourceTitle
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
@@ -280,6 +287,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
textFormat: Text.PlainText
|
||||||
text: sourceRow.sourceDetail
|
text: sourceRow.sourceDetail
|
||||||
color: Qt.darker(root.bar.foreground, 1.5)
|
color: Qt.darker(root.bar.foreground, 1.5)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ mkdir -p "$TEST_HOME/.codex/sessions/$(date +%Y/%m/%d)" "$TEST_HOME/bin"
|
|||||||
cat >"$TEST_HOME/bin/codex" <<'EOF'
|
cat >"$TEST_HOME/bin/codex" <<'EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ -n ${CODEX_ARGS_FILE:-} ]]; then
|
||||||
|
printf '%s\0' "$@" >"$CODEX_ARGS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
while read -r request; do
|
while read -r request; do
|
||||||
id=$(jq -r '.id // empty' <<<"$request")
|
id=$(jq -r '.id // empty' <<<"$request")
|
||||||
method=$(jq -r '.method // 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}}}}
|
{"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
|
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")
|
"$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" ]] ||
|
[[ $(jq -r '.todayTotalTokens' <<<"$result") == "210" ]] ||
|
||||||
fail "Codex collector counts each turn once" "$result"
|
fail "Codex collector counts each turn once" "$result"
|
||||||
pass "Codex collector counts each turn once"
|
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"
|
stub_bin="$test_dir/bin"
|
||||||
mkdir -p "$stub_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
|
export REAL_PYTHON
|
||||||
|
rm -f "$stub_bin/python3"
|
||||||
|
|
||||||
run_migration() {
|
run_migration() {
|
||||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1
|
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"
|
fail "crash capture is no longer on by default for new installs"
|
||||||
pass "crash capture is on by default"
|
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'
|
run_node_test <<'JS'
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
|
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ assert_bypass() {
|
|||||||
assert_launch pi pi "Review this project"
|
assert_launch pi pi "Review this project"
|
||||||
assert_launch omp omp --auto-approve -- "Review this project"
|
assert_launch omp omp --auto-approve -- "Review this project"
|
||||||
assert_launch opencode opencode --auto --prompt "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 claude claude --permission-mode auto -- "Review this project"
|
||||||
assert_launch codex codex --approve-for-me -- "Review this project"
|
assert_launch codex codex --approve-for-me -- "Review this project"
|
||||||
assert_launch crush crush run "Review this project"
|
assert_launch crush crush run "Review this project"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user