Merge quattro into clock-calendar
Both sides tightened the same center-layout assertion. Taking quattro's: it asserts the weather/update adjacency the test name is about instead of pinning the whole row, which is what kept breaking it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -262,7 +262,7 @@ This copies `$OMARCHY_PATH/config/hypr/hyprlock.conf` to `~/.config/hypr/hyprloc
|
|||||||
|
|
||||||
Read `docs/migrations.md` before creating or changing migrations.
|
Read `docs/migrations.md` before creating or changing migrations.
|
||||||
|
|
||||||
Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the migration notification. Put migrations directly under `migrations/<timestamp>.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it.
|
Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the login-time migration notification. Put migrations directly under `migrations/<timestamp>.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it.
|
||||||
|
|
||||||
To create a new migration, run `omarchy-dev-add-migration --no-edit`.
|
To create a new migration, run `omarchy-dev-add-migration --no-edit`.
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ GROUP_DESCRIPTIONS[dev]="Omarchy development tools"
|
|||||||
GROUP_DESCRIPTIONS[display]="Display and text scaling"
|
GROUP_DESCRIPTIONS[display]="Display and text scaling"
|
||||||
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
|
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
|
||||||
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
|
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
|
||||||
|
GROUP_DESCRIPTIONS[file]="File selection helpers"
|
||||||
GROUP_DESCRIPTIONS[font]="Font management"
|
GROUP_DESCRIPTIONS[font]="Font management"
|
||||||
GROUP_DESCRIPTIONS[games]="Game launchers and helpers"
|
GROUP_DESCRIPTIONS[games]="Game launchers and helpers"
|
||||||
GROUP_DESCRIPTIONS[hibernation]="Hibernation setup and removal"
|
GROUP_DESCRIPTIONS[hibernation]="Hibernation setup and removal"
|
||||||
@@ -78,6 +79,7 @@ GROUP_DESCRIPTIONS[snapshot]="System snapshots"
|
|||||||
GROUP_DESCRIPTIONS[style]="Global UI style controls"
|
GROUP_DESCRIPTIONS[style]="Global UI style controls"
|
||||||
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
||||||
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
||||||
|
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
|
||||||
GROUP_DESCRIPTIONS[theme]="Theme management"
|
GROUP_DESCRIPTIONS[theme]="Theme management"
|
||||||
GROUP_DESCRIPTIONS[tmux]="Tmux session helpers"
|
GROUP_DESCRIPTIONS[tmux]="Tmux session helpers"
|
||||||
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
||||||
|
|||||||
+10
-1
@@ -288,6 +288,15 @@ cmd_move() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local default_section="${PLACEMENT_SECTION:-}"
|
local default_section="${PLACEMENT_SECTION:-}"
|
||||||
|
|
||||||
|
# A bare section names the section, not the slot, so let it fall through to
|
||||||
|
# the same anchor placement 'add' uses. Passing it as an explicit target
|
||||||
|
# instead drops the widget on the far end of the row.
|
||||||
|
local target_section="$PLACEMENT_SECTION"
|
||||||
|
if [[ -z $PLACEMENT_INDEX && -z $PLACEMENT_BEFORE && -z $PLACEMENT_AFTER ]]; then
|
||||||
|
target_section=""
|
||||||
|
fi
|
||||||
|
|
||||||
local prog
|
local prog
|
||||||
prog=$(cat <<JQ
|
prog=$(cat <<JQ
|
||||||
$JQ_DEFS $NORMALIZE
|
$JQ_DEFS $NORMALIZE
|
||||||
@@ -302,7 +311,7 @@ JQ
|
|||||||
commit "$prog" \
|
commit "$prog" \
|
||||||
--arg id "$id" \
|
--arg id "$id" \
|
||||||
--arg defaultSection "$default_section" \
|
--arg defaultSection "$default_section" \
|
||||||
--arg targetSection "$PLACEMENT_SECTION" \
|
--arg targetSection "$target_section" \
|
||||||
--arg targetIndex "$PLACEMENT_INDEX" \
|
--arg targetIndex "$PLACEMENT_INDEX" \
|
||||||
--arg before "$PLACEMENT_BEFORE" \
|
--arg before "$PLACEMENT_BEFORE" \
|
||||||
--arg after "$PLACEMENT_AFTER" \
|
--arg after "$PLACEMENT_AFTER" \
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ RESOLUTION=""
|
|||||||
FULLSCREEN="false"
|
FULLSCREEN="false"
|
||||||
STOP_RECORDING="false"
|
STOP_RECORDING="false"
|
||||||
RECORDING_FILE="/tmp/omarchy-screenrecord-filename"
|
RECORDING_FILE="/tmp/omarchy-screenrecord-filename"
|
||||||
|
REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
|
||||||
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "/tmp/omarchy-screenrecord.log" || echo "/dev/null")
|
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "/tmp/omarchy-screenrecord.log" || echo "/dev/null")
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@@ -89,11 +90,26 @@ start_webcam_overlay() {
|
|||||||
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
|
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
|
||||||
--no-border --no-audio --no-osc --osd-level=0 \
|
--no-border --no-audio --no-osc --osd-level=0 \
|
||||||
--really-quiet &>/dev/null &
|
--really-quiet &>/dev/null &
|
||||||
sleep 1
|
|
||||||
|
# The move has to settle before gpu-screen-recorder starts, or the camera is
|
||||||
|
# recorded sliding into its corner. Waiting for the map is what the blind
|
||||||
|
# second was partly guessing at, so the remainder is trimmed to hold the
|
||||||
|
# pre-capture delay where it was: starting later costs the first words spoken.
|
||||||
|
local waited=0
|
||||||
|
while ((waited < 40)) && ! hyprctl clients -j | jq -e 'any(.[]; .title == "WebcamOverlay")' >/dev/null 2>&1; do
|
||||||
|
sleep 0.05
|
||||||
|
((waited++))
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ ${1:-} == region:* ]] && echo "${1#region:}" >"$REGION_FILE"
|
||||||
|
omarchy-capture-webcam-resize "$WEBCAM_SIZE"
|
||||||
|
|
||||||
|
sleep 0.6
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_webcam() {
|
cleanup_webcam() {
|
||||||
pkill -f "WebcamOverlay" 2>/dev/null
|
pkill -f "WebcamOverlay" 2>/dev/null
|
||||||
|
rm -f "$REGION_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
default_resolution() {
|
default_resolution() {
|
||||||
@@ -155,7 +171,7 @@ start_screenrecording() {
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ $WEBCAM == "true" ]] && start_webcam_overlay
|
[[ $WEBCAM == "true" ]] && start_webcam_overlay "$target"
|
||||||
|
|
||||||
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
|
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
|
||||||
local audio_devices=""
|
local audio_devices=""
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
readonly MARGIN=40
|
readonly MARGIN=40
|
||||||
|
readonly REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: omarchy-capture-webcam-resize <smaller|larger|reset|small|medium|large>" >&2
|
echo "Usage: omarchy-capture-webcam-resize <smaller|larger|reset|small|medium|large>" >&2
|
||||||
@@ -57,13 +58,39 @@ read -r monitor_x monitor_y monitor_width monitor_height < <(
|
|||||||
|
|
||||||
[[ $monitor_x =~ ^-?[0-9]+$ && $monitor_y =~ ^-?[0-9]+$ && $monitor_width =~ ^[0-9]+$ && $monitor_height =~ ^[0-9]+$ ]] || exit 0
|
[[ $monitor_x =~ ^-?[0-9]+$ && $monitor_y =~ ^-?[0-9]+$ && $monitor_width =~ ^[0-9]+$ && $monitor_height =~ ^[0-9]+$ ]] || exit 0
|
||||||
|
|
||||||
# Scale the 8:9 portrait presets from monitor height so they occupy the same
|
# Anchor to the recorded region when there is one, so a window picked on a wide
|
||||||
|
# display keeps the camera in its own corner. Full-monitor captures, the portal
|
||||||
|
# backend, and resizes outside a recording publish none and fall back here.
|
||||||
|
anchor_x=$monitor_x
|
||||||
|
anchor_y=$monitor_y
|
||||||
|
anchor_width=$monitor_width
|
||||||
|
anchor_height=$monitor_height
|
||||||
|
|
||||||
|
if [[ -f $REGION_FILE ]] && region=$(<"$REGION_FILE"); then
|
||||||
|
if [[ $region =~ ^([0-9]+)x([0-9]+)\+(-?[0-9]+)\+(-?[0-9]+)$ ]]; then
|
||||||
|
anchor_width=${BASH_REMATCH[1]}
|
||||||
|
anchor_height=${BASH_REMATCH[2]}
|
||||||
|
anchor_x=${BASH_REMATCH[3]}
|
||||||
|
anchor_y=${BASH_REMATCH[4]}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A tall, narrow region can't fit presets scaled from its own height, so cap the
|
||||||
|
# height they scale from to what the width allows — the large preset is the
|
||||||
|
# widest at 3/10 of it. Scaling the ladder as a whole leaves small, medium and
|
||||||
|
# large distinct sizes for smaller and larger to step between.
|
||||||
|
scale_height=$anchor_height
|
||||||
|
available_width=$((anchor_width - 2 * MARGIN))
|
||||||
|
((available_width > 0 && scale_height * 3 / 10 > available_width)) &&
|
||||||
|
scale_height=$((available_width * 10 / 3))
|
||||||
|
|
||||||
|
# Scale the 8:9 portrait presets from that height so they occupy the same
|
||||||
# proportion of a 1080p, HiDPI, ultrawide, or 6K recording.
|
# proportion of a 1080p, HiDPI, ultrawide, or 6K recording.
|
||||||
small_height=$(((monitor_height * 9 + 25) / 50))
|
small_height=$(((scale_height * 9 + 25) / 50))
|
||||||
small_width=$(((small_height * 8 + 4) / 9))
|
small_width=$(((small_height * 8 + 4) / 9))
|
||||||
medium_height=$(((monitor_height + 2) / 4))
|
medium_height=$(((scale_height + 2) / 4))
|
||||||
medium_width=$(((medium_height * 8 + 4) / 9))
|
medium_width=$(((medium_height * 8 + 4) / 9))
|
||||||
large_height=$(((monitor_height * 27 + 40) / 80))
|
large_height=$(((scale_height * 27 + 40) / 80))
|
||||||
large_width=$(((large_height * 8 + 4) / 9))
|
large_width=$(((large_height * 8 + 4) / 9))
|
||||||
|
|
||||||
target_width=$current_width
|
target_width=$current_width
|
||||||
@@ -107,11 +134,11 @@ larger)
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
target_x=$((monitor_x + monitor_width - target_width - MARGIN))
|
target_x=$((anchor_x + anchor_width - target_width - MARGIN))
|
||||||
target_y=$((monitor_y + monitor_height - target_height - MARGIN))
|
target_y=$((anchor_y + anchor_height - target_height - MARGIN))
|
||||||
|
|
||||||
((target_x < monitor_x + MARGIN)) && target_x=$((monitor_x + MARGIN))
|
((target_x < anchor_x + MARGIN)) && target_x=$((anchor_x + MARGIN))
|
||||||
((target_y < monitor_y + MARGIN)) && target_y=$((monitor_y + MARGIN))
|
((target_y < anchor_y + MARGIN)) && target_y=$((anchor_y + MARGIN))
|
||||||
|
|
||||||
window="address:$address"
|
window="address:$address"
|
||||||
hypr_dispatch \
|
hypr_dispatch \
|
||||||
|
|||||||
Executable
+107
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# omarchy:summary=Pick files with the desktop file chooser
|
||||||
|
# omarchy:args=[--title <title>] [--multiple]
|
||||||
|
# omarchy:examples=omarchy file select --title "Send with Tailscale" --multiple
|
||||||
|
|
||||||
|
# Python rather than bash, alone among the commands here, because the portal
|
||||||
|
# answers a request with a Response signal addressed to the connection that
|
||||||
|
# asked, and D-Bus delivers a directed signal only to that connection. Every
|
||||||
|
# shell-callable client — gdbus call, busctl call, dbus-send — opens its own
|
||||||
|
# connection and exits before the answer arrives, and gdbus monitor registers
|
||||||
|
# with AddMatch rather than BecomeMonitor, so it never sees one either. Holding
|
||||||
|
# a single connection across both the call and the wait is the whole job, and
|
||||||
|
# bash has no way to hold one.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gio", "2.0")
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
# A dialog nobody ever answers would otherwise keep this process, and whatever
|
||||||
|
# waits on its output, alive forever.
|
||||||
|
ANSWER_TIMEOUT_SEC = 600
|
||||||
|
|
||||||
|
# Callers act on these: nothing picked is a decision, a chooser that never ran
|
||||||
|
# is a fault, and the two want different handling.
|
||||||
|
EXIT_NOTHING_PICKED = 1
|
||||||
|
EXIT_CHOOSER_FAILED = 2
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
parser.add_argument("--title", default="Select file")
|
||||||
|
parser.add_argument("--multiple", action="store_true")
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
if unknown:
|
||||||
|
print("omarchy-file-select: unknown option %s" % unknown[0], file=sys.stderr)
|
||||||
|
return EXIT_CHOOSER_FAILED
|
||||||
|
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
uris = []
|
||||||
|
|
||||||
|
def on_response(connection, sender, path, interface, signal, params):
|
||||||
|
code, results = params.unpack()
|
||||||
|
if code == 0:
|
||||||
|
uris.extend(results.get("uris", []))
|
||||||
|
loop.quit()
|
||||||
|
|
||||||
|
def subscribe(path):
|
||||||
|
bus.signal_subscribe(
|
||||||
|
"org.freedesktop.portal.Desktop",
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
path,
|
||||||
|
None,
|
||||||
|
Gio.DBusSignalFlags.NONE,
|
||||||
|
on_response,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The request path is derived from our bus name and the token we pass, so it
|
||||||
|
# can be subscribed to up front. Asking first would race a dialog that gets
|
||||||
|
# answered immediately.
|
||||||
|
token = "omarchy%d" % os.getpid()
|
||||||
|
sender = bus.get_unique_name()[1:].replace(".", "_")
|
||||||
|
predicted = "/org/freedesktop/portal/desktop/request/%s/%s" % (sender, token)
|
||||||
|
subscribe(predicted)
|
||||||
|
|
||||||
|
handle = bus.call_sync(
|
||||||
|
"org.freedesktop.portal.Desktop",
|
||||||
|
"/org/freedesktop/portal/desktop",
|
||||||
|
"org.freedesktop.portal.FileChooser",
|
||||||
|
"OpenFile",
|
||||||
|
GLib.Variant("(ssa{sv})", ("", args.title, {
|
||||||
|
"handle_token": GLib.Variant("s", token),
|
||||||
|
"multiple": GLib.Variant("b", args.multiple),
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
Gio.DBusCallFlags.NONE,
|
||||||
|
-1,
|
||||||
|
None,
|
||||||
|
).unpack()[0]
|
||||||
|
|
||||||
|
# Portals predating the token convention answer on a path of their choosing.
|
||||||
|
if handle != predicted:
|
||||||
|
subscribe(handle)
|
||||||
|
|
||||||
|
GLib.timeout_add_seconds(ANSWER_TIMEOUT_SEC, loop.quit)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
for uri in uris:
|
||||||
|
print(GLib.filename_from_uri(uri)[0])
|
||||||
|
|
||||||
|
return 0 if uris else EXIT_NOTHING_PICKED
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except GLib.Error as error:
|
||||||
|
print("omarchy-file-select: %s" % error.message, file=sys.stderr)
|
||||||
|
sys.exit(EXIT_CHOOSER_FAILED)
|
||||||
+1
-38
@@ -53,37 +53,6 @@ log_first_run() {
|
|||||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$FIRST_RUN_LOG"
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$FIRST_RUN_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
notification_server_ready() {
|
|
||||||
if omarchy-cmd-present gdbus; then
|
|
||||||
gdbus call --session \
|
|
||||||
--dest org.freedesktop.Notifications \
|
|
||||||
--object-path /org/freedesktop/Notifications \
|
|
||||||
--method org.freedesktop.Notifications.GetServerInformation >/dev/null 2>&1
|
|
||||||
elif omarchy-cmd-present busctl; then
|
|
||||||
busctl --user call \
|
|
||||||
org.freedesktop.Notifications \
|
|
||||||
/org/freedesktop/Notifications \
|
|
||||||
org.freedesktop.Notifications \
|
|
||||||
GetServerInformation >/dev/null 2>&1
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_notifications() {
|
|
||||||
omarchy-cmd-present omarchy-shell || return 0
|
|
||||||
|
|
||||||
for _ in {1..100}; do
|
|
||||||
if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
log_first_run "Timed out waiting for notification service; continuing"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
run_first_run_step() {
|
run_first_run_step() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
shift
|
shift
|
||||||
@@ -98,12 +67,6 @@ run_first_run_step() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_notifications
|
|
||||||
|
|
||||||
run_first_run_step "enable migration notification watcher" \
|
|
||||||
systemctl --user enable --now omarchy-update-user-notify.path
|
|
||||||
run_first_run_step "notify about pending migrations" omarchy-migrate-notify
|
|
||||||
|
|
||||||
run_first_run_step "install Voxtype post-update hook" \
|
run_first_run_step "install Voxtype post-update hook" \
|
||||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
|
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
|
||||||
run_first_run_step "install fingerprint setup post-update hook" \
|
run_first_run_step "install fingerprint setup post-update hook" \
|
||||||
@@ -118,7 +81,7 @@ run_first_run_step "set GTK primary paste" \
|
|||||||
run_first_run_step "apply speaker tuning" \
|
run_first_run_step "apply speaker tuning" \
|
||||||
bash "$OMARCHY_PATH/install/user/first-run/audio-tuning.sh"
|
bash "$OMARCHY_PATH/install/user/first-run/audio-tuning.sh"
|
||||||
|
|
||||||
wait_for_notifications
|
omarchy-notification-wait || log_first_run "Timed out waiting for notification service; continuing"
|
||||||
run_first_run_step "show welcome notification" \
|
run_first_run_step "show welcome notification" \
|
||||||
bash "$OMARCHY_PATH/install/user/first-run/welcome.sh"
|
bash "$OMARCHY_PATH/install/user/first-run/welcome.sh"
|
||||||
# The first-run notification scripts register action callbacks in background
|
# The first-run notification scripts register action callbacks in background
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ sudo tailscale up --accept-routes
|
|||||||
echo -e "\nAllowing $USER to manage Tailscale..."
|
echo -e "\nAllowing $USER to manage Tailscale..."
|
||||||
sudo tailscale set --operator="$USER"
|
sudo tailscale set --operator="$USER"
|
||||||
|
|
||||||
|
echo -e "\nReceiving Taildrop files in $HOME/Downloads..."
|
||||||
|
systemctl --user enable --now omarchy-tailscale-receive.service
|
||||||
|
|
||||||
echo -e "\nAdding Tailscale to the bar..."
|
echo -e "\nAdding Tailscale to the bar..."
|
||||||
omarchy-bar-plugin add omarchy.tailscale
|
omarchy-bar-plugin add omarchy.tailscale
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -96,6 +96,7 @@ while IFS=$'\t' read -r name file marker; do
|
|||||||
fi
|
fi
|
||||||
done < <(migration_entries)
|
done < <(migration_entries)
|
||||||
|
|
||||||
# Clear notifications queued while an update was installing migrations. The
|
# Clear a login-time notification the user left sitting there and then resolved
|
||||||
# substring matches both the current and legacy notification titles.
|
# by running migrations some other way. The substring matches both the current
|
||||||
|
# and legacy notification titles.
|
||||||
omarchy-notification-dismiss "Omarchy Migrations" >/dev/null 2>&1 || true
|
omarchy-notification-dismiss "Omarchy Migrations" >/dev/null 2>&1 || true
|
||||||
|
|||||||
@@ -15,11 +15,17 @@ fi
|
|||||||
|
|
||||||
notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g "Pending Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message")
|
notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g "Pending Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message")
|
||||||
|
|
||||||
if omarchy-cmd-present systemd-run; then
|
# This runs from omarchy-migrate-notify.service at graphical-session.target,
|
||||||
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
|
# which the session can reach before the shell has claimed
|
||||||
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
|
# org.freedesktop.Notifications. Without the wait the toast is sent into the
|
||||||
fi
|
# void and the user never learns about their pending migrations.
|
||||||
|
omarchy-notification-wait || true
|
||||||
|
|
||||||
|
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
|
||||||
|
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
|
||||||
|
|
||||||
|
# Reached when there is no user manager to run the scope under, such as a
|
||||||
|
# non-graphical shell, so fall back to telling the user in the terminal.
|
||||||
print_pending_migrations() {
|
print_pending_migrations() {
|
||||||
echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:"
|
echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:"
|
||||||
while IFS= read -r migration; do
|
while IFS= read -r migration; do
|
||||||
|
|||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Wait for the desktop notification server to accept notifications
|
||||||
|
# omarchy:args=[timeout-seconds]
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
timeout=${1:-10}
|
||||||
|
|
||||||
|
notification_server_ready() {
|
||||||
|
busctl --user call \
|
||||||
|
org.freedesktop.Notifications \
|
||||||
|
/org/freedesktop/Notifications \
|
||||||
|
org.freedesktop.Notifications \
|
||||||
|
GetServerInformation >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The shell has to be up to serve the IPC, and it has to have claimed the
|
||||||
|
# notification bus name before notify-send has anywhere to deliver.
|
||||||
|
attempts=$((timeout * 10))
|
||||||
|
while (( attempts > 0 )); do
|
||||||
|
if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
attempts=$((attempts - 1))
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 1
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
tailscale down 2>/dev/null || true
|
tailscale down 2>/dev/null || true
|
||||||
|
systemctl --user disable --now omarchy-tailscale-receive.service 2>/dev/null || true
|
||||||
sudo systemctl disable --now tailscaled.service 2>/dev/null || true
|
sudo systemctl disable --now tailscaled.service 2>/dev/null || true
|
||||||
omarchy-bar-plugin remove omarchy.tailscale
|
omarchy-bar-plugin remove omarchy.tailscale
|
||||||
omarchy-webapp-remove "Tailscale" 2>/dev/null || true
|
omarchy-webapp-remove "Tailscale" 2>/dev/null || true
|
||||||
|
|||||||
Executable
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Save incoming Taildrop files and announce them
|
||||||
|
# omarchy:args=[--once] [directory]
|
||||||
|
# omarchy:examples=omarchy tailscale receive | omarchy tailscale receive --once ~/Desktop
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
once=false
|
||||||
|
if [[ ${1:-} == "--once" ]]; then
|
||||||
|
once=true
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
dir="${1:-${XDG_DOWNLOAD_DIR:-$HOME/Downloads}}"
|
||||||
|
|
||||||
|
# Taildrop lands in a staging directory next door rather than straight in the
|
||||||
|
# downloads directory: waiting for a delivery can take hours, and everything
|
||||||
|
# else that shows up meanwhile is somebody else's file. Same filesystem, so
|
||||||
|
# handing the finished file over is a rename.
|
||||||
|
staging="$dir/.omarchy-taildrop"
|
||||||
|
mkdir -p "$staging"
|
||||||
|
|
||||||
|
# Take the name by linking to it rather than by looking and then renaming.
|
||||||
|
# link(2) refuses an existing name, so nothing can land on the chosen one in
|
||||||
|
# the gap between the two. Staging shares the filesystem with the downloads
|
||||||
|
# directory, so the link always resolves and unlinking the staged name
|
||||||
|
# finishes the move. Prints the name it took.
|
||||||
|
claim_path() {
|
||||||
|
local staged="$1" name="${staged##*/}" base ext candidate index=0
|
||||||
|
|
||||||
|
base="${name%.*}"
|
||||||
|
ext="${name#"$base"}"
|
||||||
|
[[ -z $base ]] && { base="$name"; ext=""; }
|
||||||
|
|
||||||
|
while (( index < 1000 )); do
|
||||||
|
if (( index == 0 )); then
|
||||||
|
candidate="$dir/$name"
|
||||||
|
else
|
||||||
|
candidate="$dir/$base-$index$ext"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ln -- "$staged" "$candidate" 2>/dev/null; then
|
||||||
|
rm -f -- "$staged"
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only a taken name is worth another spin. Anything else failed the link
|
||||||
|
# itself, and the file keeps its place in staging for the next run.
|
||||||
|
[[ -e $candidate ]] || return 1
|
||||||
|
|
||||||
|
((index++))
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
announce() {
|
||||||
|
local path="$1"
|
||||||
|
local name="${path##*/}"
|
||||||
|
local args=("Received $name" "Saved to ${dir/#$HOME/~}")
|
||||||
|
|
||||||
|
case "${name,,}" in
|
||||||
|
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||||
|
args+=(--image "$path")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
args+=(-g )
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Clicking the notification opens the file, so this waits for the toast to
|
||||||
|
# go away. Callers background it to keep receiving in the meantime.
|
||||||
|
if [[ -n $(omarchy-notification-send "${args[@]}" -a) ]]; then
|
||||||
|
xdg-open "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
deliver() {
|
||||||
|
local staged target
|
||||||
|
|
||||||
|
while IFS= read -r staged; do
|
||||||
|
target=$(claim_path "$staged") || continue
|
||||||
|
announce "$target" &
|
||||||
|
done < <(find "$staging" -mindepth 1 -maxdepth 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anything left staged by an interrupted run still deserves delivering.
|
||||||
|
deliver
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! tailscale file get --wait --conflict=rename "$staging"; then
|
||||||
|
$once && exit 1
|
||||||
|
sleep 10
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
deliver
|
||||||
|
$once && exit 0
|
||||||
|
done
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Send files to a machine on your tailnet with Taildrop
|
||||||
|
# omarchy:args=<machine> [file...]
|
||||||
|
# omarchy:examples=omarchy tailscale send dhh-fd | omarchy tailscale send dhh-fd ~/Downloads/notes.pdf
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if (($# < 1)); then
|
||||||
|
echo "Usage: omarchy-tailscale-send <machine> [file...]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
machine="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
# Address the machine by whatever name we were handed, but talk about it by
|
||||||
|
# its short name, so a MagicDNS name does not spill into every message.
|
||||||
|
name="${machine%%.*}"
|
||||||
|
|
||||||
|
files=("$@")
|
||||||
|
|
||||||
|
if ((${#files[@]} == 0)); then
|
||||||
|
# Command substitution so the chooser's exit status survives: reading it
|
||||||
|
# through a process substitution reports success for a chooser that never
|
||||||
|
# opened, which is indistinguishable here from someone deciding not to send.
|
||||||
|
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
|
||||||
|
|
||||||
|
if ((${status:-0} > 1)); then
|
||||||
|
omarchy-notification-send -g "" -u critical "Could not send to $name" \
|
||||||
|
"The file chooser did not open"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
readarray -t files <<<"$picked"
|
||||||
|
[[ -n $picked ]] || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#files[@]} == 1)); then
|
||||||
|
what=$(basename "${files[0]}")
|
||||||
|
else
|
||||||
|
what="${#files[@]} files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
|
||||||
|
omarchy-notification-send -g "" "Sent to $name" "$what"
|
||||||
|
else
|
||||||
|
omarchy-notification-send -g "" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -38,6 +38,34 @@
|
|||||||
</edit>
|
</edit>
|
||||||
</match>
|
</match>
|
||||||
|
|
||||||
|
<!-- Urdu is conventionally set in Nastaliq, so keep it there. Without this
|
||||||
|
the untargeted Naskh rule below would capture Urdu too. Appended rather
|
||||||
|
than prepended so it only wins the last-resort race against that rule
|
||||||
|
and never displaces the family the app actually asked for. -->
|
||||||
|
<match target="pattern">
|
||||||
|
<test name="lang" compare="contains">
|
||||||
|
<string>ur</string>
|
||||||
|
</test>
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Nastaliq Urdu</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
|
<!-- Chromium and Electron resolve missing glyphs one character at a time
|
||||||
|
without lang on the pattern, so the rules above never fire for them and
|
||||||
|
fontconfig falls through to a raw charset scan that lands on Nastaliq
|
||||||
|
(or on Kufi, once the latin alias chains have widened the pattern).
|
||||||
|
Naming Naskh as a last-resort family covers that path. Appending keeps
|
||||||
|
it behind every family the app actually asked for, and charset matching
|
||||||
|
outranks family matching, so a font that does not cover the codepoint
|
||||||
|
can never be pulled in: Latin, monospace, emoji, Nerd Font glyphs and
|
||||||
|
CJK all resolve exactly as before. -->
|
||||||
|
<match target="pattern">
|
||||||
|
<edit name="family" mode="append" binding="strong">
|
||||||
|
<string>Noto Naskh Arabic</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
|
||||||
<alias>
|
<alias>
|
||||||
<family>system-ui</family>
|
<family>system-ui</family>
|
||||||
<prefer>
|
<prefer>
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
-- DaVinci Resolve dialog focus handling.
|
-- DaVinci Resolve window focus handling. Kept fully opaque: the default
|
||||||
o.window(".*[Rr]esolve.*", { float = true, stay_focused = true })
|
-- translucency distorts colour-critical grading work.
|
||||||
|
o.window(".*[Rr]esolve.*", {
|
||||||
|
float = true,
|
||||||
|
stay_focused = true,
|
||||||
|
tag = "-default-opacity",
|
||||||
|
opacity = "1 1",
|
||||||
|
})
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ o.window(
|
|||||||
tag = "+floating-window",
|
tag = "+floating-window",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
-- The portal only ever shows dialogs — file pickers, screen shares, permission
|
||||||
|
-- prompts — so every one of its windows belongs in the floating treatment,
|
||||||
|
-- whatever the app that asked for it titled it.
|
||||||
|
o.window("xdg-desktop-portal-gtk", { tag = "+floating-window" })
|
||||||
o.window({
|
o.window({
|
||||||
class = "(xdg-desktop-portal-gtk|sublime_text|DesktopEditors|org.gnome.Nautilus)",
|
class = "(sublime_text|DesktopEditors|org.gnome.Nautilus)",
|
||||||
title = "^(Open.*Files?|Open [F|f]older.*|Save.*Files?|Save.*As|Save|All Files|.*wants to [open|save].*|[C|c]hoose.*)",
|
title = "^(Open.*Files?|Open [F|f]older.*|Save.*Files?|Save.*As|Save|All Files|.*wants to [open|save].*|[C|c]hoose.*)",
|
||||||
}, { tag = "+floating-window" })
|
}, { tag = "+floating-window" })
|
||||||
o.window("dev.tensaku.Tensaku", { float = true })
|
o.window("dev.tensaku.Tensaku", { float = true })
|
||||||
|
|||||||
+23
-3
@@ -21,14 +21,34 @@ local function read_vconsole()
|
|||||||
return values
|
return values
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Layouts that can't type Latin letters. Keep in sync with the list in
|
||||||
|
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
|
||||||
|
local non_latin_layouts =
|
||||||
|
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
|
||||||
|
|
||||||
local vconsole = read_vconsole()
|
local vconsole = read_vconsole()
|
||||||
|
|
||||||
|
local kb_layout = vconsole.XKBLAYOUT or "us"
|
||||||
|
local kb_variant = vconsole.XKBVARIANT or ""
|
||||||
|
local kb_options = "compose:caps,shift:both_capslock"
|
||||||
|
|
||||||
|
-- Hyprland resolves keybindings against the first entry in kb_layout, not the
|
||||||
|
-- layout that's currently active, so Omarchy's Latin-keysym bindings (SUPER + W
|
||||||
|
-- and friends) only fire when a Latin layout leads. Installing with a non-Latin
|
||||||
|
-- one would otherwise leave the desktop unusable.
|
||||||
|
if non_latin_layouts:find(" " .. kb_layout:match("^[^,]*") .. " ", 1, true) then
|
||||||
|
kb_layout = "us," .. kb_layout
|
||||||
|
kb_variant = "," .. kb_variant
|
||||||
|
-- Reach the original layout with Left Alt + Right Alt.
|
||||||
|
kb_options = kb_options .. ",grp:alts_toggle"
|
||||||
|
end
|
||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
input = {
|
input = {
|
||||||
kb_layout = vconsole.XKBLAYOUT or "us",
|
kb_layout = kb_layout,
|
||||||
kb_variant = vconsole.XKBVARIANT or "",
|
kb_variant = kb_variant,
|
||||||
kb_model = "",
|
kb_model = "",
|
||||||
kb_options = "compose:caps,shift:both_capslock",
|
kb_options = kb_options,
|
||||||
kb_rules = "",
|
kb_rules = "",
|
||||||
follow_mouse = 1,
|
follow_mouse = 1,
|
||||||
sensitivity = 0,
|
sensitivity = 0,
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
"setup.default.browser.brave": {"icon":"","label":"Brave","when":"omarchy-cmd-present brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
|
"setup.default.browser.brave": {"icon":"","label":"Brave","when":"omarchy-cmd-present brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
|
||||||
"setup.default.browser.brave-origin": {"icon":"","label":"Brave Origin","when":"omarchy-cmd-present brave-origin","checked":"[[ \"$(omarchy-default-browser)\" == \"brave-origin\" ]]","action":"omarchy-default-browser brave-origin"},
|
"setup.default.browser.brave-origin": {"icon":"","label":"Brave Origin","when":"omarchy-cmd-present brave-origin","checked":"[[ \"$(omarchy-default-browser)\" == \"brave-origin\" ]]","action":"omarchy-default-browser brave-origin"},
|
||||||
"setup.default.browser.edge": {"icon":"","label":"Edge","when":"omarchy-cmd-present microsoft-edge-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
|
"setup.default.browser.edge": {"icon":"","label":"Edge","when":"omarchy-cmd-present microsoft-edge-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
|
||||||
"setup.default.browser.firefox": {"icon":"","label":"Firefox","when":"omarchy-cmd-present firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
|
"setup.default.browser.firefox": {"icon":"","label":"Firefox","when":"omarchy-cmd-present firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
|
||||||
"setup.default.browser.zen": {"icon":"","label":"Zen","when":"omarchy-cmd-present zen-browser","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
|
"setup.default.browser.zen": {"icon":"","label":"Zen","when":"omarchy-cmd-present zen-browser","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
|
||||||
"setup.default.terminal": {"icon":"","label":"Terminal"},
|
"setup.default.terminal": {"icon":"","label":"Terminal"},
|
||||||
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
|
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Notify about pending Omarchy migrations
|
||||||
|
# Login-only. There used to be an omarchy-update-user-notify.path watching
|
||||||
|
# /usr/share/omarchy/migrations, but pacman writes that directory during every
|
||||||
|
# update -- including the blessed `omarchy update`, which runs omarchy-migrate
|
||||||
|
# itself a step later -- so the watcher notified about migrations that were
|
||||||
|
# already being applied in the visible update terminal. Checking once per login
|
||||||
|
# is the only trigger that cannot collide with a running update.
|
||||||
|
ConditionPathIsDirectory=/usr/share/omarchy/migrations
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/omarchy-migrate-notify
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Save incoming Taildrop files to the downloads directory
|
||||||
|
ConditionPathExists=/usr/bin/tailscale
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/omarchy-tailscale-receive
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Watch for Omarchy migrations
|
|
||||||
|
|
||||||
[Path]
|
|
||||||
# Edge-triggered watch only. PathExistsGlob= is level-triggered: it re-fires
|
|
||||||
# every time the triggered unit deactivates for as long as the glob matches,
|
|
||||||
# and applied migrations stay on disk forever, so a glob here busy-loops the
|
|
||||||
# oneshot service. The once-per-login check lives in the service's own
|
|
||||||
# WantedBy=graphical-session.target instead.
|
|
||||||
PathModified=/usr/share/omarchy/migrations
|
|
||||||
Unit=omarchy-update-user-notify.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical-session.target
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Notify about pending Omarchy migrations
|
|
||||||
ConditionPathIsDirectory=/usr/share/omarchy/migrations
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/bin/omarchy-migrate-notify
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical-session.target
|
|
||||||
+17
-10
@@ -197,13 +197,19 @@ migration. Migrations run as the user; privileged work should invoke the
|
|||||||
appropriate helper or privilege prompt. Migrations must be idempotent;
|
appropriate helper or privilege prompt. Migrations must be idempotent;
|
||||||
machine-wide repairs should no-op when another user already applied them.
|
machine-wide repairs should no-op when another user already applied them.
|
||||||
|
|
||||||
Each graphical user has `omarchy-update-user-notify.path` watching the packaged
|
Each graphical user has `omarchy-migrate-notify.service`, started once per login
|
||||||
migration directory for changes, and `omarchy-update-user-notify.service` is
|
through `WantedBy=graphical-session.target`. The package also ships
|
||||||
also started once per login via its own `WantedBy=graphical-session.target`.
|
`omarchy-update-user-notify.service` as a symlink onto it, so users enabled
|
||||||
Either way the service runs `omarchy-migrate-notify` as that user. The notifier checks
|
under the old unit name keep working before they reach migration `1785095882`.
|
||||||
`omarchy-migrate --pending`. If this user has missing migration state, it shows a
|
It runs `omarchy-migrate-notify` as
|
||||||
notification that opens a terminal for `omarchy-migrate`. The notifier never runs
|
that user, which checks `omarchy-migrate --pending`. If this user has missing
|
||||||
migrations in the background.
|
migration state, it shows a notification that opens a terminal for
|
||||||
|
`omarchy-migrate`. The notifier never runs migrations in the background.
|
||||||
|
|
||||||
|
Login is the only trigger. Nothing watches the packaged migration directory: a
|
||||||
|
watcher cannot tell a bypassed `pacman -Syu` from the package transaction inside
|
||||||
|
a normal `omarchy update`, so it notified about migrations that `omarchy-migrate`
|
||||||
|
was already applying in the visible update terminal.
|
||||||
|
|
||||||
`omarchy-migrate` waits for any active pacman transaction to finish, then runs
|
`omarchy-migrate` waits for any active pacman transaction to finish, then runs
|
||||||
pending migrations. It does not need `--force`; migrations happen when state
|
pending migrations. It does not need `--force`; migrations happen when state
|
||||||
@@ -221,8 +227,8 @@ systemd instance:
|
|||||||
Voxtype post-update hook.
|
Voxtype post-update hook.
|
||||||
- `install/user/first-run/enable-user-units.sh` — `systemctl --user enable`
|
- `install/user/first-run/enable-user-units.sh` — `systemctl --user enable`
|
||||||
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
|
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
|
||||||
`omarchy-recover-internal-monitor`, `omarchy-update-user-notify.path`,
|
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`).
|
||||||
`omarchy-update-user-notify.service`). Done here, not at finalize, because
|
Done here, not at finalize, because
|
||||||
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
|
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
|
||||||
in the unit files keeps services inert when they don't apply.
|
in the unit files keeps services inert when they don't apply.
|
||||||
- `install/user/first-run/gnome-theme.sh`,
|
- `install/user/first-run/gnome-theme.sh`,
|
||||||
@@ -250,7 +256,8 @@ the legacy finalization marker from `~/.local/state/omarchy/` into `done/`.
|
|||||||
finalization. It sources:
|
finalization. It sources:
|
||||||
|
|
||||||
- `install/config/all.sh` — theme links, lockout limits, lockscreen PAM,
|
- `install/config/all.sh` — theme links, lockout limits, lockscreen PAM,
|
||||||
powerprofilesctl shebang fix, docker setup, service enablement, firewall.
|
powerprofilesctl shebang fix, docker setup, Snapper retention, locate
|
||||||
|
index tuning, service enablement, firewall.
|
||||||
- `install/hardware/all.sh` via `omarchy-setup-hardware` — vendor- and
|
- `install/hardware/all.sh` via `omarchy-setup-hardware` — vendor- and
|
||||||
device-specific kernel modules, udev rules, microcode, wireless regdom,
|
device-specific kernel modules, udev rules, microcode, wireless regdom,
|
||||||
ASUS / Framework / Intel / Apple / Lenovo quirks.
|
ASUS / Framework / Intel / Apple / Lenovo quirks.
|
||||||
|
|||||||
+11
-9
@@ -43,16 +43,9 @@ omarchy-hook post-update
|
|||||||
`omarchy-migrate` waits for any active pacman transaction to finish, then runs
|
`omarchy-migrate` waits for any active pacman transaction to finish, then runs
|
||||||
all pending migrations for the current user in the visible update terminal.
|
all pending migrations for the current user in the visible update terminal.
|
||||||
|
|
||||||
### During direct pacman updates
|
### At login
|
||||||
|
|
||||||
Raw `sudo pacman -Syu` is guarded. Users should normally run:
|
Every graphical login starts `omarchy-migrate-notify.service`, which checks:
|
||||||
|
|
||||||
```bash
|
|
||||||
omarchy update
|
|
||||||
```
|
|
||||||
|
|
||||||
If a user explicitly bypasses the guard, user sessions watch the packaged
|
|
||||||
migration directory and run a notifier. The notifier checks:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
omarchy-migrate --pending
|
omarchy-migrate --pending
|
||||||
@@ -67,6 +60,15 @@ omarchy-migrate
|
|||||||
|
|
||||||
The notifier never runs migrations silently in the background.
|
The notifier never runs migrations silently in the background.
|
||||||
|
|
||||||
|
This is what covers users who did not run the update themselves: someone who
|
||||||
|
bypassed the pacman guard with `sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman
|
||||||
|
-Syu`, and any second user on the machine, whose migration markers are per-user
|
||||||
|
and therefore still missing after another user updated.
|
||||||
|
|
||||||
|
Login is the only trigger on purpose. Watching the packaged migration directory
|
||||||
|
also fires during a normal `omarchy update`, which prompts for migrations that
|
||||||
|
`omarchy-migrate` is about to run in the visible update terminal.
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
||||||
Users can safely run:
|
Users can safely run:
|
||||||
|
|||||||
+26
-8
@@ -149,20 +149,37 @@ High-level flow:
|
|||||||
sudo pacman -Syu
|
sudo pacman -Syu
|
||||||
├─ pre-transaction guard aborts and tells the user to run omarchy update
|
├─ pre-transaction guard aborts and tells the user to run omarchy update
|
||||||
└─ if explicitly bypassed, upgrades omarchy and related packages
|
└─ if explicitly bypassed, upgrades omarchy and related packages
|
||||||
└─ user session notices migration directory changes
|
└─ at that user's next login
|
||||||
├─ omarchy-update-user-notify.path triggers, if enabled
|
├─ omarchy-migrate-notify.service starts with graphical-session.target
|
||||||
├─ omarchy-migrate-notify checks omarchy-migrate --pending
|
├─ omarchy-migrate-notify checks omarchy-migrate --pending
|
||||||
├─ if this user has missing migration state, show notification
|
├─ if this user has missing migration state, show notification
|
||||||
└─ click opens terminal: omarchy-migrate
|
└─ click opens terminal: omarchy-migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Login is deliberately the only trigger. A watcher on the packaged migration
|
||||||
|
directory cannot distinguish a bypassed `pacman -Syu` from the package
|
||||||
|
transaction inside a normal `omarchy update`, so it fired notifications for
|
||||||
|
migrations that `omarchy-migrate` was about to apply in the visible update
|
||||||
|
terminal. The retired unit was `omarchy-update-user-notify.path`.
|
||||||
|
|
||||||
Fallbacks:
|
Fallbacks:
|
||||||
|
|
||||||
- `omarchy-first-run` enables the user notification path unit.
|
- `omarchy-first-run` enables `omarchy-migrate-notify.service`, which also
|
||||||
- `omarchy-first-run` also invokes `omarchy-migrate-notify` on graphical
|
covers users created after install: their per-user migration markers are
|
||||||
startup, so users who updated before the path unit existed still get prompted
|
missing, so their first login prompts them to run every shipped migration.
|
||||||
if they have missing migration state.
|
- The package ships `omarchy-update-user-notify.service` as a symlink onto
|
||||||
|
`omarchy-migrate-notify.service`. Users set up before the rename hold an
|
||||||
|
absolute `graphical-session.target.wants` symlink to the old path, and the
|
||||||
|
migration that repoints it only runs for users who run an update — the
|
||||||
|
opposite of who the notifier is for. The alias can be dropped once installs
|
||||||
|
have run migration `1785095882`.
|
||||||
|
- The notifier waits for a live notification server before sending, because
|
||||||
|
`graphical-session.target` can be reached before the shell claims
|
||||||
|
`org.freedesktop.Notifications`.
|
||||||
- The notifier is only a prompt. It does not run migrations in the background.
|
- The notifier is only a prompt. It does not run migrations in the background.
|
||||||
|
- A session that is already open when another user updates is not re-checked;
|
||||||
|
it picks the migrations up at its next login, or whenever that user runs
|
||||||
|
`omarchy-migrate` or `omarchy update`.
|
||||||
- Direct pacman updates do not run `omarchy-hook post-update` unless the user
|
- Direct pacman updates do not run `omarchy-hook post-update` unless the user
|
||||||
explicitly runs that hook; without a package-update marker, the only pending
|
explicitly runs that hook; without a package-update marker, the only pending
|
||||||
state we can derive is missing per-user migration markers.
|
state we can derive is missing per-user migration markers.
|
||||||
@@ -209,7 +226,7 @@ scripts.
|
|||||||
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with targeted transition `--overwrite` entries so the ALPM guard allows the transaction and early package-layout conflicts are handled. | **Keep for now.** Small leaf command, clear/testable. |
|
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with targeted transition `--overwrite` entries so the ALPM guard allows the transaction and early package-layout conflicts are handled. | **Keep for now.** Small leaf command, clear/testable. |
|
||||||
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
|
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
|
||||||
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
|
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
|
||||||
| `omarchy-migrate-notify` | Internal notification helper for direct pacman updates. Uses `omarchy-migrate --pending` and shows notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
|
| `omarchy-migrate-notify` | Internal login-time notification helper. Uses `omarchy-migrate --pending` and shows a notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
|
||||||
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
|
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
|
||||||
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
|
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
|
||||||
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
|
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
|
||||||
@@ -229,7 +246,8 @@ scripts.
|
|||||||
idempotent when they repair machine-wide state.
|
idempotent when they repair machine-wide state.
|
||||||
|
|
||||||
2. **Migration notification naming**
|
2. **Migration notification naming**
|
||||||
- The real helper is `omarchy-migrate-notify`.
|
- The real helper is `omarchy-migrate-notify`, started by
|
||||||
|
`omarchy-migrate-notify.service`.
|
||||||
- `omarchy-update-user-notify` remains only as a hidden compatibility wrapper.
|
- `omarchy-update-user-notify` remains only as a hidden compatibility wrapper.
|
||||||
|
|
||||||
3. **Update pipeline ownership**
|
3. **Update pipeline ownership**
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ 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"
|
||||||
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
||||||
|
run_logged "$OMARCHY_INSTALL/config/locate.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
||||||
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
|
||||||
|
|
||||||
|
echo "Configuring locate to skip Btrfs snapshots and index Btrfs subvolumes"
|
||||||
|
|
||||||
|
[[ -f $UPDATEDB_CONF_PATH ]] || exit 0
|
||||||
|
|
||||||
|
# updatedb refuses to run at all on a config that defines a variable twice, so
|
||||||
|
# every setting here is rewritten where it already stands and only appended
|
||||||
|
# when the file has no line for it.
|
||||||
|
|
||||||
|
# Btrfs subvolume mounts (like /home) look like bind mounts, so pruning
|
||||||
|
# bind mounts leaves them out of the index entirely.
|
||||||
|
if grep -qE '^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then
|
||||||
|
sed -i -E 's|^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=.*|PRUNE_BIND_MOUNTS = "no"|' "$UPDATEDB_CONF_PATH"
|
||||||
|
else
|
||||||
|
printf '%s\n' 'PRUNE_BIND_MOUNTS = "no"' >>"$UPDATEDB_CONF_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Snapper snapshots are nested subvolumes reached by plain directory
|
||||||
|
# traversal, so without this updatedb indexes the system once per snapshot.
|
||||||
|
if grep -qE '^[[:space:]]*PRUNEPATHS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then
|
||||||
|
# updatedb only accepts quoted values and allows a comment after them. Read
|
||||||
|
# back what the machine already prunes and write the whole setting out again
|
||||||
|
# rather than splicing into a line of unknown shape.
|
||||||
|
pruned=$(sed -nE 's|^[[:space:]]*PRUNEPATHS[[:space:]]*=[[:space:]]*"([^"]*)".*|\1|p' "$UPDATEDB_CONF_PATH" | tail -n 1)
|
||||||
|
|
||||||
|
if [[ " $pruned " != *" /.snapshots "* ]]; then
|
||||||
|
sed -i -E "s|^[[:space:]]*PRUNEPATHS[[:space:]]*=.*|PRUNEPATHS = \"/.snapshots${pruned:+ $pruned}\"|" "$UPDATEDB_CONF_PATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' 'PRUNEPATHS = "/.snapshots"' >>"$UPDATEDB_CONF_PATH"
|
||||||
|
fi
|
||||||
@@ -16,5 +16,4 @@ systemctl --user enable --now \
|
|||||||
bt-agent.service \
|
bt-agent.service \
|
||||||
omarchy-recover-internal-monitor.service \
|
omarchy-recover-internal-monitor.service \
|
||||||
omarchy-sleep-lock.service \
|
omarchy-sleep-lock.service \
|
||||||
omarchy-update-user-notify.path \
|
omarchy-migrate-notify.service
|
||||||
omarchy-update-user-notify.service
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
echo "Configure locate to skip Btrfs snapshots and index Btrfs subvolumes"
|
||||||
|
|
||||||
|
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||||
|
locate_config_script="$OMARCHY_PATH/install/config/locate.sh"
|
||||||
|
UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}"
|
||||||
|
|
||||||
|
as_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -f $UPDATEDB_CONF_PATH ]] || exit 0
|
||||||
|
[[ -f $locate_config_script ]] || exit 0
|
||||||
|
|
||||||
|
if grep -q '^PRUNE_BIND_MOUNTS = "no"' "$UPDATEDB_CONF_PATH" &&
|
||||||
|
grep -E '^PRUNEPATHS' "$UPDATEDB_CONF_PATH" | grep -qE '(^|[[:space:]"])/\.snapshots([[:space:]"]|$)'; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
as_root env OMARCHY_UPDATEDB_CONF_PATH="$UPDATEDB_CONF_PATH" bash -euo pipefail "$locate_config_script"
|
||||||
|
|
||||||
|
# Rebuild the index with the new exclusions; pruning /.snapshots turns
|
||||||
|
# multi-hour runs on snapshot-heavy systems back into one-minute runs. Restart
|
||||||
|
# rather than start: the machines this targets are the ones with an updatedb
|
||||||
|
# already grinding through every snapshot, and a run that started before the
|
||||||
|
# rewrite keeps using the config it read at startup.
|
||||||
|
as_root systemctl restart --no-block plocate-updatedb.service >/dev/null 2>&1 || true
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
echo "Remove Snapper timeline snapshots leaked by earlier defaults"
|
||||||
|
|
||||||
|
SNAPPER_CONFIG_PATH="${OMARCHY_SNAPPER_CONFIG_PATH:-/etc/snapper/configs/root}"
|
||||||
|
|
||||||
|
as_root() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v snapper >/dev/null || exit 0
|
||||||
|
[[ -f $SNAPPER_CONFIG_PATH ]] || exit 0
|
||||||
|
|
||||||
|
# Only clean up when timeline snapshotting is off, as Omarchy configures it.
|
||||||
|
# Anyone who deliberately turned it back on keeps their snapshots. Snapper's
|
||||||
|
# own create-config leaves the file readable by root alone, and a config this
|
||||||
|
# user cannot read must not pass for one that wants its snapshots kept.
|
||||||
|
if [[ -r $SNAPPER_CONFIG_PATH ]]; then
|
||||||
|
grep -qFx 'TIMELINE_CREATE="no"' "$SNAPPER_CONFIG_PATH" || exit 0
|
||||||
|
else
|
||||||
|
as_root grep -qFx 'TIMELINE_CREATE="no"' "$SNAPPER_CONFIG_PATH" || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Earlier installs ran hourly timeline snapshots. Later configs stopped
|
||||||
|
# creating them but never deleted the existing ones, and number cleanup
|
||||||
|
# skips snapshots marked Cleanup=timeline, so they pile up forever:
|
||||||
|
# hundreds of snapshots pinning 100+ GB of extents on long-running machines.
|
||||||
|
leaked=$(as_root snapper -c root --csvout list --columns number,cleanup 2>/dev/null | awk -F, '$2 == "timeline" { print $1 }' || true)
|
||||||
|
[[ -n $leaked ]] || exit 0
|
||||||
|
|
||||||
|
echo "Deleting $(wc -w <<<"$leaked") leaked timeline snapshots (disk space is reclaimed in the background)"
|
||||||
|
|
||||||
|
# Delete in small batches; one big delete can die on a DBus timeout partway.
|
||||||
|
# A failed batch must not take the rest of the migration run down with it, so
|
||||||
|
# the drain is best effort. omarchy-migrate records the migration either way,
|
||||||
|
# so say what is left rather than counting on a rerun that will not come.
|
||||||
|
failed=0
|
||||||
|
batch=()
|
||||||
|
|
||||||
|
for number in $leaked; do
|
||||||
|
batch+=("$number")
|
||||||
|
if (( ${#batch[@]} == 20 )); then
|
||||||
|
as_root snapper -c root delete "${batch[@]}" || failed=$((failed + ${#batch[@]}))
|
||||||
|
batch=()
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ${#batch[@]} > 0 )); then
|
||||||
|
as_root snapper -c root delete "${batch[@]}" || failed=$((failed + ${#batch[@]}))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( failed > 0 )); then
|
||||||
|
echo "$failed snapshots could not be deleted. Finish with: sudo snapper -c root delete <number>"
|
||||||
|
fi
|
||||||
@@ -12,10 +12,13 @@ sudo systemctl reload systemd-logind >/dev/null 2>&1 || true
|
|||||||
# the old five second window in place. omarchy-system-sleep-lock reads this same
|
# the old five second window in place. omarchy-system-sleep-lock reads this same
|
||||||
# property at runtime, so it stays correct either way -- the reboot flag is only
|
# property at runtime, so it stays correct either way -- the reboot flag is only
|
||||||
# about getting the wider window to take effect.
|
# about getting the wider window to take effect.
|
||||||
|
#
|
||||||
|
# Both reads have to survive failing: a missing drop-in or an unreachable logind
|
||||||
|
# is the very condition being tested for, and migrations run under -e.
|
||||||
dropin=/etc/systemd/logind.conf.d/20-inhibit-delay.conf
|
dropin=/etc/systemd/logind.conf.d/20-inhibit-delay.conf
|
||||||
expected_s=$(sed -n 's/^InhibitDelayMaxSec=//p' "$dropin" 2>/dev/null)
|
expected_s=$(sed -n 's/^InhibitDelayMaxSec=//p' "$dropin" 2>/dev/null || true)
|
||||||
effective_us=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
effective_us=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
||||||
org.freedesktop.login1.Manager InhibitDelayMaxUSec 2>/dev/null | awk '{print $2}')
|
org.freedesktop.login1.Manager InhibitDelayMaxUSec 2>/dev/null | awk '{print $2}' || true)
|
||||||
|
|
||||||
if [[ -z $expected_s || $effective_us != $((expected_s * 1000000)) ]]; then
|
if [[ -z $expected_s || $effective_us != $((expected_s * 1000000)) ]]; then
|
||||||
omarchy-state set reboot-required
|
omarchy-state set reboot-required
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
echo "Move zram tuning to a vendor drop-in"
|
echo "Move zram tuning to a vendor drop-in"
|
||||||
|
|
||||||
zram_conf="${OMARCHY_ZRAM_CONF:-/etc/systemd/zram-generator.conf}"
|
zram_conf="${OMARCHY_ZRAM_CONF:-/etc/systemd/zram-generator.conf}"
|
||||||
|
zram_dropin="${OMARCHY_ZRAM_DROPIN:-/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf}"
|
||||||
|
|
||||||
# The tuning ships as /usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf.
|
# The tuning ships as /usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf.
|
||||||
# Drop-ins outrank the main config file, so a leftover /etc copy decides nothing
|
# Drop-ins outrank the main config file, so a leftover /etc copy decides nothing
|
||||||
@@ -8,6 +9,13 @@ zram_conf="${OMARCHY_ZRAM_CONF:-/etc/systemd/zram-generator.conf}"
|
|||||||
|
|
||||||
[[ -f $zram_conf ]] || exit 0
|
[[ -f $zram_conf ]] || exit 0
|
||||||
|
|
||||||
|
# Only once the replacement is on disk. zram-generator makes no device at all
|
||||||
|
# when nothing configures one, so until the drop-in lands the /etc copy is the
|
||||||
|
# only thing standing between this machine and no zram swap. The update pipeline
|
||||||
|
# installs packages before it runs migrations, but a dev checkout carries
|
||||||
|
# migrations from a release the installed package does not have yet.
|
||||||
|
[[ -f $zram_dropin ]] || exit 0
|
||||||
|
|
||||||
# Package-owned copies go away with their package on upgrade.
|
# Package-owned copies go away with their package on upgrade.
|
||||||
pacman -Qo "$zram_conf" &>/dev/null && exit 0
|
pacman -Qo "$zram_conf" &>/dev/null && exit 0
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,16 @@ if sudo systemctl daemon-reload; then
|
|||||||
# pressure keeps its old size until the next boot.
|
# pressure keeps its old size until the next boot.
|
||||||
zram_used=$(awk '$1 == "/dev/zram0" {print $4}' "$swaps")
|
zram_used=$(awk '$1 == "/dev/zram0" {print $4}' "$swaps")
|
||||||
|
|
||||||
if [[ ${zram_used:-0} == 0 ]] && sudo systemctl restart dev-zram0.swap; then
|
# No row at all means the device is not swap right now, and that covers two
|
||||||
|
# unlike situations. A device that doesn't exist yet is the one worth acting
|
||||||
|
# on: the restart brings it up against the config daemon-reload just picked
|
||||||
|
# up. A device that exists but is swapped off is not, because the restart
|
||||||
|
# resets it first, and reset returns EBUSY for as long as anything still
|
||||||
|
# holds it open. That leaves a "Job failed" from systemd in the migration
|
||||||
|
# output and still ends up asking for the reboot, so go straight there.
|
||||||
|
if [[ -n $zram_used || ! -e $zram_disksize ]] &&
|
||||||
|
[[ ${zram_used:-0} == 0 ]] &&
|
||||||
|
sudo systemctl restart dev-zram0.swap; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
echo "Only check for pending migrations at login, not on every package update"
|
||||||
|
|
||||||
|
# omarchy-update-user-notify.path watched /usr/share/omarchy/migrations, but
|
||||||
|
# pacman writes that directory during every update -- including the blessed
|
||||||
|
# `omarchy update`, which runs omarchy-migrate a step later. The watcher fired a
|
||||||
|
# critical notification for migrations that were already being applied in the
|
||||||
|
# visible update terminal. Retire the watcher and keep only the once-per-login
|
||||||
|
# check, now named after the command it runs.
|
||||||
|
|
||||||
|
wants_dir="$HOME/.config/systemd/user/graphical-session.target.wants"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# The watcher's unit file is already gone, but it stays loaded in a session that
|
||||||
|
# started before this update, so stop it before it can fire again.
|
||||||
|
systemctl --user stop omarchy-update-user-notify.path >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Enable the replacement before dropping the old enablement, so a failure here
|
||||||
|
# can never leave a user with no notifier at all. Enable without --now: this
|
||||||
|
# usually runs from inside `omarchy update`, and starting the notifier here would
|
||||||
|
# pop a toast for the migrations running right after it -- the exact behavior
|
||||||
|
# being removed. `systemctl enable` also needs a live user manager, which
|
||||||
|
# `omarchy update` over SSH does not have, so fall back to writing precisely the
|
||||||
|
# symlink it would have written rather than silently doing nothing.
|
||||||
|
if ! systemctl --user enable omarchy-migrate-notify.service >/dev/null 2>&1; then
|
||||||
|
mkdir -p "$wants_dir"
|
||||||
|
ln -sfn /usr/lib/systemd/user/omarchy-migrate-notify.service \
|
||||||
|
"$wants_dir/omarchy-migrate-notify.service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Drop the retired enablement by hand instead of through `systemctl disable`.
|
||||||
|
# The package ships omarchy-update-user-notify.service as a compatibility
|
||||||
|
# symlink onto the new unit, for users who have not reached this migration yet,
|
||||||
|
# so disabling that name here would disable the replacement along with it.
|
||||||
|
rm -f "$wants_dir/omarchy-update-user-notify.path" \
|
||||||
|
"$wants_dir/omarchy-update-user-notify.service"
|
||||||
|
|
||||||
|
systemctl --user reset-failed omarchy-update-user-notify.path >/dev/null 2>&1 || true
|
||||||
|
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
echo "Save incoming Taildrop files to ~/Downloads"
|
||||||
|
|
||||||
|
if omarchy-cmd-present tailscale; then
|
||||||
|
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Report what systemctl actually said; "could not enable" on its own gives
|
||||||
|
# nothing to act on.
|
||||||
|
if ! error=$(systemctl --user enable --now omarchy-tailscale-receive.service 2>&1); then
|
||||||
|
echo "Could not enable omarchy-tailscale-receive.service: $error"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -309,6 +309,9 @@ function searchScore(items, entry, query) {
|
|||||||
else if (descriptionTextMatches(needle, descriptionText)) score = 60
|
else if (descriptionTextMatches(needle, descriptionText)) score = 60
|
||||||
|
|
||||||
if (entry.kind === "menu" || entry.kind === "link") score -= 2
|
if (entry.kind === "menu" || entry.kind === "link") score -= 2
|
||||||
|
// App rows sort after all menu items, so they lose the tiebreak below to an
|
||||||
|
// equal match. Outrank those, but stay inside the tier so better ones win.
|
||||||
|
if (entry.kind === "app") score -= 5
|
||||||
|
|
||||||
return score * 1000 + depthFor(items, entry.id) * 25 + entry.order
|
return score * 1000 + depthFor(items, entry.id) * 25 + entry.order
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,10 +73,35 @@ function loginPlan(needsLogin, authUrl) {
|
|||||||
return { authUrl: "", command: ["tailscale", "up"] }
|
return { authUrl: "", command: ["tailscale", "up"] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Taildrop is a tailnet feature the admin can turn off, so the button for it
|
||||||
|
// only makes sense when this profile actually carries the capability.
|
||||||
|
function hasFileSharing(self) {
|
||||||
|
var capability = "https://tailscale.com/cap/file-sharing"
|
||||||
|
var capMap = (self && self.CapMap) || null
|
||||||
|
if (capMap && capMap[capability] !== undefined) return true
|
||||||
|
var capabilities = (self && self.Capabilities) || []
|
||||||
|
for (var i = 0; i < capabilities.length; i++) {
|
||||||
|
if (String(capabilities[i]) === capability) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tailscale grades every peer itself — offline, wrong owner, an OS without
|
||||||
|
// Taildrop, no peer API — so take its word when the status carries one, and
|
||||||
|
// fall back to same-owner for daemons too old to say.
|
||||||
|
function isTaildropTarget(peer, selfUserId) {
|
||||||
|
var target = peer && peer.TaildropTarget
|
||||||
|
if (typeof target === "number" && target !== 0) return target === 1
|
||||||
|
var owner = String((peer && peer.UserID) || "")
|
||||||
|
return owner !== "" && owner === String(selfUserId || "")
|
||||||
|
}
|
||||||
|
|
||||||
function peerFromStatus(id, peer) {
|
function peerFromStatus(id, peer) {
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
HostName: displayHostName(peer.HostName, peer.DNSName),
|
HostName: displayHostName(peer.HostName, peer.DNSName),
|
||||||
|
UserID: String(peer.UserID || ""),
|
||||||
|
TaildropTarget: typeof peer.TaildropTarget === "number" ? peer.TaildropTarget : 0,
|
||||||
DNSName: cleanDnsName(peer.DNSName),
|
DNSName: cleanDnsName(peer.DNSName),
|
||||||
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
||||||
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
||||||
@@ -235,6 +260,8 @@ function parseStatus(raw) {
|
|||||||
selfName: displayHostName(self.HostName, self.DNSName),
|
selfName: displayHostName(self.HostName, self.DNSName),
|
||||||
selfDnsName: cleanDnsName(self.DNSName),
|
selfDnsName: cleanDnsName(self.DNSName),
|
||||||
selfIp: selfIps.length > 0 ? selfIps[0] : "",
|
selfIp: selfIps.length > 0 ? selfIps[0] : "",
|
||||||
|
selfUserId: String(self.UserID || ""),
|
||||||
|
fileSharing: hasFileSharing(self),
|
||||||
peers: peers,
|
peers: peers,
|
||||||
exitNodes: exitNodes
|
exitNodes: exitNodes
|
||||||
}
|
}
|
||||||
@@ -285,6 +312,8 @@ if (typeof module !== "undefined") {
|
|||||||
osIcon: osIcon,
|
osIcon: osIcon,
|
||||||
accountLabel: accountLabel,
|
accountLabel: accountLabel,
|
||||||
loginPlan: loginPlan,
|
loginPlan: loginPlan,
|
||||||
|
hasFileSharing: hasFileSharing,
|
||||||
|
isTaildropTarget: isTaildropTarget,
|
||||||
isMullvadPeer: isMullvadPeer,
|
isMullvadPeer: isMullvadPeer,
|
||||||
peerFromStatus: peerFromStatus,
|
peerFromStatus: peerFromStatus,
|
||||||
parseExitNodeList: parseExitNodeList,
|
parseExitNodeList: parseExitNodeList,
|
||||||
|
|||||||
@@ -295,6 +295,13 @@ Panel {
|
|||||||
scrollCursorIntoView()
|
scrollCursorIntoView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The file picker takes over from here, so get the panel out of the way.
|
||||||
|
function sendPeerFile(peer) {
|
||||||
|
if (!tailscale.canSendFiles(peer)) return
|
||||||
|
tailscale.sendFile(peer)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
function openSelectedPeerCopyMenu() {
|
function openSelectedPeerCopyMenu() {
|
||||||
if (!peerColumn || peerIndex < 0 || peerIndex >= peerColumn.children.length) return
|
if (!peerColumn || peerIndex < 0 || peerIndex >= peerColumn.children.length) return
|
||||||
var item = peerColumn.children[peerIndex]
|
var item = peerColumn.children[peerIndex]
|
||||||
@@ -416,6 +423,7 @@ Panel {
|
|||||||
else if (t === "c" || t === "C") tailscale.copyPeerIp(root.selectedPeer())
|
else if (t === "c" || t === "C") tailscale.copyPeerIp(root.selectedPeer())
|
||||||
else if (t === "n" || t === "N") tailscale.copyPeerName(root.selectedPeer())
|
else if (t === "n" || t === "N") tailscale.copyPeerName(root.selectedPeer())
|
||||||
else if (t === "d" || t === "D") tailscale.copyPeerDnsName(root.selectedPeer())
|
else if (t === "d" || t === "D") tailscale.copyPeerDnsName(root.selectedPeer())
|
||||||
|
else if (t === "s" || t === "S") root.sendPeerFile(root.selectedPeer())
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
Flickable {
|
||||||
@@ -973,6 +981,17 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PanelActionButton {
|
||||||
|
id: sendButton
|
||||||
|
visible: tailscale.canSendFiles(peerRow.peer)
|
||||||
|
iconText: ""
|
||||||
|
tooltipText: "Send files"
|
||||||
|
foreground: root.foreground
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onClicked: root.sendPeerFile(peerRow.peer)
|
||||||
|
}
|
||||||
|
|
||||||
PanelActionButton {
|
PanelActionButton {
|
||||||
id: copyButton
|
id: copyButton
|
||||||
iconText: ""
|
iconText: ""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Native Omarchy bar widget for Tailscale.
|
|||||||
- Switch between available Tailscale connections when multiple are available
|
- Switch between available Tailscale connections when multiple are available
|
||||||
- Browse machines from `tailscale status --json`
|
- Browse machines from `tailscale status --json`
|
||||||
- Copy a machine's Tailscale IP, host name, or DNS name
|
- Copy a machine's Tailscale IP, host name, or DNS name
|
||||||
|
- Send files to a machine with Taildrop, when the tailnet allows file sharing
|
||||||
|
|
||||||
## Keyboard shortcuts
|
## Keyboard shortcuts
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ Inside the panel:
|
|||||||
- `c`: copy selected peer IP
|
- `c`: copy selected peer IP
|
||||||
- `n`: copy selected peer name
|
- `n`: copy selected peer name
|
||||||
- `d`: copy selected peer DNS name
|
- `d`: copy selected peer DNS name
|
||||||
|
- `s`: send files to selected peer
|
||||||
- `t`: toggle Tailscale
|
- `t`: toggle Tailscale
|
||||||
- `r`: refresh status
|
- `r`: refresh status
|
||||||
- `esc`: close
|
- `esc`: close
|
||||||
@@ -28,6 +30,15 @@ Inside the panel:
|
|||||||
|
|
||||||
- `tailscale` CLI on `PATH`
|
- `tailscale` CLI on `PATH`
|
||||||
- `wl-copy` for clipboard copy actions
|
- `wl-copy` for clipboard copy actions
|
||||||
|
- Taildrop enabled for the tailnet, to send files
|
||||||
|
|
||||||
|
## Receiving files
|
||||||
|
|
||||||
|
Incoming Taildrop files are saved to `~/Downloads` by the
|
||||||
|
`omarchy-tailscale-receive` service, which announces each one with a
|
||||||
|
notification (an image preview when the file is an image, and a click to open
|
||||||
|
it). The Tailscale service install enables it; `omarchy tailscale receive`
|
||||||
|
runs the same loop by hand.
|
||||||
|
|
||||||
## Icon
|
## Icon
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ Item {
|
|||||||
property string selfName: ""
|
property string selfName: ""
|
||||||
property string selfDnsName: ""
|
property string selfDnsName: ""
|
||||||
property string selfIp: ""
|
property string selfIp: ""
|
||||||
|
property string selfUserId: ""
|
||||||
|
property bool fileSharing: false
|
||||||
property string authUrl: ""
|
property string authUrl: ""
|
||||||
property var peers: []
|
property var peers: []
|
||||||
property var exitNodes: []
|
property var exitNodes: []
|
||||||
@@ -123,6 +125,26 @@ Item {
|
|||||||
copyToClipboard(cleanDnsName(peer.DNSName), displayHostName(peer.HostName, peer.DNSName) + " DNS name")
|
copyToClipboard(cleanDnsName(peer.DNSName), displayHostName(peer.HostName, peer.DNSName) + " DNS name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peerAddress(peer) {
|
||||||
|
if (!peer) return ""
|
||||||
|
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
||||||
|
if (peer.HostName) return String(peer.HostName)
|
||||||
|
var ips = filterIPv4(peer.TailscaleIPs || [])
|
||||||
|
return ips.length > 0 ? ips[0] : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function canSendFiles(peer) {
|
||||||
|
if (!fileSharing || !running || !peer) return false
|
||||||
|
return Model.isTaildropTarget(peer, selfUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendFile(peer) {
|
||||||
|
if (!canSendFiles(peer)) return
|
||||||
|
var target = peerAddress(peer)
|
||||||
|
if (target === "") return
|
||||||
|
Quickshell.execDetached(["omarchy-tailscale-send", target])
|
||||||
|
}
|
||||||
|
|
||||||
function refresh(forceAccounts) {
|
function refresh(forceAccounts) {
|
||||||
if (installed) {
|
if (installed) {
|
||||||
refreshStatusAndAccounts(forceAccounts === true)
|
refreshStatusAndAccounts(forceAccounts === true)
|
||||||
@@ -137,18 +159,21 @@ Item {
|
|||||||
|
|
||||||
function refreshStatusAndAccounts(forceAccounts) {
|
function refreshStatusAndAccounts(forceAccounts) {
|
||||||
if (!installed) return
|
if (!installed) return
|
||||||
|
var launched = false
|
||||||
if (!statusProcess.running) {
|
if (!statusProcess.running) {
|
||||||
_statusOutput = ""
|
_statusOutput = ""
|
||||||
_statusError = ""
|
_statusError = ""
|
||||||
refreshing = true
|
refreshing = true
|
||||||
statusProcess.command = ["tailscale", "status", "--json"]
|
statusProcess.command = ["tailscale", "status", "--json"]
|
||||||
statusProcess.running = true
|
statusProcess.running = true
|
||||||
|
launched = true
|
||||||
}
|
}
|
||||||
if (!mullvadExitNodesProcess.running) {
|
if (!mullvadExitNodesProcess.running) {
|
||||||
_mullvadExitNodesOutput = ""
|
_mullvadExitNodesOutput = ""
|
||||||
_mullvadExitNodesError = ""
|
_mullvadExitNodesError = ""
|
||||||
mullvadExitNodesProcess.command = ["tailscale", "exit-node", "list"]
|
mullvadExitNodesProcess.command = ["tailscale", "exit-node", "list"]
|
||||||
mullvadExitNodesProcess.running = true
|
mullvadExitNodesProcess.running = true
|
||||||
|
launched = true
|
||||||
}
|
}
|
||||||
var now = Date.now()
|
var now = Date.now()
|
||||||
var shouldRefreshAccounts = forceAccounts === true || accounts.length === 0 || now - _lastAccountsRefreshMs > 60000
|
var shouldRefreshAccounts = forceAccounts === true || accounts.length === 0 || now - _lastAccountsRefreshMs > 60000
|
||||||
@@ -158,7 +183,13 @@ Item {
|
|||||||
_lastAccountsRefreshMs = now
|
_lastAccountsRefreshMs = now
|
||||||
accountsProcess.command = ["tailscale", "switch", "--list", "--json"]
|
accountsProcess.command = ["tailscale", "switch", "--list", "--json"]
|
||||||
accountsProcess.running = true
|
accountsProcess.running = true
|
||||||
|
launched = true
|
||||||
}
|
}
|
||||||
|
// Arm on the launch that needs watching and leave it alone after that.
|
||||||
|
// Restarting it every refresh pushes the deadline out ahead of a hung
|
||||||
|
// process forever once the refresh interval is shorter than the timeout,
|
||||||
|
// and refreshIntervalSec goes down to five seconds.
|
||||||
|
if (launched && !pollWatchdog.running) pollWatchdog.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
function elideStatus(text) {
|
function elideStatus(text) {
|
||||||
@@ -175,6 +206,8 @@ Item {
|
|||||||
selfName = ""
|
selfName = ""
|
||||||
selfDnsName = ""
|
selfDnsName = ""
|
||||||
selfIp = ""
|
selfIp = ""
|
||||||
|
selfUserId = ""
|
||||||
|
fileSharing = false
|
||||||
authUrl = ""
|
authUrl = ""
|
||||||
peers = []
|
peers = []
|
||||||
exitNodes = []
|
exitNodes = []
|
||||||
@@ -212,6 +245,8 @@ Item {
|
|||||||
selfName = parsed.selfName
|
selfName = parsed.selfName
|
||||||
selfDnsName = parsed.selfDnsName
|
selfDnsName = parsed.selfDnsName
|
||||||
selfIp = parsed.selfIp
|
selfIp = parsed.selfIp
|
||||||
|
selfUserId = parsed.selfUserId
|
||||||
|
fileSharing = parsed.fileSharing
|
||||||
peers = parsed.running ? parsed.peers : []
|
peers = parsed.running ? parsed.peers : []
|
||||||
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
||||||
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadRegions) : []
|
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||||
@@ -295,10 +330,7 @@ Item {
|
|||||||
var mullvadIps = filterIPv4(peer.TailscaleIPs || [])
|
var mullvadIps = filterIPv4(peer.TailscaleIPs || [])
|
||||||
if (mullvadIps.length > 0) return mullvadIps[0]
|
if (mullvadIps.length > 0) return mullvadIps[0]
|
||||||
}
|
}
|
||||||
if (peer.DNSName) return cleanDnsName(peer.DNSName)
|
return peerAddress(peer)
|
||||||
if (peer.HostName) return String(peer.HostName)
|
|
||||||
var ips = filterIPv4(peer.TailscaleIPs || [])
|
|
||||||
return ips.length > 0 ? ips[0] : ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExitNode(peer) {
|
function setExitNode(peer) {
|
||||||
@@ -386,6 +418,22 @@ Item {
|
|||||||
onTriggered: root.refresh()
|
onTriggered: root.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
// Every poll is skipped while its own process is still running, so one that
|
||||||
|
// never exits — tailscale can hang on a network that is coming and going —
|
||||||
|
// silently stops the panel refreshing at all, and it stays stopped. Reap
|
||||||
|
// anything still running well inside the refresh interval so the next tick
|
||||||
|
// starts clean.
|
||||||
|
id: pollWatchdog
|
||||||
|
interval: 15000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (statusProcess.running) statusProcess.running = false
|
||||||
|
if (mullvadExitNodesProcess.running) mullvadExitNodesProcess.running = false
|
||||||
|
if (accountsProcess.running) accountsProcess.running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: actionStatusTimer
|
id: actionStatusTimer
|
||||||
interval: 2200
|
interval: 2200
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ pass "default shell.json is valid JSON"
|
|||||||
jq -e '.version == 1 and (.bar.layout.left | type == "array") and (.bar.layout.center | type == "array") and (.bar.layout.right | type == "array")' "$ROOT/config/omarchy/shell.json" >/dev/null
|
jq -e '.version == 1 and (.bar.layout.left | type == "array") and (.bar.layout.center | type == "array") and (.bar.layout.right | type == "array")' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||||
pass "default shell.json has versioned bar layout"
|
pass "default shell.json has versioned bar layout"
|
||||||
|
|
||||||
|
# Pinning the whole row made this fail every time an unrelated widget moved,
|
||||||
|
# so assert the adjacency the name is about and let the rest of the row change.
|
||||||
jq -e '
|
jq -e '
|
||||||
def ids: map(.id // .);
|
def ids: map(.id // .);
|
||||||
.bar.layout.center | ids == [
|
(.bar.layout.center | ids) as $ids |
|
||||||
"omarchy.indicators",
|
($ids | index("omarchy.weather")) as $weather |
|
||||||
"omarchy.clock",
|
($ids | index("omarchy.system-update")) as $update |
|
||||||
"omarchy.weather",
|
$weather != null and $update == $weather + 1
|
||||||
"omarchy.system-update"
|
|
||||||
]
|
|
||||||
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||||
pass "default center layout keeps update next to weather"
|
pass "default center layout keeps update next to weather"
|
||||||
|
|
||||||
@@ -94,12 +94,29 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
root = Path(os.environ["ROOT"])
|
root = Path(os.environ["ROOT"])
|
||||||
|
home = Path.home()
|
||||||
pkgs_candidates = [
|
pkgs_candidates = [
|
||||||
root.parent / "omarchy-pkgs/pkgbuilds",
|
root.parent / "omarchy-pkgs/pkgbuilds",
|
||||||
root.parent / "omarchy/omarchy-pkgs/pkgbuilds",
|
root.parent / "omarchy/omarchy-pkgs/pkgbuilds",
|
||||||
root.parent.parent / "omarchy-pkgs/pkgbuilds",
|
root.parent.parent / "omarchy-pkgs/pkgbuilds",
|
||||||
|
root.parent / "omacom/omarchy-pkgs/pkgbuilds",
|
||||||
|
root.parent.parent / "omacom/omarchy-pkgs/pkgbuilds",
|
||||||
|
home / "Work/omacom/omarchy-pkgs/pkgbuilds",
|
||||||
]
|
]
|
||||||
pkgs_root = next((path for path in pkgs_candidates if path.exists()), pkgs_candidates[0])
|
# Checkouts differ per machine, so allow an explicit pointer at the sibling repo.
|
||||||
|
# Accepts either the omarchy-pkgs checkout or its pkgbuilds/ directory.
|
||||||
|
override = os.environ.get("OMARCHY_PKGS_PATH")
|
||||||
|
if override:
|
||||||
|
pkgs_candidates = [Path(override) / "pkgbuilds", Path(override)] + pkgs_candidates
|
||||||
|
pkgs_root = next((path for path in pkgs_candidates if path.exists()), None)
|
||||||
|
if pkgs_root is None:
|
||||||
|
print("not ok - omarchy-pkgs checkout found for PKGBUILD coverage", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
"looked in:\n " + "\n ".join(str(path) for path in pkgs_candidates) +
|
||||||
|
"\nset OMARCHY_PKGS_PATH to the omarchy-pkgs checkout",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
settings_pkgbuild_path = pkgs_root / "omarchy-settings/PKGBUILD"
|
settings_pkgbuild_path = pkgs_root / "omarchy-settings/PKGBUILD"
|
||||||
omarchy_pkgbuild_path = pkgs_root / "omarchy/PKGBUILD"
|
omarchy_pkgbuild_path = pkgs_root / "omarchy/PKGBUILD"
|
||||||
if not settings_pkgbuild_path.exists():
|
if not settings_pkgbuild_path.exists():
|
||||||
@@ -120,8 +137,8 @@ package_defaults = [
|
|||||||
("default/systemd/user/bt-agent.service", "/usr/lib/systemd/user/bt-agent.service", "systemd/user/bt-agent.service"),
|
("default/systemd/user/bt-agent.service", "/usr/lib/systemd/user/bt-agent.service", "systemd/user/bt-agent.service"),
|
||||||
("default/systemd/user/omarchy-sleep-lock.service", "/usr/lib/systemd/user/omarchy-sleep-lock.service", "systemd/user/omarchy-sleep-lock.service"),
|
("default/systemd/user/omarchy-sleep-lock.service", "/usr/lib/systemd/user/omarchy-sleep-lock.service", "systemd/user/omarchy-sleep-lock.service"),
|
||||||
("default/systemd/user/omarchy-recover-internal-monitor.service", "/usr/lib/systemd/user/omarchy-recover-internal-monitor.service", "systemd/user/omarchy-recover-internal-monitor.service"),
|
("default/systemd/user/omarchy-recover-internal-monitor.service", "/usr/lib/systemd/user/omarchy-recover-internal-monitor.service", "systemd/user/omarchy-recover-internal-monitor.service"),
|
||||||
("default/systemd/user/omarchy-update-user-notify.service", "/usr/lib/systemd/user/omarchy-update-user-notify.service", "systemd/user/omarchy-update-user-notify.service"),
|
("default/systemd/user/omarchy-migrate-notify.service", "/usr/lib/systemd/user/omarchy-migrate-notify.service", "systemd/user/omarchy-migrate-notify.service"),
|
||||||
("default/systemd/user/omarchy-update-user-notify.path", "/usr/lib/systemd/user/omarchy-update-user-notify.path", "systemd/user/omarchy-update-user-notify.path"),
|
("default/systemd/user/omarchy-tailscale-receive.service", "/usr/lib/systemd/user/omarchy-tailscale-receive.service", "systemd/user/omarchy-tailscale-receive.service"),
|
||||||
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
|
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
|
||||||
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
|
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
|
||||||
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
|
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
|
||||||
@@ -135,6 +152,16 @@ for source, destination, legacy in package_defaults:
|
|||||||
if destination and (source not in pkgbuild or destination not in pkgbuild):
|
if destination and (source not in pkgbuild or destination not in pkgbuild):
|
||||||
errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}")
|
errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}")
|
||||||
|
|
||||||
|
# Existing users have an absolute wants symlink to the old unit path, and the
|
||||||
|
# migration that repoints it only runs for users who run an update -- the
|
||||||
|
# opposite of who the notifier is for. Dropping this alias strands them.
|
||||||
|
notify_alias = 'ln -sfn omarchy-migrate-notify.service "$pkgdir/usr/lib/systemd/user/omarchy-update-user-notify.service"'
|
||||||
|
if notify_alias not in pkgbuild:
|
||||||
|
errors.append(
|
||||||
|
"PKGBUILD does not ship the omarchy-update-user-notify.service compatibility "
|
||||||
|
"alias, so users who have not run migration 1785095882 lose the login notifier"
|
||||||
|
)
|
||||||
|
|
||||||
alpm_hooks = [
|
alpm_hooks = [
|
||||||
"00-omarchy-update-guard.hook",
|
"00-omarchy-update-guard.hook",
|
||||||
"10-omarchy-hyprland-reload-pause.hook",
|
"10-omarchy-hyprland-reload-pause.hook",
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||||
|
|
||||||
|
require_command lua
|
||||||
|
|
||||||
|
resolved_input() {
|
||||||
|
OMARCHY_PATH="$ROOT" OMARCHY_VCONSOLE="${1-}" lua <<'LUA'
|
||||||
|
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||||
|
|
||||||
|
local vconsole = os.getenv("OMARCHY_VCONSOLE")
|
||||||
|
local real_open = io.open
|
||||||
|
|
||||||
|
io.open = function(path, mode)
|
||||||
|
if path ~= "/etc/vconsole.conf" then
|
||||||
|
return real_open(path, mode)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not vconsole then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = io.tmpfile()
|
||||||
|
file:write(vconsole)
|
||||||
|
file:seek("set")
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
|
||||||
|
hl = {
|
||||||
|
config = function(config)
|
||||||
|
local input = config.input
|
||||||
|
print(("[%s] [%s] [%s]"):format(input.kb_layout, input.kb_variant, input.kb_options))
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
o = { window = function() end }
|
||||||
|
|
||||||
|
require("default.hypr.input")
|
||||||
|
LUA
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_input() {
|
||||||
|
local description="$1"
|
||||||
|
local expected="$2"
|
||||||
|
local actual
|
||||||
|
|
||||||
|
if (( $# > 2 )); then
|
||||||
|
actual=$(resolved_input "$3")
|
||||||
|
else
|
||||||
|
actual=$(resolved_input)
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $actual == "$expected" ]] ||
|
||||||
|
fail "$description" "expected: $expected"$'\n'"actual: $actual"
|
||||||
|
pass "$description"
|
||||||
|
}
|
||||||
|
|
||||||
|
base_options="compose:caps,shift:both_capslock"
|
||||||
|
toggle_options="$base_options,grp:alts_toggle"
|
||||||
|
|
||||||
|
assert_input "missing vconsole.conf falls back to us" "[us] [] [$base_options]"
|
||||||
|
assert_input "us layout passes through" "[us] [intl] [$base_options]" 'XKBLAYOUT=us
|
||||||
|
XKBVARIANT=intl
|
||||||
|
'
|
||||||
|
assert_input "latin layouts are left alone" "[de] [nodeadkeys] [$base_options]" 'XKBLAYOUT=de
|
||||||
|
XKBVARIANT=nodeadkeys
|
||||||
|
'
|
||||||
|
assert_input "non-latin layout gains us in front" "[us,ara] [,] [$toggle_options]" 'XKBLAYOUT=ara
|
||||||
|
'
|
||||||
|
assert_input "prepended us keeps variants aligned" "[us,ru] [,phonetic] [$toggle_options]" 'XKBLAYOUT=ru
|
||||||
|
XKBVARIANT=phonetic
|
||||||
|
'
|
||||||
|
assert_input "non-latin layout in front gains us even when us trails" "[us,il,us] [,] [$toggle_options]" 'XKBLAYOUT=il,us
|
||||||
|
'
|
||||||
|
|
||||||
|
hooks_conf="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
|
||||||
|
input_lua="$ROOT/default/hypr/input.lua"
|
||||||
|
|
||||||
|
hooks_layouts=$(awk -F')' '/\) ;;$/ { gsub(/[[:space:]|]+/, "\n", $1); print $1 }' "$hooks_conf" | grep '^[a-z]\+$' | sort)
|
||||||
|
lua_layouts=$(sed -n '/^local non_latin_layouts =/,+1p' "$input_lua" | grep -o '"[^"]*"' | tr -d '"' | tr ' ' '\n' | grep '^[a-z]\+$' | sort)
|
||||||
|
|
||||||
|
[[ -n $hooks_layouts ]] || fail "non-latin layout list is readable from omarchy_hooks.conf"
|
||||||
|
[[ $hooks_layouts == "$lua_layouts" ]] ||
|
||||||
|
fail "non-latin layout lists stay in sync" "$(diff <(echo "$hooks_layouts") <(echo "$lua_layouts"))"
|
||||||
|
pass "non-latin layout lists stay in sync with the initramfs hook"
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
config_script="$ROOT/install/config/locate.sh"
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
stock_conf() {
|
||||||
|
cat >"$1" <<'CONF'
|
||||||
|
PRUNE_BIND_MOUNTS = "yes"
|
||||||
|
PRUNEFS = "9p afs autofs cifs fuse nfs nfs4 proc sysfs tmpfs"
|
||||||
|
PRUNENAMES = ".git .hg .svn"
|
||||||
|
PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache /var/lib/pacman/local /var/lock /var/run /var/spool /var/tmp"
|
||||||
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
|
# updatedb dies on a config that defines a variable twice, so hand every
|
||||||
|
# rewritten file to the real parser rather than trusting the greps below.
|
||||||
|
empty_tree="$test_tmp/empty-tree"
|
||||||
|
mkdir -p "$empty_tree"
|
||||||
|
|
||||||
|
assert_conf_parses() {
|
||||||
|
command -v updatedb >/dev/null || return 0
|
||||||
|
|
||||||
|
local errors
|
||||||
|
errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true)
|
||||||
|
[[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors"
|
||||||
|
}
|
||||||
|
|
||||||
|
conf="$test_tmp/updatedb.conf"
|
||||||
|
stock_conf "$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home"
|
||||||
|
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config leaves an already-configured file alone"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
pass "locate config tolerates a missing updatedb.conf"
|
||||||
|
|
||||||
|
# A hand-edited updatedb.conf may drop the settings entirely, or write them
|
||||||
|
# without the spaces around the "=" or the quotes that the stock Arch file uses.
|
||||||
|
conf="$test_tmp/sparse-updatedb.conf"
|
||||||
|
printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS"
|
||||||
|
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config adds settings a hand-edited updatedb.conf is missing"
|
||||||
|
|
||||||
|
conf="$test_tmp/unspaced-updatedb.conf"
|
||||||
|
printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS"
|
||||||
|
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS"
|
||||||
|
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config keeps a single PRUNEPATHS setting"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config handles updatedb.conf written without spaces around ="
|
||||||
|
|
||||||
|
# updatedb allows a comment after a value and indented settings, and defining
|
||||||
|
# either setting twice makes it refuse to run at all.
|
||||||
|
conf="$test_tmp/commented-updatedb.conf"
|
||||||
|
printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \
|
||||||
|
'PRUNEPATHS = "/tmp" # scratch' >"$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS"
|
||||||
|
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes"
|
||||||
|
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces a commented PRUNEPATHS instead of adding a second one"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config handles indented settings and trailing comments"
|
||||||
|
|
||||||
|
# A hand-edited file may have dropped the quotes updatedb requires, which
|
||||||
|
# leaves it unparseable until something writes the setting out properly.
|
||||||
|
conf="$test_tmp/unquoted-updatedb.conf"
|
||||||
|
printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS"
|
||||||
|
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config handles updatedb.conf written without quotes"
|
||||||
|
|
||||||
|
# A path that merely ends in /.snapshots is not the root snapshot directory.
|
||||||
|
conf="$test_tmp/nested-snapshots-updatedb.conf"
|
||||||
|
printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf"
|
||||||
|
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it"
|
||||||
|
assert_conf_parses "$conf"
|
||||||
|
pass "locate config tells /.snapshots apart from a path that ends in it"
|
||||||
|
|
||||||
|
locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true)
|
||||||
|
[[ -n $locate_migration ]] || fail "locate migration exists"
|
||||||
|
|
||||||
|
fake_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$fake_bin"
|
||||||
|
|
||||||
|
cat >"$fake_bin/sudo" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
chmod +x "$fake_bin/sudo"
|
||||||
|
|
||||||
|
cat >"$fake_bin/systemctl" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'systemctl %s\n' "$*" >>"$TEST_LOG"
|
||||||
|
STUB
|
||||||
|
chmod +x "$fake_bin/systemctl"
|
||||||
|
|
||||||
|
conf="$test_tmp/migration-updatedb.conf"
|
||||||
|
stock_conf "$conf"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||||
|
bash -euo pipefail "$locate_migration" >/dev/null
|
||||||
|
|
||||||
|
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf"
|
||||||
|
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots"
|
||||||
|
grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking"
|
||||||
|
pass "locate migration fixes existing installs and rebuilds the index"
|
||||||
|
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||||
|
bash -euo pipefail "$locate_migration" >/dev/null
|
||||||
|
|
||||||
|
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs"
|
||||||
|
pass "locate migration is a no-op once updatedb.conf is configured"
|
||||||
|
|
||||||
|
# A dev checkout carries migrations from a release whose install scripts the
|
||||||
|
# checked-out tree may not have yet, and omarchy-migrate runs under set -e.
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
conf="$test_tmp/no-config-script-updatedb.conf"
|
||||||
|
stock_conf "$conf"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_PATH="$test_tmp/empty" \
|
||||||
|
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
|
||||||
|
bash -euo pipefail "$locate_migration" >/dev/null ||
|
||||||
|
fail "locate migration survives a tree without the locate config script"
|
||||||
|
|
||||||
|
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script"
|
||||||
|
pass "locate migration is a no-op when the locate config script is missing"
|
||||||
@@ -109,6 +109,25 @@ assertDeepEqual(
|
|||||||
|
|
||||||
const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc)
|
const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc)
|
||||||
const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item]))
|
const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item]))
|
||||||
|
|
||||||
|
// Needs the real menu: app rows sort after all menu items, and only at that
|
||||||
|
// item count does the order tiebreak alone bury an installed app.
|
||||||
|
const rankBase = menu.mergeMenuSources(defaultItems, [])
|
||||||
|
const ranked = menu.mergeAppRows(rankBase.items, rankBase.itemOrder, [
|
||||||
|
{ id: 'apps.brave', parent: 'apps', kind: 'app', label: 'Brave', description: '', aliases: [] },
|
||||||
|
{ id: 'apps.fontforge', parent: 'apps', kind: 'app', label: 'FontForge', description: '', aliases: [] }
|
||||||
|
])
|
||||||
|
const rankScore = (id, query) => menu.searchScore(ranked.items, ranked.items[id], query)
|
||||||
|
assert(
|
||||||
|
['install.browser.brave', 'remove.browser.brave', 'setup.default.browser.brave'].every(
|
||||||
|
id => rankScore('apps.brave', 'brave') < rankScore(id, 'brave')
|
||||||
|
),
|
||||||
|
'menu ranks an installed app above menu entries matching the query equally well'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
rankScore('style.font', 'font') < rankScore('apps.fontforge', 'font'),
|
||||||
|
'menu keeps a better-matching menu entry above a weaker app match'
|
||||||
|
)
|
||||||
const triggerItems = defaultItems.filter(item => item.parent === 'trigger')
|
const triggerItems = defaultItems.filter(item => item.parent === 'trigger')
|
||||||
assertEqual(
|
assertEqual(
|
||||||
triggerItems[0].id,
|
triggerItems[0].id,
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ bash -c "$command"
|
|||||||
SH
|
SH
|
||||||
chmod +x "$stub_bin/systemd-run"
|
chmod +x "$stub_bin/systemd-run"
|
||||||
|
|
||||||
|
cat >"$stub_bin/omarchy-notification-wait" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$stub_bin/omarchy-notification-wait"
|
||||||
|
|
||||||
cat >"$stub_bin/omarchy-notification-send" <<'SH'
|
cat >"$stub_bin/omarchy-notification-send" <<'SH'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
|
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ SH
|
|||||||
chmod +x "$stub_bin"/*
|
chmod +x "$stub_bin"/*
|
||||||
|
|
||||||
export PATH="$stub_bin:$ROOT/bin:$PATH"
|
export PATH="$stub_bin:$ROOT/bin:$PATH"
|
||||||
|
# The resize helper anchors to a region file here, so keep it out of the real one
|
||||||
|
export XDG_RUNTIME_DIR="$tmp_dir"
|
||||||
export OMARCHY_TEST_MENU_ARGS="$tmp_dir/menu-args"
|
export OMARCHY_TEST_MENU_ARGS="$tmp_dir/menu-args"
|
||||||
export OMARCHY_TEST_RECORDER_ARGS="$tmp_dir/recorder-args"
|
export OMARCHY_TEST_RECORDER_ARGS="$tmp_dir/recorder-args"
|
||||||
export OMARCHY_TEST_NOTIFICATION_ARGS="$tmp_dir/notification-args"
|
export OMARCHY_TEST_NOTIFICATION_ARGS="$tmp_dir/notification-args"
|
||||||
@@ -153,6 +155,59 @@ if [[ -s $OMARCHY_TEST_HYPRCTL_ARGS ]]; then
|
|||||||
fi
|
fi
|
||||||
pass "webcam resize ignores other windows"
|
pass "webcam resize ignores other windows"
|
||||||
|
|
||||||
|
region_file="$XDG_RUNTIME_DIR/omarchy-screenrecord-region"
|
||||||
|
|
||||||
|
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||||
|
echo "800x600+100+100" >"$region_file"
|
||||||
|
"$ROOT/bin/omarchy-capture-webcam-resize" reset
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 133, y = 150 })' \
|
||||||
|
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 727, y = 510 })' >"$expected_hyprctl_args"
|
||||||
|
|
||||||
|
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||||
|
fail "webcam anchors to the recorded region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||||
|
fi
|
||||||
|
pass "webcam anchors to the recorded region"
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 178, y = 200 })' \
|
||||||
|
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 2342, y = 460 })' >"$expected_hyprctl_args"
|
||||||
|
|
||||||
|
for region in "not-a-region" ""; do
|
||||||
|
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||||
|
printf '%s' "$region" >"$region_file"
|
||||||
|
"$ROOT/bin/omarchy-capture-webcam-resize" reset
|
||||||
|
|
||||||
|
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||||
|
fail "webcam falls back to the monitor for an unusable region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
pass "webcam falls back to the monitor for an unusable region"
|
||||||
|
|
||||||
|
# A region too narrow for presets scaled from its height shrinks the whole
|
||||||
|
# ladder, so the three sizes stay distinct and each one fits inside the margins
|
||||||
|
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||||
|
echo "200x1200+0+0" >"$region_file"
|
||||||
|
for size in small medium large; do
|
||||||
|
"$ROOT/bin/omarchy-capture-webcam-resize" "$size"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 64, y = 72 })' \
|
||||||
|
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 96, y = 1088 })' \
|
||||||
|
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 89, y = 100 })' \
|
||||||
|
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 71, y = 1060 })' \
|
||||||
|
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 120, y = 135 })' \
|
||||||
|
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 40, y = 1025 })' >"$expected_hyprctl_args"
|
||||||
|
|
||||||
|
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||||
|
fail "webcam sizes stay distinct and inside a narrow region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||||
|
fi
|
||||||
|
pass "webcam sizes stay distinct and inside a narrow region"
|
||||||
|
|
||||||
|
rm -f "$region_file"
|
||||||
|
|
||||||
grep -F 'o.bind("SUPER + ALT + code:34", "Make webcam overlay smaller", "omarchy-capture-webcam-resize smaller")' \
|
grep -F 'o.bind("SUPER + ALT + code:34", "Make webcam overlay smaller", "omarchy-capture-webcam-resize smaller")' \
|
||||||
"$ROOT/default/hypr/bindings/utilities.lua" >/dev/null || fail "webcam smaller hotkey is configured"
|
"$ROOT/default/hypr/bindings/utilities.lua" >/dev/null || fail "webcam smaller hotkey is configured"
|
||||||
grep -F 'o.bind("SUPER + ALT + code:35", "Make webcam overlay larger", "omarchy-capture-webcam-resize larger")' \
|
grep -F 'o.bind("SUPER + ALT + code:35", "Make webcam overlay larger", "omarchy-capture-webcam-resize larger")' \
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ grep -Fx 'systemctl enable --now snapper-cleanup.timer limine-snapper-sync.servi
|
|||||||
pass "snapshot configure normalizes Snapper policy and services"
|
pass "snapshot configure normalizes Snapper policy and services"
|
||||||
|
|
||||||
setup_system="$ROOT/bin/omarchy-setup-system"
|
setup_system="$ROOT/bin/omarchy-setup-system"
|
||||||
grep -F 'config/snapper.sh' "$setup_system" >/dev/null
|
grep -F 'config/all.sh' "$setup_system" >/dev/null ||
|
||||||
|
fail "system setup runs the config phase"
|
||||||
|
grep -F 'config/snapper.sh' "$ROOT/install/config/all.sh" >/dev/null ||
|
||||||
|
fail "config phase normalizes Snapper"
|
||||||
pass "system setup normalizes Snapper during fresh installs"
|
pass "system setup normalizes Snapper during fresh installs"
|
||||||
|
|
||||||
migration=$(grep -rl 'Normalize Snapper snapshot services' "$ROOT/migrations" | head -n 1 || true)
|
migration=$(grep -rl 'Normalize Snapper snapshot services' "$ROOT/migrations" | head -n 1 || true)
|
||||||
@@ -84,12 +87,18 @@ grep -F 'as_root env OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$snapper_c
|
|||||||
! grep -F 'NUMBER_LIMIT="5"' "$migration" >/dev/null || fail "Snapper service migration does not overwrite working custom retention"
|
! grep -F 'NUMBER_LIMIT="5"' "$migration" >/dev/null || fail "Snapper service migration does not overwrite working custom retention"
|
||||||
pass "Snapper service migration only repairs broken services idempotently"
|
pass "Snapper service migration only repairs broken services idempotently"
|
||||||
|
|
||||||
|
# Checkouts differ per machine, so allow an explicit pointer at the sibling repo.
|
||||||
|
# Accepts either the omarchy-pkgs checkout or its pkgbuilds/ directory.
|
||||||
find_omarchy_pks_root() {
|
find_omarchy_pks_root() {
|
||||||
local candidate
|
local candidate
|
||||||
for candidate in \
|
for candidate in \
|
||||||
|
${OMARCHY_PKGS_PATH:+"$OMARCHY_PKGS_PATH/pkgbuilds" "$OMARCHY_PKGS_PATH"} \
|
||||||
"$ROOT/../omarchy-pkgs/pkgbuilds" \
|
"$ROOT/../omarchy-pkgs/pkgbuilds" \
|
||||||
"$ROOT/../omarchy/omarchy-pkgs/pkgbuilds" \
|
"$ROOT/../omarchy/omarchy-pkgs/pkgbuilds" \
|
||||||
"$ROOT/../../omarchy-pkgs/pkgbuilds"; do
|
"$ROOT/../../omarchy-pkgs/pkgbuilds" \
|
||||||
|
"$ROOT/../omacom/omarchy-pkgs/pkgbuilds" \
|
||||||
|
"$ROOT/../../omacom/omarchy-pkgs/pkgbuilds" \
|
||||||
|
"$HOME/Work/omacom/omarchy-pkgs/pkgbuilds"; do
|
||||||
if [[ -d $candidate ]]; then
|
if [[ -d $candidate ]]; then
|
||||||
cd "$candidate" && pwd
|
cd "$candidate" && pwd
|
||||||
return 0
|
return 0
|
||||||
@@ -111,12 +120,17 @@ grep -F 'cp -a install "$pkgdir/usr/share/omarchy/"' "$omarchy_pkgbuild" >/dev/n
|
|||||||
grep -F 'cp -a migrations "$pkgdir/usr/share/omarchy/"' "$omarchy_pkgbuild" >/dev/null || fail "omarchy package bundles migrations"
|
grep -F 'cp -a migrations "$pkgdir/usr/share/omarchy/"' "$omarchy_pkgbuild" >/dev/null || fail "omarchy package bundles migrations"
|
||||||
pass "omarchy-pkgs packages Snapper template, setup, and migration coverage"
|
pass "omarchy-pkgs packages Snapper template, setup, and migration coverage"
|
||||||
|
|
||||||
|
# Same per-machine checkout problem as omarchy-pkgs; OMARCHY_ISO_PATH points at it.
|
||||||
find_omarchy_iso_root() {
|
find_omarchy_iso_root() {
|
||||||
local candidate
|
local candidate
|
||||||
for candidate in \
|
for candidate in \
|
||||||
|
${OMARCHY_ISO_PATH:+"$OMARCHY_ISO_PATH"} \
|
||||||
"$ROOT/../omarchy-iso" \
|
"$ROOT/../omarchy-iso" \
|
||||||
"$ROOT/../omarchy/omarchy-iso" \
|
"$ROOT/../omarchy/omarchy-iso" \
|
||||||
"$ROOT/../../omarchy-iso"; do
|
"$ROOT/../../omarchy-iso" \
|
||||||
|
"$ROOT/../omacom/omarchy-iso" \
|
||||||
|
"$ROOT/../../omacom/omarchy-iso" \
|
||||||
|
"$HOME/Work/omacom/omarchy-iso"; do
|
||||||
if [[ -d $candidate ]]; then
|
if [[ -d $candidate ]]; then
|
||||||
cd "$candidate" && pwd
|
cd "$candidate" && pwd
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
leak_migration=$(grep -rl 'timeline snapshots leaked by earlier defaults' "$ROOT/migrations" | head -n 1 || true)
|
||||||
|
[[ -n $leak_migration ]] || fail "Snapper timeline leak migration exists"
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
fake_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$fake_bin"
|
||||||
|
|
||||||
|
cat >"$fake_bin/sudo" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'sudo %s\n' "$*" >>"$TEST_LOG"
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
chmod +x "$fake_bin/sudo"
|
||||||
|
|
||||||
|
cat >"$fake_bin/snapper" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'snapper %s\n' "$*" >>"$TEST_LOG"
|
||||||
|
if [[ "$*" == *"--csvout list"* ]]; then
|
||||||
|
echo "number,cleanup"
|
||||||
|
for i in $(seq 1 45); do
|
||||||
|
echo "$i,timeline"
|
||||||
|
done
|
||||||
|
echo "100,number"
|
||||||
|
echo "101,"
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
chmod +x "$fake_bin/snapper"
|
||||||
|
|
||||||
|
snapper_config="$test_tmp/root"
|
||||||
|
printf '%s\n' 'TIMELINE_CREATE="no"' 'NUMBER_CLEANUP="yes"' >"$snapper_config"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||||
|
bash -euo pipefail "$leak_migration" >/dev/null
|
||||||
|
|
||||||
|
deletes=$(grep -c '^snapper -c root delete ' "$test_tmp/calls.log" || true)
|
||||||
|
[[ $deletes -eq 3 ]] || fail "leak migration deletes snapshots in batches" "expected 3 delete calls, got $deletes"
|
||||||
|
|
||||||
|
first_batch=$(grep -m1 '^snapper -c root delete ' "$test_tmp/calls.log")
|
||||||
|
[[ $first_batch == "snapper -c root delete $(seq -s ' ' 1 20)" ]] || fail "leak migration caps delete batches at 20 snapshots" "$first_batch"
|
||||||
|
|
||||||
|
last_batch=$(grep '^snapper -c root delete ' "$test_tmp/calls.log" | tail -n 1)
|
||||||
|
[[ $last_batch == "snapper -c root delete $(seq -s ' ' 41 45)" ]] || fail "leak migration deletes the final partial batch" "$last_batch"
|
||||||
|
|
||||||
|
! grep -E '^snapper -c root delete .*\b(100|101)\b' "$test_tmp/calls.log" || fail "leak migration only deletes timeline snapshots"
|
||||||
|
pass "leak migration removes leaked timeline snapshots in batches and keeps the rest"
|
||||||
|
|
||||||
|
# omarchy-migrate runs under set -e, so a batch that dies on a DBus timeout
|
||||||
|
# would otherwise abort the run and skip every migration queued behind it.
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
printf '%s\n' 'TIMELINE_CREATE="no"' 'NUMBER_CLEANUP="yes"' >"$snapper_config"
|
||||||
|
|
||||||
|
cat >"$fake_bin/snapper" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'snapper %s\n' "$*" >>"$TEST_LOG"
|
||||||
|
if [[ "$*" == *"--csvout list"* ]]; then
|
||||||
|
echo "number,cleanup"
|
||||||
|
for i in $(seq 1 45); do
|
||||||
|
echo "$i,timeline"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "failure: dbus timeout" >&2
|
||||||
|
exit 1
|
||||||
|
STUB
|
||||||
|
|
||||||
|
output=$(TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||||
|
bash -euo pipefail "$leak_migration" 2>/dev/null) ||
|
||||||
|
fail "leak migration survives a failed delete batch"
|
||||||
|
|
||||||
|
deletes=$(grep -c '^snapper -c root delete ' "$test_tmp/calls.log" || true)
|
||||||
|
[[ $deletes -eq 3 ]] || fail "leak migration keeps draining after a failed batch" "expected 3 delete calls, got $deletes"
|
||||||
|
|
||||||
|
# omarchy-migrate writes the completion marker even when the drain gave up, so
|
||||||
|
# what is left has to be said out loud rather than left for a rerun.
|
||||||
|
grep -qF '45 snapshots could not be deleted' <<<"$output" || fail "leak migration reports the snapshots it could not delete" "$output"
|
||||||
|
pass "leak migration tolerates a batch that fails partway"
|
||||||
|
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
printf '%s\n' 'TIMELINE_CREATE="yes"' >"$snapper_config"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||||
|
bash -euo pipefail "$leak_migration" >/dev/null
|
||||||
|
|
||||||
|
[[ ! -s $test_tmp/calls.log ]] || fail "leak migration leaves deliberate timeline setups alone"
|
||||||
|
pass "leak migration skips systems where timeline snapshots are intentional"
|
||||||
|
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_SNAPPER_CONFIG_PATH="$test_tmp/missing" \
|
||||||
|
bash -euo pipefail "$leak_migration" >/dev/null
|
||||||
|
|
||||||
|
[[ ! -s $test_tmp/calls.log ]] || fail "leak migration skips systems without a Snapper root config"
|
||||||
|
pass "leak migration is a no-op without Snapper configured"
|
||||||
|
|
||||||
|
# Snapper's create-config writes a root-only config, and a config this user
|
||||||
|
# cannot read says nothing about whether timeline snapshots are wanted.
|
||||||
|
: >"$test_tmp/calls.log"
|
||||||
|
printf '%s\n' 'TIMELINE_CREATE="no"' >"$snapper_config"
|
||||||
|
chmod 000 "$snapper_config"
|
||||||
|
|
||||||
|
TEST_LOG="$test_tmp/calls.log" \
|
||||||
|
PATH="$fake_bin:$PATH" \
|
||||||
|
OMARCHY_SNAPPER_CONFIG_PATH="$snapper_config" \
|
||||||
|
bash -euo pipefail "$leak_migration" >/dev/null 2>&1
|
||||||
|
|
||||||
|
chmod 600 "$snapper_config"
|
||||||
|
|
||||||
|
grep -qF "sudo grep -qFx TIMELINE_CREATE=\"no\" $snapper_config" "$test_tmp/calls.log" ||
|
||||||
|
fail "leak migration reads a root-only Snapper config as root" "$(cat "$test_tmp/calls.log")"
|
||||||
|
pass "leak migration does not mistake an unreadable Snapper config for an intentional one"
|
||||||
@@ -28,12 +28,19 @@ grep -F 'ExecStart=/usr/bin/omarchy-system-sleep-monitor' "$upgrade_to_quattro"
|
|||||||
grep -F 'reset-failed omarchy-sleep-lock.service' "$upgrade_to_quattro" >/dev/null
|
grep -F 'reset-failed omarchy-sleep-lock.service' "$upgrade_to_quattro" >/dev/null
|
||||||
pass "Omarchy 4 upgrade repairs the legacy sleep lock unit path"
|
pass "Omarchy 4 upgrade repairs the legacy sleep lock unit path"
|
||||||
|
|
||||||
notify_path="$ROOT/default/systemd/user/omarchy-update-user-notify.path"
|
[[ -e $ROOT/default/systemd/user/omarchy-update-user-notify.path ]] &&
|
||||||
! grep -q 'PathExistsGlob' "$notify_path"
|
fail "the retired migration watcher is back; pacman writing the migration directory during omarchy update would notify about migrations that update is already applying"
|
||||||
grep -Fx 'PathModified=/usr/share/omarchy/migrations' "$notify_path" >/dev/null
|
grep -rlE '^(Path[A-Za-z]+|DirectoryNotEmpty)=.*/usr/share/omarchy/migrations' "$ROOT/default/systemd/user" >/dev/null 2>&1 &&
|
||||||
pass "migration watcher is edge-triggered so applied migrations on disk cannot re-trigger it"
|
fail "a user unit watches the migration directory again; the notifier must stay login-only"
|
||||||
|
pass "no unit watches the migration directory, so package updates cannot trigger the notifier"
|
||||||
|
|
||||||
notify_service="$ROOT/default/systemd/user/omarchy-update-user-notify.service"
|
notify_service="$ROOT/default/systemd/user/omarchy-migrate-notify.service"
|
||||||
! grep -q 'StartLimit' "$notify_service"
|
grep -Fx 'ExecStart=/usr/bin/omarchy-migrate-notify' "$notify_service" >/dev/null
|
||||||
grep -Fx 'WantedBy=graphical-session.target' "$notify_service" >/dev/null
|
grep -Fx 'WantedBy=graphical-session.target' "$notify_service" >/dev/null
|
||||||
pass "migration notifier keeps its start-rate limit and still runs once per login"
|
pass "migration notifier only checks once per login"
|
||||||
|
|
||||||
|
grep -F 'omarchy-migrate-notify.service' "$first_run_units" >/dev/null ||
|
||||||
|
fail "first-run does not enable the login migration notifier"
|
||||||
|
grep -F 'omarchy-update-user-notify' "$first_run_units" >/dev/null &&
|
||||||
|
fail "first-run still enables the retired notifier units"
|
||||||
|
pass "first-run enables the login-only migration notifier"
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
cleanup() { rm -rf "$WORKDIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
downloads="$WORKDIR/downloads"
|
||||||
|
mkdir -p "$WORKDIR/bin" "$downloads" "$WORKDIR/outbox"
|
||||||
|
printf 'mine' >"$downloads/unrelated.txt"
|
||||||
|
|
||||||
|
# Stands in for the daemon handing over whatever is waiting in the inbox. A
|
||||||
|
# decoy is whatever else drops into the downloads directory while Taildrop is
|
||||||
|
# still blocking on the next delivery.
|
||||||
|
cat >"$WORKDIR/bin/tailscale" <<SH
|
||||||
|
#!/bin/bash
|
||||||
|
target="\${*: -1}"
|
||||||
|
[[ -n \${DECOY:-} ]] && printf 'iso' >"$downloads/\$DECOY"
|
||||||
|
mv "$WORKDIR/outbox/"* "\$target/"
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat >"$WORKDIR/bin/omarchy-notification-send" <<SH
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s\n' "\$*" >>"$WORKDIR/notifications"
|
||||||
|
# Only the photo notification gets clicked.
|
||||||
|
[[ \$* == *photo.png* ]] && echo default
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat >"$WORKDIR/bin/xdg-open" <<SH
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s\n' "\$1" >>"$WORKDIR/opened"
|
||||||
|
SH
|
||||||
|
|
||||||
|
chmod +x "$WORKDIR/bin/"*
|
||||||
|
|
||||||
|
receive() {
|
||||||
|
local expected="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
: >"$WORKDIR/notifications"
|
||||||
|
PATH="$WORKDIR/bin:$PATH" "$@" "$ROOT/bin/omarchy-tailscale-receive" --once "$downloads"
|
||||||
|
|
||||||
|
for _ in {1..50}; do
|
||||||
|
(($(wc -l <"$WORKDIR/notifications") >= expected)) && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
printf 'png' >"$WORKDIR/outbox/photo.png"
|
||||||
|
printf 'pdf' >"$WORKDIR/outbox/notes with space.pdf"
|
||||||
|
receive 2 env
|
||||||
|
|
||||||
|
notifications=$(<"$WORKDIR/notifications")
|
||||||
|
|
||||||
|
[[ -f $downloads/photo.png && -f "$downloads/notes with space.pdf" ]] ||
|
||||||
|
fail "taildrop receive saves incoming files" "$(ls "$downloads")"
|
||||||
|
pass "taildrop receive saves incoming files"
|
||||||
|
|
||||||
|
grep -qF -- "Received photo.png Saved to $downloads --image $downloads/photo.png" <<<"$notifications" ||
|
||||||
|
fail "taildrop receive previews received images" "$notifications"
|
||||||
|
pass "taildrop receive previews received images"
|
||||||
|
|
||||||
|
grep -q "^Received notes with space.pdf .* -g " <<<"$notifications" ||
|
||||||
|
fail "taildrop receive announces other files with a glyph" "$notifications"
|
||||||
|
pass "taildrop receive announces other files with a glyph"
|
||||||
|
|
||||||
|
grep -qxF "$downloads/photo.png" "$WORKDIR/opened" ||
|
||||||
|
fail "taildrop receive opens a clicked file" "$(cat "$WORKDIR/opened" 2>/dev/null)"
|
||||||
|
pass "taildrop receive opens a clicked file"
|
||||||
|
|
||||||
|
grep -q "unrelated.txt" <<<"$notifications" &&
|
||||||
|
fail "taildrop receive leaves the rest of the downloads directory alone" "$notifications"
|
||||||
|
pass "taildrop receive leaves the rest of the downloads directory alone"
|
||||||
|
|
||||||
|
# A second delivery of the same name, alongside a download that arrives while
|
||||||
|
# Taildrop is waiting.
|
||||||
|
printf 'png' >"$WORKDIR/outbox/photo.png"
|
||||||
|
receive 1 env DECOY=browser-download.iso
|
||||||
|
|
||||||
|
notifications=$(<"$WORKDIR/notifications")
|
||||||
|
|
||||||
|
[[ -f $downloads/photo-1.png ]] || fail "taildrop receive keeps both files on a name clash" "$(ls "$downloads")"
|
||||||
|
grep -q "^Received photo-1.png " <<<"$notifications" ||
|
||||||
|
fail "taildrop receive keeps both files on a name clash" "$notifications"
|
||||||
|
pass "taildrop receive keeps both files on a name clash"
|
||||||
|
|
||||||
|
grep -q "browser-download.iso" <<<"$notifications" &&
|
||||||
|
fail "taildrop receive ignores downloads that arrive while it waits" "$notifications"
|
||||||
|
pass "taildrop receive ignores downloads that arrive while it waits"
|
||||||
|
|
||||||
|
[[ -z $(ls -A "$downloads/.omarchy-taildrop") ]] ||
|
||||||
|
fail "taildrop receive empties its staging directory" "$(ls -A "$downloads/.omarchy-taildrop")"
|
||||||
|
pass "taildrop receive empties its staging directory"
|
||||||
@@ -28,7 +28,9 @@ const status = tailscale.parseStatus(JSON.stringify({
|
|||||||
Self: {
|
Self: {
|
||||||
HostName: 'dhh-fd',
|
HostName: 'dhh-fd',
|
||||||
DNSName: 'dhh-fd.tail32f559.ts.net.',
|
DNSName: 'dhh-fd.tail32f559.ts.net.',
|
||||||
TailscaleIPs: ['100.74.97.73']
|
TailscaleIPs: ['100.74.97.73'],
|
||||||
|
UserID: 1001,
|
||||||
|
CapMap: { 'https://tailscale.com/cap/file-sharing': null }
|
||||||
},
|
},
|
||||||
Peer: {
|
Peer: {
|
||||||
onlineB: {
|
onlineB: {
|
||||||
@@ -38,7 +40,9 @@ const status = tailscale.parseStatus(JSON.stringify({
|
|||||||
Online: true,
|
Online: true,
|
||||||
OS: 'linux',
|
OS: 'linux',
|
||||||
ExitNodeOption: true,
|
ExitNodeOption: true,
|
||||||
ExitNode: true
|
ExitNode: true,
|
||||||
|
UserID: 1002,
|
||||||
|
TaildropTarget: 5
|
||||||
},
|
},
|
||||||
offline: {
|
offline: {
|
||||||
HostName: 'offline',
|
HostName: 'offline',
|
||||||
@@ -61,7 +65,9 @@ const status = tailscale.parseStatus(JSON.stringify({
|
|||||||
DNSName: 'alpha.tail32f559.ts.net.',
|
DNSName: 'alpha.tail32f559.ts.net.',
|
||||||
TailscaleIPs: ['100.1.1.1', 'fd7a:115c:a1e0::1901:334b'],
|
TailscaleIPs: ['100.1.1.1', 'fd7a:115c:a1e0::1901:334b'],
|
||||||
Online: true,
|
Online: true,
|
||||||
OS: 'macos'
|
OS: 'macos',
|
||||||
|
UserID: 1001,
|
||||||
|
TaildropTarget: 1
|
||||||
},
|
},
|
||||||
mullvadExit: {
|
mullvadExit: {
|
||||||
HostName: 'al-tia-wg-003',
|
HostName: 'al-tia-wg-003',
|
||||||
@@ -83,6 +89,20 @@ assert(status.peers[1].ExitNodeOption && status.peers[1].ExitNode, 'tailscale pr
|
|||||||
assertDeepEqual(status.exitNodes.map(peer => peer.HostName), ['zed'], 'tailscale lists only online tailnet exit nodes')
|
assertDeepEqual(status.exitNodes.map(peer => peer.HostName), ['zed'], 'tailscale lists only online tailnet exit nodes')
|
||||||
assert(tailscale.isMullvadPeer({ HostName: 'al-tia-wg-003', DNSName: 'al-tia-wg-003.mullvad.ts.net.' }), 'tailscale detects Mullvad status peers')
|
assert(tailscale.isMullvadPeer({ HostName: 'al-tia-wg-003', DNSName: 'al-tia-wg-003.mullvad.ts.net.' }), 'tailscale detects Mullvad status peers')
|
||||||
|
|
||||||
|
assert(status.fileSharing, 'tailscale reads Taildrop capability from the status capability map')
|
||||||
|
assertEqual(status.selfUserId, '1001', 'tailscale records the owning user of this machine')
|
||||||
|
assertDeepEqual(status.peers.map(peer => peer.UserID), ['1001', '1002'], 'tailscale records the owning user of each peer')
|
||||||
|
assert(
|
||||||
|
tailscale.hasFileSharing({ Capabilities: ['https://tailscale.com/cap/file-sharing'] }),
|
||||||
|
'tailscale reads Taildrop capability from the legacy capability list'
|
||||||
|
)
|
||||||
|
assert(!tailscale.hasFileSharing({ CapMap: { funnel: null } }), 'tailscale reports no Taildrop without the capability')
|
||||||
|
assertDeepEqual(status.peers.map(peer => peer.TaildropTarget), [1, 5], 'tailscale records how Tailscale grades each Taildrop target')
|
||||||
|
assert(tailscale.isTaildropTarget({ TaildropTarget: 1, UserID: '1001' }, '2002'), 'tailscale trusts an available Taildrop target')
|
||||||
|
assert(!tailscale.isTaildropTarget({ TaildropTarget: 7, UserID: '1001' }, '1001'), 'tailscale skips peers Tailscale rules out')
|
||||||
|
assert(tailscale.isTaildropTarget({ UserID: '1001' }, '1001'), 'tailscale falls back to same-owner peers without a grade')
|
||||||
|
assert(!tailscale.isTaildropTarget({ UserID: '1002' }, '1001'), 'tailscale skips other owners without a grade')
|
||||||
|
|
||||||
const mullvadNodes = tailscale.parseExitNodeList(`
|
const mullvadNodes = tailscale.parseExitNodeList(`
|
||||||
IP HOSTNAME COUNTRY CITY STATUS
|
IP HOSTNAME COUNTRY CITY STATUS
|
||||||
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Any -
|
100.65.216.13 au-adl-wg-301.mullvad.ts.net Australia Any -
|
||||||
|
|||||||
@@ -26,11 +26,17 @@ STUB
|
|||||||
|
|
||||||
chmod +x "$stub_bin/pacman" "$stub_bin/sudo"
|
chmod +x "$stub_bin/pacman" "$stub_bin/sudo"
|
||||||
|
|
||||||
|
# The migration removes the /etc copy only once the drop-in that replaces it is
|
||||||
|
# installed. Point that at a fixture so the result does not depend on whether
|
||||||
|
# the machine running the tests happens to carry the real one.
|
||||||
|
dropin="$TMPDIR/90-omarchy.conf"
|
||||||
|
: >"$dropin"
|
||||||
|
|
||||||
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
|
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
|
||||||
# whole chain on a non-zero exit, so match that invocation exactly.
|
# whole chain on a non-zero exit, so match that invocation exactly.
|
||||||
run_migration() {
|
run_migration() {
|
||||||
local conf="$1"
|
local conf="$1"
|
||||||
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" \
|
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
|
||||||
bash -euo pipefail "$migration" >/dev/null ||
|
bash -euo pipefail "$migration" >/dev/null ||
|
||||||
fail "migration exits clean for $(basename "$conf")"
|
fail "migration exits clean for $(basename "$conf")"
|
||||||
}
|
}
|
||||||
@@ -74,7 +80,7 @@ pass "migration keeps a locally edited config"
|
|||||||
# them.
|
# them.
|
||||||
conf="$TMPDIR/owned.conf"
|
conf="$TMPDIR/owned.conf"
|
||||||
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
||||||
PATH="$stub_bin:$PATH" PACMAN_OWNS=1 OMARCHY_ZRAM_CONF="$conf" \
|
PATH="$stub_bin:$PATH" PACMAN_OWNS=1 OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
|
||||||
bash -euo pipefail "$migration" >/dev/null ||
|
bash -euo pipefail "$migration" >/dev/null ||
|
||||||
fail "migration exits clean for a package-owned config"
|
fail "migration exits clean for a package-owned config"
|
||||||
[[ -f $conf ]] || fail "migration keeps a package-owned config"
|
[[ -f $conf ]] || fail "migration keeps a package-owned config"
|
||||||
@@ -85,3 +91,14 @@ conf="$TMPDIR/absent.conf"
|
|||||||
run_migration "$conf"
|
run_migration "$conf"
|
||||||
run_migration "$conf"
|
run_migration "$conf"
|
||||||
pass "migration no-ops when the config is already gone"
|
pass "migration no-ops when the config is already gone"
|
||||||
|
|
||||||
|
# Without the drop-in installed, the /etc copy is the only thing configuring
|
||||||
|
# zram at all. Removing it would leave the machine with no zram device, so the
|
||||||
|
# migration has to leave it alone and stay clean doing it.
|
||||||
|
conf="$TMPDIR/no-dropin.conf"
|
||||||
|
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
||||||
|
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$TMPDIR/absent-dropin.conf" \
|
||||||
|
bash -euo pipefail "$migration" >/dev/null ||
|
||||||
|
fail "migration exits clean when the drop-in is missing"
|
||||||
|
[[ -f $conf ]] || fail "migration keeps the config when the drop-in is missing"
|
||||||
|
pass "migration keeps the config until the drop-in is installed"
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ desired_bytes=$((8192 * 1024 * 1024))
|
|||||||
run_migration() {
|
run_migration() {
|
||||||
local disksize="$1" used="$2" fail_reload="${3:-0}"
|
local disksize="$1" used="$2" fail_reload="${3:-0}"
|
||||||
|
|
||||||
printf '%s' "$disksize" >"$TMPDIR/disksize"
|
# A machine with no zram device has no /sys/block/zram0 at all, so an empty
|
||||||
|
# size means the file is gone rather than blank; the migration tells those
|
||||||
|
# two apart now.
|
||||||
|
if [[ -n $disksize ]]; then
|
||||||
|
printf '%s' "$disksize" >"$TMPDIR/disksize"
|
||||||
|
else
|
||||||
|
rm -f "$TMPDIR/disksize"
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'Filename\tType\tSize\tUsed\tPriority\n' >"$TMPDIR/swaps"
|
printf 'Filename\tType\tSize\tUsed\tPriority\n' >"$TMPDIR/swaps"
|
||||||
[[ -n $used ]] &&
|
[[ -n $used ]] &&
|
||||||
printf '/dev/zram0 partition 8388604 %s 100\n' "$used" >>"$TMPDIR/swaps"
|
printf '/dev/zram0 partition 8388604 %s 100\n' "$used" >>"$TMPDIR/swaps"
|
||||||
@@ -101,6 +109,15 @@ run_migration "" ""
|
|||||||
did "systemctl restart dev-zram0.swap" || fail "absent device is created"
|
did "systemctl restart dev-zram0.swap" || fail "absent device is created"
|
||||||
pass "absent device is created"
|
pass "absent device is created"
|
||||||
|
|
||||||
|
# A device that exists but is swapped off reads empty too, and there the
|
||||||
|
# restart resets it, which fails against whatever still holds it open. Nothing
|
||||||
|
# to gain over the reboot that would have resized it anyway.
|
||||||
|
run_migration $((4096 * 1024 * 1024)) ""
|
||||||
|
did "systemctl restart" && fail "swapped-off device is not restarted"
|
||||||
|
did "omarchy-state set reboot-required" || fail "swapped-off device asks for a reboot"
|
||||||
|
pass "swapped-off device is not restarted"
|
||||||
|
pass "swapped-off device asks for a reboot"
|
||||||
|
|
||||||
# A failed daemon-reload must fall back to asking for a reboot.
|
# A failed daemon-reload must fall back to asking for a reboot.
|
||||||
run_migration $((4096 * 1024 * 1024)) 0 1
|
run_migration $((4096 * 1024 * 1024)) 0 1
|
||||||
did "systemctl restart" && fail "failed reload does not restart"
|
did "systemctl restart" && fail "failed reload does not restart"
|
||||||
|
|||||||
Reference in New Issue
Block a user