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.
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ GROUP_DESCRIPTIONS[dev]="Omarchy development tools"
|
||||
GROUP_DESCRIPTIONS[display]="Display and text scaling"
|
||||
GROUP_DESCRIPTIONS[dns]="DNS resolver configuration"
|
||||
GROUP_DESCRIPTIONS[drive]="Drive selection and encryption"
|
||||
GROUP_DESCRIPTIONS[file]="File selection helpers"
|
||||
GROUP_DESCRIPTIONS[font]="Font management"
|
||||
GROUP_DESCRIPTIONS[games]="Game launchers and helpers"
|
||||
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[sudo]="Sudo configuration helpers"
|
||||
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
||||
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
|
||||
GROUP_DESCRIPTIONS[theme]="Theme management"
|
||||
GROUP_DESCRIPTIONS[tmux]="Tmux session helpers"
|
||||
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
||||
|
||||
+10
-1
@@ -288,6 +288,15 @@ cmd_move() {
|
||||
fi
|
||||
|
||||
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
|
||||
prog=$(cat <<JQ
|
||||
$JQ_DEFS $NORMALIZE
|
||||
@@ -302,7 +311,7 @@ JQ
|
||||
commit "$prog" \
|
||||
--arg id "$id" \
|
||||
--arg defaultSection "$default_section" \
|
||||
--arg targetSection "$PLACEMENT_SECTION" \
|
||||
--arg targetSection "$target_section" \
|
||||
--arg targetIndex "$PLACEMENT_INDEX" \
|
||||
--arg before "$PLACEMENT_BEFORE" \
|
||||
--arg after "$PLACEMENT_AFTER" \
|
||||
|
||||
@@ -35,6 +35,7 @@ RESOLUTION=""
|
||||
FULLSCREEN="false"
|
||||
STOP_RECORDING="false"
|
||||
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")
|
||||
|
||||
for arg in "$@"; do
|
||||
@@ -89,11 +90,26 @@ start_webcam_overlay() {
|
||||
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
|
||||
--no-border --no-audio --no-osc --osd-level=0 \
|
||||
--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() {
|
||||
pkill -f "WebcamOverlay" 2>/dev/null
|
||||
rm -f "$REGION_FILE"
|
||||
}
|
||||
|
||||
default_resolution() {
|
||||
@@ -155,7 +171,7 @@ start_screenrecording() {
|
||||
esac
|
||||
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 audio_devices=""
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
readonly MARGIN=40
|
||||
readonly REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
|
||||
|
||||
usage() {
|
||||
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
|
||||
|
||||
# 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.
|
||||
small_height=$(((monitor_height * 9 + 25) / 50))
|
||||
small_height=$(((scale_height * 9 + 25) / 50))
|
||||
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))
|
||||
large_height=$(((monitor_height * 27 + 40) / 80))
|
||||
large_height=$(((scale_height * 27 + 40) / 80))
|
||||
large_width=$(((large_height * 8 + 4) / 9))
|
||||
|
||||
target_width=$current_width
|
||||
@@ -107,11 +134,11 @@ larger)
|
||||
;;
|
||||
esac
|
||||
|
||||
target_x=$((monitor_x + monitor_width - target_width - MARGIN))
|
||||
target_y=$((monitor_y + monitor_height - target_height - MARGIN))
|
||||
target_x=$((anchor_x + anchor_width - target_width - MARGIN))
|
||||
target_y=$((anchor_y + anchor_height - target_height - MARGIN))
|
||||
|
||||
((target_x < monitor_x + MARGIN)) && target_x=$((monitor_x + MARGIN))
|
||||
((target_y < monitor_y + MARGIN)) && target_y=$((monitor_y + MARGIN))
|
||||
((target_x < anchor_x + MARGIN)) && target_x=$((anchor_x + MARGIN))
|
||||
((target_y < anchor_y + MARGIN)) && target_y=$((anchor_y + MARGIN))
|
||||
|
||||
window="address:$address"
|
||||
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"
|
||||
}
|
||||
|
||||
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() {
|
||||
local name="$1"
|
||||
shift
|
||||
@@ -98,12 +67,6 @@ run_first_run_step() {
|
||||
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" \
|
||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.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" \
|
||||
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" \
|
||||
bash "$OMARCHY_PATH/install/user/first-run/welcome.sh"
|
||||
# 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..."
|
||||
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..."
|
||||
omarchy-bar-plugin add omarchy.tailscale
|
||||
|
||||
|
||||
+3
-2
@@ -96,6 +96,7 @@ while IFS=$'\t' read -r name file marker; do
|
||||
fi
|
||||
done < <(migration_entries)
|
||||
|
||||
# Clear notifications queued while an update was installing migrations. The
|
||||
# substring matches both the current and legacy notification titles.
|
||||
# Clear a login-time notification the user left sitting there and then resolved
|
||||
# by running migrations some other way. The substring matches both the current
|
||||
# and legacy notification titles.
|
||||
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")
|
||||
|
||||
if omarchy-cmd-present systemd-run; then
|
||||
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
|
||||
fi
|
||||
# This runs from omarchy-migrate-notify.service at graphical-session.target,
|
||||
# which the session can reach before the shell has claimed
|
||||
# org.freedesktop.Notifications. Without the wait the toast is sent into the
|
||||
# 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() {
|
||||
echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:"
|
||||
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
|
||||
|
||||
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
|
||||
omarchy-bar-plugin remove omarchy.tailscale
|
||||
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>
|
||||
</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>
|
||||
<family>system-ui</family>
|
||||
<prefer>
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
-- DaVinci Resolve dialog focus handling.
|
||||
o.window(".*[Rr]esolve.*", { float = true, stay_focused = true })
|
||||
-- DaVinci Resolve window focus handling. Kept fully opaque: the default
|
||||
-- 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",
|
||||
}
|
||||
)
|
||||
-- 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({
|
||||
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.*)",
|
||||
}, { tag = "+floating-window" })
|
||||
o.window("dev.tensaku.Tensaku", { float = true })
|
||||
|
||||
+23
-3
@@ -21,14 +21,34 @@ local function read_vconsole()
|
||||
return values
|
||||
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 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({
|
||||
input = {
|
||||
kb_layout = vconsole.XKBLAYOUT or "us",
|
||||
kb_variant = vconsole.XKBVARIANT or "",
|
||||
kb_layout = kb_layout,
|
||||
kb_variant = kb_variant,
|
||||
kb_model = "",
|
||||
kb_options = "compose:caps,shift:both_capslock",
|
||||
kb_options = kb_options,
|
||||
kb_rules = "",
|
||||
follow_mouse = 1,
|
||||
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-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.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.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"},
|
||||
|
||||
@@ -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;
|
||||
machine-wide repairs should no-op when another user already applied them.
|
||||
|
||||
Each graphical user has `omarchy-update-user-notify.path` watching the packaged
|
||||
migration directory for changes, and `omarchy-update-user-notify.service` is
|
||||
also started once per login via its own `WantedBy=graphical-session.target`.
|
||||
Either way the service runs `omarchy-migrate-notify` as that user. The notifier checks
|
||||
`omarchy-migrate --pending`. If this user has missing migration state, it shows a
|
||||
notification that opens a terminal for `omarchy-migrate`. The notifier never runs
|
||||
migrations in the background.
|
||||
Each graphical user has `omarchy-migrate-notify.service`, started once per login
|
||||
through `WantedBy=graphical-session.target`. The package also ships
|
||||
`omarchy-update-user-notify.service` as a symlink onto it, so users enabled
|
||||
under the old unit name keep working before they reach migration `1785095882`.
|
||||
It runs `omarchy-migrate-notify` as
|
||||
that user, which checks `omarchy-migrate --pending`. If this user has missing
|
||||
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
|
||||
pending migrations. It does not need `--force`; migrations happen when state
|
||||
@@ -221,8 +227,8 @@ systemd instance:
|
||||
Voxtype post-update hook.
|
||||
- `install/user/first-run/enable-user-units.sh` — `systemctl --user enable`
|
||||
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
|
||||
`omarchy-recover-internal-monitor`, `omarchy-update-user-notify.path`,
|
||||
`omarchy-update-user-notify.service`). Done here, not at finalize, because
|
||||
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`).
|
||||
Done here, not at finalize, because
|
||||
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
|
||||
in the unit files keeps services inert when they don't apply.
|
||||
- `install/user/first-run/gnome-theme.sh`,
|
||||
@@ -250,7 +256,8 @@ the legacy finalization marker from `~/.local/state/omarchy/` into `done/`.
|
||||
finalization. It sources:
|
||||
|
||||
- `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
|
||||
device-specific kernel modules, udev rules, microcode, wireless regdom,
|
||||
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
|
||||
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:
|
||||
|
||||
```bash
|
||||
omarchy update
|
||||
```
|
||||
|
||||
If a user explicitly bypasses the guard, user sessions watch the packaged
|
||||
migration directory and run a notifier. The notifier checks:
|
||||
Every graphical login starts `omarchy-migrate-notify.service`, which checks:
|
||||
|
||||
```bash
|
||||
omarchy-migrate --pending
|
||||
@@ -67,6 +60,15 @@ omarchy-migrate
|
||||
|
||||
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
|
||||
|
||||
Users can safely run:
|
||||
|
||||
+26
-8
@@ -149,20 +149,37 @@ High-level flow:
|
||||
sudo pacman -Syu
|
||||
├─ pre-transaction guard aborts and tells the user to run omarchy update
|
||||
└─ if explicitly bypassed, upgrades omarchy and related packages
|
||||
└─ user session notices migration directory changes
|
||||
├─ omarchy-update-user-notify.path triggers, if enabled
|
||||
└─ at that user's next login
|
||||
├─ omarchy-migrate-notify.service starts with graphical-session.target
|
||||
├─ omarchy-migrate-notify checks omarchy-migrate --pending
|
||||
├─ if this user has missing migration state, show notification
|
||||
└─ 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:
|
||||
|
||||
- `omarchy-first-run` enables the user notification path unit.
|
||||
- `omarchy-first-run` also invokes `omarchy-migrate-notify` on graphical
|
||||
startup, so users who updated before the path unit existed still get prompted
|
||||
if they have missing migration state.
|
||||
- `omarchy-first-run` enables `omarchy-migrate-notify.service`, which also
|
||||
covers users created after install: their per-user migration markers are
|
||||
missing, so their first login prompts them to run every shipped migration.
|
||||
- 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.
|
||||
- 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
|
||||
explicitly runs that hook; without a package-update marker, the only pending
|
||||
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-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-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-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. |
|
||||
@@ -229,7 +246,8 @@ scripts.
|
||||
idempotent when they repair machine-wide state.
|
||||
|
||||
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.
|
||||
|
||||
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/docker.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/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 \
|
||||
omarchy-recover-internal-monitor.service \
|
||||
omarchy-sleep-lock.service \
|
||||
omarchy-update-user-notify.path \
|
||||
omarchy-update-user-notify.service
|
||||
omarchy-migrate-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
|
||||
# property at runtime, so it stays correct either way -- the reboot flag is only
|
||||
# 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
|
||||
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 \
|
||||
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
|
||||
omarchy-state set reboot-required
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
echo "Move zram tuning to a vendor drop-in"
|
||||
|
||||
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.
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
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.
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,10 +73,35 @@ function loginPlan(needsLogin, authUrl) {
|
||||
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) {
|
||||
return {
|
||||
id: id,
|
||||
HostName: displayHostName(peer.HostName, peer.DNSName),
|
||||
UserID: String(peer.UserID || ""),
|
||||
TaildropTarget: typeof peer.TaildropTarget === "number" ? peer.TaildropTarget : 0,
|
||||
DNSName: cleanDnsName(peer.DNSName),
|
||||
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
||||
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
||||
@@ -235,6 +260,8 @@ function parseStatus(raw) {
|
||||
selfName: displayHostName(self.HostName, self.DNSName),
|
||||
selfDnsName: cleanDnsName(self.DNSName),
|
||||
selfIp: selfIps.length > 0 ? selfIps[0] : "",
|
||||
selfUserId: String(self.UserID || ""),
|
||||
fileSharing: hasFileSharing(self),
|
||||
peers: peers,
|
||||
exitNodes: exitNodes
|
||||
}
|
||||
@@ -285,6 +312,8 @@ if (typeof module !== "undefined") {
|
||||
osIcon: osIcon,
|
||||
accountLabel: accountLabel,
|
||||
loginPlan: loginPlan,
|
||||
hasFileSharing: hasFileSharing,
|
||||
isTaildropTarget: isTaildropTarget,
|
||||
isMullvadPeer: isMullvadPeer,
|
||||
peerFromStatus: peerFromStatus,
|
||||
parseExitNodeList: parseExitNodeList,
|
||||
|
||||
@@ -295,6 +295,13 @@ Panel {
|
||||
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() {
|
||||
if (!peerColumn || peerIndex < 0 || peerIndex >= peerColumn.children.length) return
|
||||
var item = peerColumn.children[peerIndex]
|
||||
@@ -416,6 +423,7 @@ Panel {
|
||||
else if (t === "c" || t === "C") tailscale.copyPeerIp(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 === "s" || t === "S") root.sendPeerFile(root.selectedPeer())
|
||||
}
|
||||
|
||||
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 {
|
||||
id: copyButton
|
||||
iconText: ""
|
||||
|
||||
@@ -10,6 +10,7 @@ Native Omarchy bar widget for Tailscale.
|
||||
- Switch between available Tailscale connections when multiple are available
|
||||
- Browse machines from `tailscale status --json`
|
||||
- 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
|
||||
|
||||
@@ -20,6 +21,7 @@ Inside the panel:
|
||||
- `c`: copy selected peer IP
|
||||
- `n`: copy selected peer name
|
||||
- `d`: copy selected peer DNS name
|
||||
- `s`: send files to selected peer
|
||||
- `t`: toggle Tailscale
|
||||
- `r`: refresh status
|
||||
- `esc`: close
|
||||
@@ -28,6 +30,15 @@ Inside the panel:
|
||||
|
||||
- `tailscale` CLI on `PATH`
|
||||
- `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
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ Item {
|
||||
property string selfName: ""
|
||||
property string selfDnsName: ""
|
||||
property string selfIp: ""
|
||||
property string selfUserId: ""
|
||||
property bool fileSharing: false
|
||||
property string authUrl: ""
|
||||
property var peers: []
|
||||
property var exitNodes: []
|
||||
@@ -123,6 +125,26 @@ Item {
|
||||
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) {
|
||||
if (installed) {
|
||||
refreshStatusAndAccounts(forceAccounts === true)
|
||||
@@ -137,18 +159,21 @@ Item {
|
||||
|
||||
function refreshStatusAndAccounts(forceAccounts) {
|
||||
if (!installed) return
|
||||
var launched = false
|
||||
if (!statusProcess.running) {
|
||||
_statusOutput = ""
|
||||
_statusError = ""
|
||||
refreshing = true
|
||||
statusProcess.command = ["tailscale", "status", "--json"]
|
||||
statusProcess.running = true
|
||||
launched = true
|
||||
}
|
||||
if (!mullvadExitNodesProcess.running) {
|
||||
_mullvadExitNodesOutput = ""
|
||||
_mullvadExitNodesError = ""
|
||||
mullvadExitNodesProcess.command = ["tailscale", "exit-node", "list"]
|
||||
mullvadExitNodesProcess.running = true
|
||||
launched = true
|
||||
}
|
||||
var now = Date.now()
|
||||
var shouldRefreshAccounts = forceAccounts === true || accounts.length === 0 || now - _lastAccountsRefreshMs > 60000
|
||||
@@ -158,7 +183,13 @@ Item {
|
||||
_lastAccountsRefreshMs = now
|
||||
accountsProcess.command = ["tailscale", "switch", "--list", "--json"]
|
||||
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) {
|
||||
@@ -175,6 +206,8 @@ Item {
|
||||
selfName = ""
|
||||
selfDnsName = ""
|
||||
selfIp = ""
|
||||
selfUserId = ""
|
||||
fileSharing = false
|
||||
authUrl = ""
|
||||
peers = []
|
||||
exitNodes = []
|
||||
@@ -212,6 +245,8 @@ Item {
|
||||
selfName = parsed.selfName
|
||||
selfDnsName = parsed.selfDnsName
|
||||
selfIp = parsed.selfIp
|
||||
selfUserId = parsed.selfUserId
|
||||
fileSharing = parsed.fileSharing
|
||||
peers = parsed.running ? parsed.peers : []
|
||||
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
||||
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadRegions) : []
|
||||
@@ -295,10 +330,7 @@ Item {
|
||||
var mullvadIps = filterIPv4(peer.TailscaleIPs || [])
|
||||
if (mullvadIps.length > 0) return mullvadIps[0]
|
||||
}
|
||||
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] : ""
|
||||
return peerAddress(peer)
|
||||
}
|
||||
|
||||
function setExitNode(peer) {
|
||||
@@ -386,6 +418,22 @@ Item {
|
||||
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 {
|
||||
id: actionStatusTimer
|
||||
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
|
||||
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 '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.center | ids == [
|
||||
"omarchy.indicators",
|
||||
"omarchy.clock",
|
||||
"omarchy.weather",
|
||||
"omarchy.system-update"
|
||||
]
|
||||
(.bar.layout.center | ids) as $ids |
|
||||
($ids | index("omarchy.weather")) as $weather |
|
||||
($ids | index("omarchy.system-update")) as $update |
|
||||
$weather != null and $update == $weather + 1
|
||||
' "$ROOT/config/omarchy/shell.json" >/dev/null
|
||||
pass "default center layout keeps update next to weather"
|
||||
|
||||
@@ -94,12 +94,29 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(os.environ["ROOT"])
|
||||
home = Path.home()
|
||||
pkgs_candidates = [
|
||||
root.parent / "omarchy-pkgs/pkgbuilds",
|
||||
root.parent / "omarchy/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"
|
||||
omarchy_pkgbuild_path = pkgs_root / "omarchy/PKGBUILD"
|
||||
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/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-update-user-notify.service", "/usr/lib/systemd/user/omarchy-update-user-notify.service", "systemd/user/omarchy-update-user-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-migrate-notify.service", "/usr/lib/systemd/user/omarchy-migrate-notify.service", "systemd/user/omarchy-migrate-notify.service"),
|
||||
("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/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
|
||||
("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):
|
||||
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 = [
|
||||
"00-omarchy-update-guard.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 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')
|
||||
assertEqual(
|
||||
triggerItems[0].id,
|
||||
|
||||
@@ -33,6 +33,12 @@ bash -c "$command"
|
||||
SH
|
||||
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'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
|
||||
|
||||
@@ -45,6 +45,8 @@ SH
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
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_RECORDER_ARGS="$tmp_dir/recorder-args"
|
||||
export OMARCHY_TEST_NOTIFICATION_ARGS="$tmp_dir/notification-args"
|
||||
@@ -153,6 +155,59 @@ if [[ -s $OMARCHY_TEST_HYPRCTL_ARGS ]]; then
|
||||
fi
|
||||
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")' \
|
||||
"$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")' \
|
||||
|
||||
@@ -72,7 +72,10 @@ grep -Fx 'systemctl enable --now snapper-cleanup.timer limine-snapper-sync.servi
|
||||
pass "snapshot configure normalizes Snapper policy and services"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
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() {
|
||||
local candidate
|
||||
for candidate in \
|
||||
${OMARCHY_PKGS_PATH:+"$OMARCHY_PKGS_PATH/pkgbuilds" "$OMARCHY_PKGS_PATH"} \
|
||||
"$ROOT/../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
|
||||
cd "$candidate" && pwd
|
||||
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"
|
||||
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() {
|
||||
local candidate
|
||||
for candidate in \
|
||||
${OMARCHY_ISO_PATH:+"$OMARCHY_ISO_PATH"} \
|
||||
"$ROOT/../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
|
||||
cd "$candidate" && pwd
|
||||
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
|
||||
pass "Omarchy 4 upgrade repairs the legacy sleep lock unit path"
|
||||
|
||||
notify_path="$ROOT/default/systemd/user/omarchy-update-user-notify.path"
|
||||
! grep -q 'PathExistsGlob' "$notify_path"
|
||||
grep -Fx 'PathModified=/usr/share/omarchy/migrations' "$notify_path" >/dev/null
|
||||
pass "migration watcher is edge-triggered so applied migrations on disk cannot re-trigger it"
|
||||
[[ -e $ROOT/default/systemd/user/omarchy-update-user-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 -rlE '^(Path[A-Za-z]+|DirectoryNotEmpty)=.*/usr/share/omarchy/migrations' "$ROOT/default/systemd/user" >/dev/null 2>&1 &&
|
||||
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"
|
||||
! grep -q 'StartLimit' "$notify_service"
|
||||
notify_service="$ROOT/default/systemd/user/omarchy-migrate-notify.service"
|
||||
grep -Fx 'ExecStart=/usr/bin/omarchy-migrate-notify' "$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: {
|
||||
HostName: 'dhh-fd',
|
||||
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: {
|
||||
onlineB: {
|
||||
@@ -38,7 +40,9 @@ const status = tailscale.parseStatus(JSON.stringify({
|
||||
Online: true,
|
||||
OS: 'linux',
|
||||
ExitNodeOption: true,
|
||||
ExitNode: true
|
||||
ExitNode: true,
|
||||
UserID: 1002,
|
||||
TaildropTarget: 5
|
||||
},
|
||||
offline: {
|
||||
HostName: 'offline',
|
||||
@@ -61,7 +65,9 @@ const status = tailscale.parseStatus(JSON.stringify({
|
||||
DNSName: 'alpha.tail32f559.ts.net.',
|
||||
TailscaleIPs: ['100.1.1.1', 'fd7a:115c:a1e0::1901:334b'],
|
||||
Online: true,
|
||||
OS: 'macos'
|
||||
OS: 'macos',
|
||||
UserID: 1001,
|
||||
TaildropTarget: 1
|
||||
},
|
||||
mullvadExit: {
|
||||
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')
|
||||
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(`
|
||||
IP HOSTNAME COUNTRY CITY STATUS
|
||||
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"
|
||||
|
||||
# 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
|
||||
# whole chain on a non-zero exit, so match that invocation exactly.
|
||||
run_migration() {
|
||||
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 ||
|
||||
fail "migration exits clean for $(basename "$conf")"
|
||||
}
|
||||
@@ -74,7 +80,7 @@ pass "migration keeps a locally edited config"
|
||||
# them.
|
||||
conf="$TMPDIR/owned.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 ||
|
||||
fail "migration exits clean for 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"
|
||||
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() {
|
||||
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"
|
||||
[[ -n $used ]] &&
|
||||
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"
|
||||
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.
|
||||
run_migration $((4096 * 1024 * 1024)) 0 1
|
||||
did "systemctl restart" && fail "failed reload does not restart"
|
||||
|
||||
Reference in New Issue
Block a user