Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83881e979b | ||
|
|
9ece53cede | ||
|
|
946704f309 | ||
|
|
c5a5e14e99 | ||
|
|
9d02bb08f8 | ||
|
|
b68d4142d7 | ||
|
|
ea6ee9440a | ||
|
|
eeb4206c7b | ||
|
|
8d14869689 | ||
|
|
eb7ecd13f3 | ||
|
|
2c93e66b0c | ||
|
|
e66c27f1e7 | ||
|
|
7c896d3521 |
@@ -40,6 +40,7 @@ GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
|
||||
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
||||
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
||||
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
||||
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
|
||||
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
||||
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||
|
||||
+3
-1
@@ -92,8 +92,10 @@ omp)
|
||||
;;
|
||||
ori)
|
||||
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
|
||||
# A prompt alone means one headless turn there, printed after the turn ends,
|
||||
# so --interactive is what seeds the session with it and keeps the window.
|
||||
command=(ori code)
|
||||
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
|
||||
;;
|
||||
pi)
|
||||
command=(pi)
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
|
||||
# omarchy:args=[--] [<program>] [on|off|toggle]
|
||||
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
|
||||
|
||||
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
|
||||
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
# Only regular files, because that is all the watcher honours: anything else in
|
||||
# there would be reported as muted while the crashes kept arriving. The dotted
|
||||
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
|
||||
# same -f test that keeps them out.
|
||||
list() {
|
||||
local entry found=0
|
||||
|
||||
for entry in "$MUTES"/* "$MUTES"/.*; do
|
||||
[[ -f $entry ]] || continue
|
||||
printf '%s\n' "${entry##*/}"
|
||||
found=1
|
||||
done
|
||||
|
||||
((found)) || echo "No programs muted. Crashes all notify."
|
||||
}
|
||||
|
||||
# A program may be named -h, and the router answers that with its own help
|
||||
# before this ever runs. `omarchy crash mute -- -h` is the way through.
|
||||
[[ ${1:-} == "--" ]] && shift
|
||||
|
||||
if (($# == 0)); then
|
||||
list
|
||||
exit 0
|
||||
fi
|
||||
|
||||
program=$1
|
||||
action=${2:-on}
|
||||
|
||||
# The watcher keys the mute on the executable's basename, so accept the path it
|
||||
# reports as readily as the name, and reduce either the same way it does.
|
||||
program=${program##*/}
|
||||
|
||||
if [[ -z $program || $program == "." || $program == ".." ]]; then
|
||||
echo "Not a program name: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
on|off|toggle) ;;
|
||||
*)
|
||||
echo "Not an action: $action" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
|
||||
|
||||
# Report what is now true rather than what was asked for: the flag is what the
|
||||
# watcher reads, and a toggle does not say which way it went.
|
||||
if omarchy-toggle-enabled "crash-ignore/$program"; then
|
||||
echo "Muted crash notifications for $program."
|
||||
else
|
||||
echo "Crash notifications for $program are back on."
|
||||
fi
|
||||
+28
-5
@@ -48,12 +48,17 @@ announce() {
|
||||
# -n 0 so a restart does not re-announce crashes already dealt with.
|
||||
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
while IFS= read -r entry; do
|
||||
# A dash for a field that is empty as well as one that is missing: tab is
|
||||
# IFS whitespace, so an empty field collapses into the next delimiter and
|
||||
# every field after it shifts along one. A process can set its own comm to
|
||||
# nothing, and that crash used to be read as somebody else's and dropped.
|
||||
IFS=$'\t' read -r uid comm pid exe signal < <(
|
||||
jq -r '[(._UID // "-"),
|
||||
(.COREDUMP_COMM // "-"),
|
||||
(.COREDUMP_PID // "-"),
|
||||
(.COREDUMP_EXE // "-"),
|
||||
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
||||
jq -r 'def field: if . == null or . == "" then "-" else . end;
|
||||
[(._UID | field),
|
||||
(.COREDUMP_COMM | field),
|
||||
(.COREDUMP_PID | field),
|
||||
(.COREDUMP_EXE | field),
|
||||
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
|
||||
)
|
||||
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
@@ -71,11 +76,29 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
name=$comm
|
||||
[[ $exe == /* ]] && name=${exe##*/}
|
||||
|
||||
# A process can set its own comm to anything prctl takes, slashes included,
|
||||
# and a crash with no recorded executable falls back to it. The mute below
|
||||
# turns this name into a path, so keep it one component: a crash must not
|
||||
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
|
||||
name=${name##*/}
|
||||
|
||||
# What that leaves is not always a name. "/" leaves nothing, which is no
|
||||
# kind of array subscript and no kind of toast; a dot component names a
|
||||
# directory rather than a flag, so a mute on it would touch that directory
|
||||
# and then never match; and a dash is what the read above puts there when
|
||||
# the crash recorded no name at all.
|
||||
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
|
||||
|
||||
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
|
||||
|
||||
# Never announce our own machinery, or it notifies about itself.
|
||||
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
|
||||
|
||||
# Muted at the end of a diagnosis, when the user was offered it and said
|
||||
# yes. A flag per program rather than one list, so omarchy-crash-mute can
|
||||
# lift one without reading, rewriting and re-parsing the rest.
|
||||
omarchy-toggle-enabled "crash-ignore/$name" && continue
|
||||
|
||||
now=$EPOCHSECONDS
|
||||
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
|
||||
sudo systemctl enable --now once-background.service
|
||||
|
||||
echo -e "\nLaunching ONCE..."
|
||||
once
|
||||
sudo once
|
||||
|
||||
@@ -13,6 +13,18 @@ safe_icon_name() {
|
||||
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
|
||||
}
|
||||
|
||||
require_plain_name() {
|
||||
# The name becomes a filename. A slash would turn it into directory levels, so
|
||||
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
|
||||
# is stuck in the launcher; a leading ../ leaves the applications directory
|
||||
# altogether. Refuse rather than silently renaming what the user typed -- most
|
||||
# often it is a URL entered in the name field.
|
||||
if [[ $1 == */* ]]; then
|
||||
echo "App name cannot contain '/': $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
icon_name_from_ref() {
|
||||
local ref="$1"
|
||||
local name
|
||||
@@ -68,6 +80,7 @@ fetch_site_icon() {
|
||||
if (( $# < 3 )); then
|
||||
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
|
||||
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
|
||||
require_plain_name "$APP_NAME"
|
||||
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
@@ -104,6 +117,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_plain_name "$APP_NAME"
|
||||
|
||||
if [[ -z $ICON_REF ]]; then
|
||||
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -132,8 +147,9 @@ fi
|
||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||
|
||||
# Create application .desktop file
|
||||
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
|
||||
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
mkdir -p "$DESKTOP_DIR"
|
||||
|
||||
cat >"$DESKTOP_FILE" <<EOF
|
||||
[Desktop Entry]
|
||||
|
||||
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
|
||||
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications/"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
# Find all web apps
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
|
||||
# Always index the launchers, so removal deletes the file that was found rather
|
||||
# than a path rebuilt from the displayed name. Installs predating the name
|
||||
# validation could nest the launcher inside directories, and those are exactly
|
||||
# the ones a reconstructed path cannot reach.
|
||||
WEB_APP_PATHS=()
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
WEB_APP_PATHS+=("$file")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
|
||||
|
||||
# The launcher matching a chosen name, or empty when nothing was indexed under
|
||||
# it (an app removed between the scan and the pick, say).
|
||||
path_for_web_app() {
|
||||
local wanted="$1" i
|
||||
for i in "${!WEB_APPS[@]}"; do
|
||||
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
|
||||
printf '%s\n' "${WEB_APP_PATHS[$i]}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if (( $# == 0 )); then
|
||||
if ((${#WEB_APPS[@]})); then
|
||||
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
|
||||
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
|
||||
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
|
||||
fi
|
||||
|
||||
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
|
||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
desktop_file=$(path_for_web_app "$APP_NAME")
|
||||
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||
|
||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||
|
||||
@@ -87,7 +87,38 @@ ambiguous, say so rather than assembling confidence out of guesswork.
|
||||
|
||||
**Leave the system as you found it.** Diagnosis reads; it does not fix, tidy, or
|
||||
reconfigure. The one thing to clean up is your own: delete the core you extracted
|
||||
above, which is a copy of the crashed process's memory.
|
||||
above, which is a copy of the crashed process's memory. The single change a
|
||||
diagnosis may make is the mute below, and only when the user asks for it.
|
||||
|
||||
## Offer to stop the notifications for this program
|
||||
|
||||
A crash you have explained often keeps happening anyway. Finish by offering to
|
||||
silence notifications for **that one program**, and never run it unprompted. Say
|
||||
how to lift it in the same breath, so it is not a one-way door.
|
||||
|
||||
```bash
|
||||
omarchy-crash-mute '<program>' # silence it
|
||||
omarchy-crash-mute '<program>' off # let it speak again
|
||||
omarchy-crash-mute # list what is muted
|
||||
```
|
||||
|
||||
Pass the `binary:` path from the crash facts, or the `process:` name where no
|
||||
binary was recorded; the command reduces either to the name the watcher keys on.
|
||||
A diagnosis run by hand from `omarchy agent crash <pid>` has neither, so take
|
||||
them from `coredumpctl info`. Prefer the binary: a process name is truncated to
|
||||
15 characters and a basename is not, so muting the truncated form matches
|
||||
nothing, forever, while looking like it worked.
|
||||
|
||||
Quote it. The name is whatever the crashed program's author called a file, and a
|
||||
single quote inside one closes yours and runs the rest as your shell.
|
||||
|
||||
The key is a bare name, so anything run through an interpreter is keyed as the
|
||||
interpreter: muting `python3.13` silences every Python program on the machine.
|
||||
Say so rather than quietly doing it.
|
||||
|
||||
None of this fixes anything, and a mute offered in place of a fix that was within
|
||||
reach is the wrong answer. For every program rather than one, the switch is
|
||||
_Trigger > Toggle > Crash Capture_.
|
||||
|
||||
## If it is an Omarchy bug
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
# Repositories for debug symbol packages.
|
||||
|
||||
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/stable/$arch
|
||||
|
||||
@@ -39,6 +39,8 @@ Omarchy watches systemd-coredump for process crashes. When something segfaults,
|
||||
|
||||
The watching is on by default. Turn it off under _Trigger > Toggle > Crash Capture_ (or with `omarchy toggle crash-capture`) and the notifications stop; `omarchy agent crash <pid>` still works by hand.
|
||||
|
||||
Crashes can also be silenced one program at a time, which is what the diagnosis offers you at the end. `omarchy crash mute hyprland` stops the notifications for that program only, `omarchy crash mute hyprland off` brings them back, and `omarchy crash mute` on its own lists what you've muted. It takes the binary's path as happily as its name, so `omarchy crash mute /usr/bin/hyprland` does the same thing. Quote a name with a space in it, as in `omarchy crash mute 'Some App'`. Everything else still notifies, and the muted program still crashes — this hides the reminder, it doesn't fix anything.
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
|
||||
|
||||
@@ -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
|
||||
@@ -54,6 +54,334 @@ grep -F 'omarchy-crash-watch.service' "$ROOT/install/user/first-run/enable-user-
|
||||
fail "crash capture is no longer on by default for new installs"
|
||||
pass "crash capture is on by default"
|
||||
|
||||
require_command jq
|
||||
|
||||
# The per-program mute, driven through the real watcher with a stubbed journal:
|
||||
# these prove what a person sees -- a toast arriving or not -- where asserting
|
||||
# that a flag file was read would prove only that a flag file was read.
|
||||
watch_bin="$TMPDIR/watch-bin"
|
||||
watch_home="$TMPDIR/watch-home"
|
||||
NOTIFY_LOG="$TMPDIR/notify-log"
|
||||
JOURNAL_ENTRIES="$TMPDIR/journal-entries"
|
||||
|
||||
mkdir -p "$watch_bin" "$watch_home"
|
||||
|
||||
cat >"$watch_bin/journalctl" <<'SH'
|
||||
#!/bin/bash
|
||||
cat "$JOURNAL_ENTRIES"
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-default-agent" <<'SH'
|
||||
#!/bin/bash
|
||||
echo claude
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-notification-wait" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$watch_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"$NOTIFY_LOG"
|
||||
SH
|
||||
|
||||
chmod +x "$watch_bin/journalctl" "$watch_bin/omarchy-default-agent" \
|
||||
"$watch_bin/omarchy-notification-wait" "$watch_bin/omarchy-notification-send"
|
||||
|
||||
reset_entries() {
|
||||
: >"$JOURNAL_ENTRIES"
|
||||
}
|
||||
|
||||
# One core dump as systemd-coredump journals it. The UID must be this user's, or
|
||||
# the watcher discards it as somebody else's crash before anything under test.
|
||||
crash_entry() {
|
||||
local comm="$1" exe="$2"
|
||||
|
||||
jq -cn --arg uid "$UID" --arg comm "$comm" --arg exe "$exe" \
|
||||
'{_UID: $uid, COREDUMP_COMM: $comm, COREDUMP_PID: "4242",
|
||||
COREDUMP_EXE: $exe, COREDUMP_SIGNAL_NAME: "SIGSEGV"}' >>"$JOURNAL_ENTRIES"
|
||||
}
|
||||
|
||||
# The stubbed journalctl ends after the entries, so the watcher's loop ends too.
|
||||
# Its exit status is asserted rather than discarded: a watcher that dies on a
|
||||
# muted crash notifies about nothing afterwards, which every assertion below
|
||||
# that expects silence would otherwise read as success.
|
||||
run_watch() {
|
||||
local status=0
|
||||
|
||||
: >"$NOTIFY_LOG"
|
||||
|
||||
PATH="$watch_bin:$ROOT/bin:$PATH" \
|
||||
JOURNAL_ENTRIES="$JOURNAL_ENTRIES" \
|
||||
NOTIFY_LOG="$NOTIFY_LOG" \
|
||||
HOME="$watch_home" \
|
||||
"$ROOT/bin/omarchy-crash-watch" || status=$?
|
||||
|
||||
(( status == 0 )) ||
|
||||
fail "the watcher exited $status rather than carrying on, so a mute takes the service down with it"
|
||||
}
|
||||
|
||||
# Through the real command rather than writing the flag by hand: these assertions
|
||||
# are then the guard that the thing the diagnosis runs and the thing the watcher
|
||||
# reads have not drifted apart.
|
||||
mute() {
|
||||
HOME="$watch_home" PATH="$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" "$1" "$2" >/dev/null
|
||||
}
|
||||
|
||||
announced() {
|
||||
grep -Fq "Process crashed: $1" "$NOTIFY_LOG"
|
||||
}
|
||||
|
||||
reset_entries
|
||||
crash_entry hyprland /usr/bin/hyprland
|
||||
run_watch
|
||||
announced hyprland ||
|
||||
fail "a crash nobody muted still announces itself"
|
||||
pass "a crash nobody muted still announces itself"
|
||||
|
||||
mute hyprland on
|
||||
run_watch
|
||||
! announced hyprland ||
|
||||
fail "muting a program stops the crash notifications the diagnosis offered to stop"
|
||||
pass "muting a program stops its crash notifications"
|
||||
|
||||
reset_entries
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced nautilus ||
|
||||
fail "muting one program silences every other program, which is the global toggle's job and not this one's"
|
||||
pass "muting one program leaves every other program announcing"
|
||||
|
||||
mute hyprland off
|
||||
reset_entries
|
||||
crash_entry hyprland /usr/bin/hyprland
|
||||
run_watch
|
||||
announced hyprland ||
|
||||
fail "un-muting a program brings its crash notifications back"
|
||||
pass "un-muting a program brings its crash notifications back"
|
||||
|
||||
# The diagnosis tells the user to mute the name the toast showed them, so the
|
||||
# toast has to show the name the watcher checks. COMM is truncated to 15
|
||||
# characters and the executable's basename is not, and announcing the truncated
|
||||
# one would leave a dutifully-followed mute matching nothing forever.
|
||||
reset_entries
|
||||
crash_entry chromium-browse /usr/lib/chromium/chromium-browser
|
||||
run_watch
|
||||
announced chromium-browser ||
|
||||
fail "the toast announces a name the mute cannot be keyed on, so following the diagnosis mutes nothing"
|
||||
pass "the toast announces the name the mute is keyed on"
|
||||
|
||||
mute chromium-browser on
|
||||
run_watch
|
||||
! announced chromium-browser ||
|
||||
fail "the mute is keyed on the name the notification announced, not on the truncated COMM"
|
||||
pass "muting the announced name silences a program whose COMM was truncated"
|
||||
|
||||
# A muted crash must not end the watcher. Restart=always would paper over it
|
||||
# with a five-second gap, and the watcher restarts on `journalctl -n 0`, which
|
||||
# never replays the crashes it missed while it was away.
|
||||
reset_entries
|
||||
crash_entry chromium-browse /usr/lib/chromium/chromium-browser
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced nautilus ||
|
||||
fail "a muted crash stops the watcher reading the journal, losing every crash after it"
|
||||
pass "a muted crash does not stop the watcher reading the next one"
|
||||
|
||||
# A process can set its own comm to anything prctl takes, slashes included, and
|
||||
# a crash with no recorded executable falls back to it. A name that climbed out
|
||||
# of crash-ignore/ would let a crashing program silence itself against an
|
||||
# unrelated flag -- and have the diagnosis write one there on the user's behalf.
|
||||
# The fixture carries two slashes so that dropping only the first is not mistaken
|
||||
# for dropping all of them.
|
||||
reset_entries
|
||||
crash_entry a/../bar-off -
|
||||
sibling_flag="$watch_home/.local/state/omarchy/toggles/bar-off"
|
||||
touch "$sibling_flag"
|
||||
run_watch
|
||||
announced bar-off ||
|
||||
fail "a comm that climbs out of crash-ignore/ reads an unrelated toggle, letting a crash suppress its own notification"
|
||||
pass "a comm that climbs out of crash-ignore/ cannot reach an unrelated toggle"
|
||||
rm -f "$sibling_flag"
|
||||
|
||||
# Stripping to the last component does not always leave a component. An empty
|
||||
# name is no kind of array subscript and no kind of toast, and a dot component
|
||||
# names a directory the mute would touch and then never match.
|
||||
for empty_comm in / a/ . ..; do
|
||||
reset_entries
|
||||
crash_entry "$empty_comm" -
|
||||
run_watch
|
||||
announced unknown ||
|
||||
fail "a comm of '$empty_comm' leaves no usable name, so the toast cannot say what crashed and the mute has nothing to key on"
|
||||
done
|
||||
pass "a comm that strips down to nothing or a dot still announces under a name a mute can use"
|
||||
|
||||
# An empty comm is not a missing entry. Tab is IFS whitespace, so an empty field
|
||||
# collapses and every field after it shifts along one -- the pid becomes a path,
|
||||
# the crash reads as somebody else's, and it is dropped without a word.
|
||||
reset_entries
|
||||
crash_entry "" -
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced unknown ||
|
||||
fail "a crash whose comm is empty is dropped instead of announced, because the empty field shifted every field after it"
|
||||
announced nautilus ||
|
||||
fail "an empty comm derails the rest of the journal entry"
|
||||
pass "an empty comm is announced rather than parsed into the next field"
|
||||
|
||||
# Only "." and ".." are special. A leading dot is an ordinary filename, and
|
||||
# folding those into the fallback would have one program's mute silence another.
|
||||
for dotted_comm in .hidden ...; do
|
||||
reset_entries
|
||||
crash_entry "$dotted_comm" -
|
||||
run_watch
|
||||
announced "$dotted_comm" ||
|
||||
fail "'$dotted_comm' is an ordinary name, but it lands in the fallback, so muting it would silence unrelated crashes"
|
||||
done
|
||||
pass "a leading dot is an ordinary name rather than a special component"
|
||||
|
||||
# And the name it settles on is mutable like any other.
|
||||
mute unknown on
|
||||
reset_entries
|
||||
crash_entry / -
|
||||
run_watch
|
||||
! announced unknown ||
|
||||
fail "the fallback name cannot be muted, so the one crash most likely to repeat is the one that cannot be silenced"
|
||||
pass "the fallback name can be muted like any other"
|
||||
mute unknown off
|
||||
|
||||
# What omarchy-crash-mute does on its own. That it agrees with the watcher is
|
||||
# already covered above, which drives it for every mute it makes.
|
||||
mute_home="$TMPDIR/mute-home"
|
||||
mkdir -p "$mute_home"
|
||||
|
||||
crash_mute() {
|
||||
HOME="$mute_home" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-crash-mute" "$@"
|
||||
}
|
||||
|
||||
mute_flag() {
|
||||
[[ $1 == "--" ]] && shift
|
||||
printf '%s' "$mute_home/.local/state/omarchy/toggles/crash-ignore/$1"
|
||||
}
|
||||
|
||||
crash_mute | grep -Fq "No programs muted" ||
|
||||
fail "an empty mute list prints nothing, so a user cannot tell it from a broken command"
|
||||
pass "the command says so when nothing is muted"
|
||||
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute | grep -Fqx hyprland ||
|
||||
fail "a muted program is missing from the list, so a mute cannot be found again to lift it"
|
||||
pass "the command lists what it muted"
|
||||
|
||||
# The watcher keys on the basename, so the command has to take the path a crash
|
||||
# recorded and land on the same flag the watcher will look for.
|
||||
crash_mute /usr/lib/chromium/chromium-browser >/dev/null
|
||||
[[ -f $(mute_flag chromium-browser) ]] ||
|
||||
fail "a binary's path is muted verbatim rather than by name, so the watcher never sees that flag"
|
||||
pass "the command reduces a path to the name the watcher checks"
|
||||
|
||||
crash_mute hyprland off >/dev/null
|
||||
[[ ! -f $(mute_flag hyprland) ]] ||
|
||||
fail "off leaves the program muted, making the mute a one-way door"
|
||||
pass "the command un-mutes"
|
||||
|
||||
# Muting is not flipping. The diagnosis offers this on a program the user may
|
||||
# already have muted, and asking for a mute twice has to leave it muted.
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute hyprland >/dev/null
|
||||
[[ -f $(mute_flag hyprland) ]] ||
|
||||
fail "muting an already-muted program un-mutes it, so offering the mute a second time turns it back on"
|
||||
pass "asking to mute twice leaves it muted"
|
||||
|
||||
# A program may legitimately be called .hidden, and a mute nobody can see is a
|
||||
# mute nobody can lift.
|
||||
crash_mute .hidden >/dev/null
|
||||
crash_mute | grep -Fqx .hidden ||
|
||||
fail "a mute on a dotted name is missing from the list, so it can never be found and lifted"
|
||||
pass "the list shows a name that begins with a dot"
|
||||
|
||||
# It turns what it is given into a path, so it has to refuse whatever is not one
|
||||
# component of one.
|
||||
for bad_name in . .. /; do
|
||||
! crash_mute "$bad_name" >/dev/null 2>&1 ||
|
||||
fail "'$bad_name' is taken as a program name, and the flag that writes is not one the watcher will ever read"
|
||||
done
|
||||
pass "the command refuses a name that is not a name"
|
||||
|
||||
! crash_mute hyprland sideways >/dev/null 2>&1 ||
|
||||
fail "an action it does not know is treated as a mute, so a typo silences a program"
|
||||
pass "the command refuses an action it does not know"
|
||||
|
||||
# And says what it refused, or the user retypes the same thing. Captured rather
|
||||
# than piped: the command exits non-zero here, which pipefail would surface as
|
||||
# the pipeline's status and read as a failed assertion.
|
||||
refusal=$(crash_mute hyprland sideways 2>&1) || true
|
||||
grep -Fq "Not an action" <<<"$refusal" ||
|
||||
fail "an unknown action is refused without naming it, leaving the user nothing to correct"
|
||||
pass "the command names the action it refused"
|
||||
|
||||
crash_mute ../bar-off >/dev/null
|
||||
[[ ! -e "$mute_home/.local/state/omarchy/toggles/bar-off" ]] ||
|
||||
fail "a name that climbs out writes a sibling toggle, so muting a crash could turn off the bar instead"
|
||||
pass "the command cannot be talked into writing outside crash-ignore/"
|
||||
|
||||
# A program may be called -h, and the router answers that with its own help
|
||||
# before the command runs. A leading -- is the way through, so it has to be
|
||||
# consumed rather than taken for the program name.
|
||||
crash_mute -- -h >/dev/null 2>&1 ||
|
||||
fail "a leading -- is refused rather than consumed, so a program named like a flag cannot be muted at all"
|
||||
[[ -f $(mute_flag -- -h) ]] ||
|
||||
fail "a leading -- is taken for the program name, so muting -h mutes something else"
|
||||
pass "a leading -- lets a program named like a flag be muted"
|
||||
|
||||
# toggle is advertised, so it has to flip both ways rather than quietly mute.
|
||||
crash_mute toggler off >/dev/null
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle does not mute an un-muted program"
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ ! -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle mutes but never un-mutes, so the advertised action only goes one way"
|
||||
pass "toggle flips a mute both ways"
|
||||
|
||||
# The listing means what the watcher means, and the watcher honours a regular
|
||||
# file. Anything else in there is not a mute, however much it looks like one.
|
||||
mkdir -p "$(mute_flag notactuallymuted)"
|
||||
! crash_mute | grep -Fqx notactuallymuted ||
|
||||
fail "a directory is reported as muted while that program's crashes keep arriving"
|
||||
pass "the listing counts only the flags the watcher honours"
|
||||
rmdir "$(mute_flag notactuallymuted)"
|
||||
|
||||
# A mute that could not be written must not be reported as one. Without this the
|
||||
# command can print success for a flag that was never created.
|
||||
failing_bin="$TMPDIR/failing-bin"
|
||||
mkdir -p "$failing_bin"
|
||||
cat >"$failing_bin/omarchy-toggle" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$failing_bin/omarchy-toggle"
|
||||
|
||||
status=0
|
||||
refusal=$(HOME="$mute_home" PATH="$failing_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" hyprland 2>&1) || status=$?
|
||||
(( status != 0 )) ||
|
||||
fail "a mute that could not be written exits zero, so nothing downstream learns it failed"
|
||||
! grep -Fq "Muted crash notifications" <<<"$refusal" ||
|
||||
fail "a mute that could not be written still reports success, so the user believes a program is silenced when it is not"
|
||||
pass "a mute that could not be written is not reported as one"
|
||||
|
||||
skill="$ROOT/default/agents/skills/diagnose-crash/SKILL.md"
|
||||
grep -Fq 'omarchy-crash-mute' "$skill" ||
|
||||
fail "the diagnosis no longer names the command that mutes, so the offer it makes cannot be carried out"
|
||||
pass "the diagnosis names the command that mutes"
|
||||
|
||||
grep -Fq 'GROUP_DESCRIPTIONS[crash]' "$ROOT/bin/omarchy" ||
|
||||
fail "the crash group has no description, so the router lists a group it cannot describe"
|
||||
pass "the crash group is described in the router"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
|
||||
|
||||
@@ -457,7 +457,7 @@ assert_bypass() {
|
||||
assert_launch pi pi "Review this project"
|
||||
assert_launch omp omp --auto-approve -- "Review this project"
|
||||
assert_launch opencode opencode --auto --prompt "Review this project"
|
||||
assert_launch ori ori code --prompt "Review this project"
|
||||
assert_launch ori ori code --interactive --prompt "Review this project"
|
||||
assert_launch claude claude --permission-mode auto -- "Review this project"
|
||||
assert_launch codex codex --approve-for-me -- "Review this project"
|
||||
assert_launch crush crush run "Review this project"
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
mkdir -p "$tmp_dir/bin" "$tmp_dir/home"
|
||||
|
||||
for stub in gtk-update-icon-cache update-desktop-database omarchy-notification-send; do
|
||||
printf '#!/bin/bash\n:\n' >"$tmp_dir/bin/$stub"
|
||||
chmod +x "$tmp_dir/bin/$stub"
|
||||
done
|
||||
|
||||
run_install() {
|
||||
HOME="$tmp_dir/home" PATH="$tmp_dir/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-webapp-install" "$@"
|
||||
}
|
||||
|
||||
run_remove() {
|
||||
HOME="$tmp_dir/home" PATH="$tmp_dir/bin:$PATH" OMARCHY_REMOVE_NOTIFY=false \
|
||||
"$ROOT/bin/omarchy-webapp-remove" "$@"
|
||||
}
|
||||
|
||||
apps_dir="$tmp_dir/home/.local/share/applications"
|
||||
icons_dir="$tmp_dir/home/.local/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# A URL typed into the name field is the reported way in. Every slash used to
|
||||
# become a directory level, leaving a launcher nothing could address. Assert on
|
||||
# the message: creating the launcher directly in the applications directory
|
||||
# already makes the redirect fail on its own, so a bare non-zero exit would pass
|
||||
# just as well with no validation at all.
|
||||
output=$(run_install "http://example.test/oops" "https://example.com" hey 2>&1) &&
|
||||
fail "webapp install rejects a name containing a slash"
|
||||
[[ $output == *"App name cannot contain '/'"* ]] ||
|
||||
fail "webapp install says why it refused a slashed name" "$output"
|
||||
[[ -e "$apps_dir/http:" ]] &&
|
||||
fail "webapp install does not create a directory from a slashed name"
|
||||
pass "webapp install rejects a name that would nest the launcher"
|
||||
|
||||
# The name was a path fragment until something said otherwise, so ../ climbed
|
||||
# out of the applications directory entirely and wrote wherever it landed.
|
||||
if run_install "../../../../escaped" "https://example.com" hey >/dev/null 2>&1; then
|
||||
fail "webapp install rejects a name that climbs out of the applications directory"
|
||||
fi
|
||||
[[ -e "$tmp_dir/escaped.desktop" ]] &&
|
||||
fail "webapp install writes no launcher outside the applications directory"
|
||||
pass "webapp install refuses a name that would escape the applications directory"
|
||||
|
||||
# The interactive prompt reads the name long before it is used as a path, and
|
||||
# fetches the site icon in between. Rejecting only at the write leaves that icon
|
||||
# behind in the user's icon theme, once per attempt.
|
||||
mkdir -p "$tmp_dir/ibin"
|
||||
cp "$tmp_dir/bin"/* "$tmp_dir/ibin/"
|
||||
cat >"$tmp_dir/ibin/gum" <<'STUB'
|
||||
#!/bin/bash
|
||||
count_file="${GUM_STUB_COUNT:?}"
|
||||
count=$(cat "$count_file" 2>/dev/null || echo 0)
|
||||
count=$((count + 1))
|
||||
echo "$count" >"$count_file"
|
||||
if (( count == 1 )); then
|
||||
echo "http://example.test/oops"
|
||||
else
|
||||
echo "https://example.com"
|
||||
fi
|
||||
STUB
|
||||
cat >"$tmp_dir/ibin/curl" <<'STUB'
|
||||
#!/bin/bash
|
||||
# Answer any download with a real PNG so the icon fetch reports success.
|
||||
out=""
|
||||
prev=""
|
||||
for arg in "$@"; do
|
||||
[[ $prev == "-o" ]] && out="$arg"
|
||||
prev="$arg"
|
||||
done
|
||||
if [[ -n $out ]]; then
|
||||
printf '%s' 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' | base64 -d >"$out"
|
||||
fi
|
||||
STUB
|
||||
chmod +x "$tmp_dir/ibin/gum" "$tmp_dir/ibin/curl"
|
||||
|
||||
if HOME="$tmp_dir/home" PATH="$tmp_dir/ibin:$PATH" \
|
||||
GUM_STUB_COUNT="$tmp_dir/gum-count" \
|
||||
"$ROOT/bin/omarchy-webapp-install" >/dev/null 2>&1; then
|
||||
fail "interactive webapp install rejects a name containing a slash"
|
||||
fi
|
||||
if compgen -G "$icons_dir/*.png" >/dev/null; then
|
||||
fail "interactive webapp install downloads no icon for a name it refuses" \
|
||||
"$(ls "$icons_dir")"
|
||||
fi
|
||||
pass "webapp install refuses a slashed name before fetching its icon"
|
||||
|
||||
# A normal name still installs and removes.
|
||||
run_install "Example App" "https://example.com" hey >/dev/null
|
||||
[[ -f "$apps_dir/Example App.desktop" ]] ||
|
||||
fail "webapp install writes the launcher for an ordinary name"
|
||||
run_remove "Example App" >/dev/null
|
||||
[[ -f "$apps_dir/Example App.desktop" ]] &&
|
||||
fail "webapp remove deletes the launcher it installed"
|
||||
pass "webapp install and remove round-trip an ordinary name"
|
||||
|
||||
# Anything installed by an older version can still be nested. Removal has to
|
||||
# reach it, which a path rebuilt from the displayed name never could.
|
||||
mkdir -p "$apps_dir/http:/127.0.0.1:4000"
|
||||
cat >"$apps_dir/http:/127.0.0.1:4000/.desktop" <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Name=http://127.0.0.1:4000
|
||||
Exec=omarchy-launch-webapp https://127.0.0.1:4000
|
||||
Type=Application
|
||||
DESKTOP
|
||||
|
||||
# This is the name the picker shows for that file: the script strips .desktop
|
||||
# from the path and then takes the basename, which lands on the directory.
|
||||
run_remove "127.0.0.1:4000" >/dev/null
|
||||
[[ -f "$apps_dir/http:/127.0.0.1:4000/.desktop" ]] &&
|
||||
fail "webapp remove deletes a launcher left nested by an older install"
|
||||
pass "webapp remove reaches a nested legacy launcher"
|
||||
|
||||
# Removing by name on a machine with no applications directory yet must stay
|
||||
# quiet: omarchy-remove-gaming-xbox-cloud calls it without hiding stderr.
|
||||
noise=$(HOME="$tmp_dir/empty" PATH="$tmp_dir/bin:$PATH" OMARCHY_REMOVE_NOTIFY=false \
|
||||
"$ROOT/bin/omarchy-webapp-remove" "Xbox Cloud Gaming" 2>&1 >/dev/null)
|
||||
[[ -n $noise ]] &&
|
||||
fail "webapp remove stays quiet with no applications directory" "$noise"
|
||||
pass "webapp remove stays quiet when there is no applications directory"
|
||||
Reference in New Issue
Block a user