From 9b8c9fd5372ac08db94c42537a8d5d1b52e17ce1 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Tue, 9 Jun 2026 13:05:30 -0400 Subject: [PATCH] Update Omarchy channel switcher --- bin/omarchy-channel-current | 33 +++++ bin/omarchy-channel-set | 120 ++++++++++++++++-- default/omarchy/omarchy-menu.jsonc | 10 +- test/shell.d/channel-test.sh | 191 +++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 14 deletions(-) create mode 100755 bin/omarchy-channel-current create mode 100644 test/shell.d/channel-test.sh diff --git a/bin/omarchy-channel-current b/bin/omarchy-channel-current new file mode 100755 index 00000000..49be7d61 --- /dev/null +++ b/bin/omarchy-channel-current @@ -0,0 +1,33 @@ +#!/bin/bash + +# omarchy:summary=Print the active Omarchy package channel + +set -euo pipefail + +conf=${OMARCHY_CONFIG_FILE:-/etc/omarchy.conf} +omarchy_path=${OMARCHY_PATH:-/usr/share/omarchy} + +if [[ -r $conf ]]; then + configured_path=$(bash -c 'source "$1"; printf "%s\n" "${OMARCHY_PATH:-}"' bash "$conf" 2>/dev/null || true) + [[ -n $configured_path ]] && omarchy_path="$configured_path" +fi + +channel=$(omarchy-version-channel 2>/dev/null | awk '{ print $1 }') + +if pacman -Q omarchy-dev omarchy-settings-dev >/dev/null 2>&1; then + case "$channel" in + rc) echo rc ;; + edge) + if [[ $omarchy_path == "/usr/share/omarchy" ]]; then + echo dev + else + echo live-dev + fi + ;; + *) echo unknown ;; + esac +elif pacman -Q omarchy omarchy-settings >/dev/null 2>&1 && [[ $channel == "stable" ]]; then + echo stable +else + echo unknown +fi diff --git a/bin/omarchy-channel-set b/bin/omarchy-channel-set index 55d1e1bb..b84c7aa3 100755 --- a/bin/omarchy-channel-set +++ b/bin/omarchy-channel-set @@ -1,21 +1,123 @@ #!/bin/bash # omarchy:summary=Set the Omarchy package channel. -# omarchy:args= +# omarchy:args= # omarchy:requires-sudo=true -set -e +set -euo pipefail -if (($# == 0)); then - echo "Usage: omarchy-channel-set [stable|rc|edge|dev]" - exit 1 -fi +usage() { echo "Usage: omarchy-channel-set [stable|rc|dev|live-dev]"; } +fail() { echo "Error: $*" >&2; exit 1; } +confirm_live_dev() { + cat <<'WARNING' + +Live Dev links Omarchy directly to a mutable source checkout. + +This disables package-based protections for the Omarchy code path, including +normal package updates and package-backed snapshot restores. You'll be +responsible for pulling, fixing, and restoring that checkout yourself. + +WARNING + + if omarchy-cmd-present gum; then + gum confirm --default=false "Enable Live Dev anyway?" + else + local answer="" + read -r -p "Enable Live Dev anyway? [y/N] " answer + [[ $answer == "y" || $answer == "Y" || $answer == "yes" || $answer == "YES" ]] + fi +} + +choose_live_dev_checkout() { + local default_path="$HOME/Work/omarchy" + local path="${OMARCHY_LIVE_DEV_PATH:-}" + + if [[ -z $path ]]; then + if omarchy-cmd-present gum; then + path=$(gum input --value "$default_path" --placeholder "$default_path" --header "Where should Live Dev checkout live? Existing non-checkout paths will not be overwritten.") || exit 1 + else + read -r -p "Live Dev checkout path [$default_path]: " path + fi + fi + + path="${path:-$default_path}" + case "$path" in + "~") path="$HOME" ;; + "~/"*) path="$HOME/${path#~/}" ;; + /*) ;; + *) path="$PWD/$path" ;; + esac + + if [[ -e $path && ! -d $path/.git ]]; then + fail "$path already exists and is not a git checkout. Choose an empty path or an existing Omarchy checkout." + fi + + if [[ -d $path/.git && ( ! -d $path/bin || ! -d $path/default || ! -d $path/shell ) ]]; then + fail "$path is a git checkout, but it does not look like Omarchy." + fi + + printf '%s\n' "$path" +} + +sync_live_dev_checkout() { + local checkout="$1" + + if [[ -d $checkout/.git ]]; then + echo "Updating Live Dev checkout at $checkout" + git -C "$checkout" fetch origin quattro + git -C "$checkout" checkout quattro 2>/dev/null || git -C "$checkout" checkout --track origin/quattro + git -C "$checkout" pull --ff-only origin quattro + else + mkdir -p "$(dirname -- "$checkout")" + git clone --branch quattro --single-branch https://github.com/basecamp/omarchy.git "$checkout" + fi + + omarchy-dev-link "$checkout" +} + +(( $# > 0 )) || { usage; exit 1; } + +live_dev_checkout="" channel="$1" + case "$channel" in - stable|rc|edge) omarchy-refresh-pacman "$channel" ;; - dev) omarchy-refresh-pacman edge ;; - *) echo "Unknown channel: $channel"; exit 1 ;; + stable) + pacman_channel=stable + packages=(omarchy omarchy-settings) + ;; + rc) + pacman_channel=rc + packages=(omarchy-dev omarchy-settings-dev) + ;; + dev) + pacman_channel=edge + packages=(omarchy-dev omarchy-settings-dev) + ;; + live-dev|live|source) + confirm_live_dev || { echo "Cancelled."; exit 0; } + live_dev_checkout=$(choose_live_dev_checkout) + pacman_channel=edge + packages=(omarchy-dev omarchy-settings-dev) + ;; + *) + echo "Unknown channel: $channel" >&2 + usage >&2 + exit 1 + ;; esac +omarchy-refresh-pacman "$pacman_channel" +# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites. +sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}" + +if [[ -z $live_dev_checkout ]] && omarchy-cmd-present omarchy-dev-unlink; then + omarchy-dev-unlink + export OMARCHY_PATH=/usr/share/omarchy +fi + omarchy-update -y + +if [[ -n $live_dev_checkout ]]; then + sync_live_dev_checkout "$live_dev_checkout" +fi diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 478c3792..4c7975a1 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -284,7 +284,7 @@ // Update "update.omarchy": {"icon":"","label":"Omarchy","keywords":"system","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update"}, - "update.channel": {"icon":"󰔫","label":"Channel","keywords":"stable rc edge dev"}, + "update.channel": {"icon":"󰔫","label":"Channel","keywords":"stable rc dev live source"}, "update.config": {"icon":"","label":"Config","keywords":"refresh default"}, "update.themes": {"icon":"󰸌","label":"Extra Themes","action":"omarchy-launch-floating-terminal-with-presentation omarchy-theme-update"}, "update.process": {"icon":"","label":"Process","keywords":"restart services"}, @@ -293,10 +293,10 @@ "update.password": {"icon":"","label":"Password","keywords":"drive user"}, "update.timezone": {"icon":"","label":"Timezone","keywords":"time zone","action":"omarchy-menu-timezone"}, "update.time": {"icon":"","label":"Time","keywords":"clock ntp","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update-time"}, - "update.channel.stable": {"icon":"🟢","label":"Stable","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set stable'"}, - "update.channel.rc": {"icon":"🟡","label":"RC","keywords":"release candidate","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set rc'"}, - "update.channel.edge": {"icon":"🟠","label":"Edge","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set edge'"}, - "update.channel.dev": {"icon":"🔴","label":"Dev","keywords":"development","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set dev'"}, + "update.channel.stable": {"icon":"🟢","label":"Stable","checked":"[[ \"$(omarchy-channel-current)\" == \"stable\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set stable'"}, + "update.channel.rc": {"icon":"🟡","label":"RC","keywords":"release candidate","checked":"[[ \"$(omarchy-channel-current)\" == \"rc\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set rc'"}, + "update.channel.dev": {"icon":"🟠","label":"Dev","keywords":"development","checked":"[[ \"$(omarchy-channel-current)\" == \"dev\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set dev'"}, + "update.channel.live-dev": {"icon":"🔴","label":"Live Dev","keywords":"source mode development quattro","checked":"[[ \"$(omarchy-channel-current)\" == \"live-dev\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set live-dev'"}, "update.process.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"restart","action":"omarchy-restart-hyprsunset"}, "update.process.osd": {"icon":"","label":"OSD","keywords":"restart","action":"omarchy-restart-shell"}, "update.process.shell": {"icon":"󰍜","label":"Shell","keywords":"restart bar quickshell omarchy-shell","action":"omarchy-restart-shell"}, diff --git a/test/shell.d/channel-test.sh b/test/shell.d/channel-test.sh new file mode 100644 index 00000000..5b820fb1 --- /dev/null +++ b/test/shell.d/channel-test.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +log_file="$test_tmp/channel.log" +mkdir -p "$stub_bin" "$test_tmp/home" + +write_stub() { + local name="$1" + local body="$2" + + cat >"$stub_bin/$name" <<<"$body" + chmod +x "$stub_bin/$name" +} + +write_stub omarchy-refresh-pacman '#!/bin/bash +printf "refresh" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +' + +write_stub sudo '#!/bin/bash +printf "sudo" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +' + +write_stub omarchy-dev-unlink '#!/bin/bash +printf "unlink" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +' + +write_stub omarchy-update '#!/bin/bash +printf "update" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +' + +write_stub gum '#!/bin/bash +printf "gum" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +if [[ $1 == "input" ]]; then + printf "%s\n" "${OMARCHY_TEST_GUM_INPUT:-$HOME/Work/omarchy}" +fi +exit 0 +' + +write_stub omarchy-cmd-missing '#!/bin/bash +[[ $1 == "git" ]] && exit 1 +exit 0 +' + +write_stub omarchy-cmd-present '#!/bin/bash +[[ $1 == "omarchy-dev-unlink" || $1 == "gum" || $1 == "git" ]] && exit 0 +exit 1 +' + +write_stub git '#!/bin/bash +printf "git" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +if [[ $1 == "clone" ]]; then + dest="${@: -1}" + mkdir -p "$dest/.git" "$dest/bin" "$dest/default" "$dest/shell" +fi +' + +write_stub omarchy-dev-link '#!/bin/bash +printf "link" >>"$OMARCHY_CHANNEL_TEST_LOG" +for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done +printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG" +' + +write_stub omarchy-version-channel '#!/bin/bash +printf "%s\n" "${OMARCHY_TEST_VERSION_CHANNEL:-unknown}" +' + +write_stub pacman '#!/bin/bash +[[ $1 == "-Q" ]] || exit 1 +shift +case "${OMARCHY_TEST_PACKAGES:-}" in + stable) [[ $* == "omarchy omarchy-settings" ]] ;; + dev) [[ $* == "omarchy-dev omarchy-settings-dev" ]] ;; + *) exit 1 ;; +esac +' + +run_channel() { + : >"$log_file" + OMARCHY_CHANNEL_TEST_LOG="$log_file" \ + OMARCHY_LIVE_DEV_PATH="${OMARCHY_LIVE_DEV_PATH:-}" \ + OMARCHY_TEST_GUM_INPUT="${OMARCHY_TEST_GUM_INPUT:-}" \ + OMARCHY_PATH="$ROOT" \ + HOME="$test_tmp/home" \ + PATH="$stub_bin:$ROOT/bin:$PATH" \ + "$ROOT/bin/omarchy-channel-set" "$@" +} + +assert_log_line() { + local expected="$1" + local description="$2" + + grep -Fx -- "$expected" "$log_file" >/dev/null || fail "$description" "$(cat "$log_file")" + pass "$description" +} + +assert_numbered_log_line() { + local number="$1" + local expected="$2" + local description="$3" + local actual="" + + actual=$(sed -n "${number}p" "$log_file") + [[ $actual == $expected ]] || fail "$description" "$(cat "$log_file")" + pass "$description" +} + +run_channel stable +assert_log_line $'refresh\tstable' "stable refreshes the stable pacman channel" +assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages" +assert_log_line 'unlink' "stable restores the package-backed Omarchy path" +assert_log_line $'update\t-y' "stable runs the normal update pipeline" + +run_channel dev +assert_log_line $'refresh\tedge' "dev refreshes the edge pacman channel" +assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages" +assert_log_line 'unlink' "dev remains package-backed" + +checkout="$test_tmp/live-checkout" +default_checkout="$test_tmp/home/Work/omarchy" +OMARCHY_TEST_GUM_INPUT="$checkout" run_channel live-dev +assert_numbered_log_line 1 $'gum\tconfirm\t--default=false\tEnable Live Dev anyway?' "live dev warns before changing packages" +assert_numbered_log_line 2 $'gum\tinput\t--value\t'"$default_checkout"$'\t--placeholder\t'"$default_checkout"$'\t--header\tWhere should Live Dev checkout live? Existing non-checkout paths will not be overwritten.' "live dev prompts for the checkout path before changing packages" +assert_log_line $'gum\tconfirm\t--default=false\tEnable Live Dev anyway?' "live dev asks for confirmation" +assert_log_line $'refresh\tedge' "live dev refreshes the edge pacman channel" +assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "live dev installs development Omarchy packages" +assert_log_line $'git\tclone\t--branch\tquattro\t--single-branch\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "live dev clones the quattro checkout" +assert_log_line $'link\t'"$checkout" "live dev links the source checkout" + +occupied_checkout="$test_tmp/occupied" +mkdir -p "$occupied_checkout" +if OMARCHY_TEST_GUM_INPUT="$occupied_checkout" run_channel live-dev >"$test_tmp/occupied.out" 2>"$test_tmp/occupied.err"; then + fail "live dev refuses to use an occupied non-checkout path" +fi + +grep -q "already exists and is not a git checkout" "$test_tmp/occupied.err" || fail "live dev explains occupied checkout paths" "$(cat "$test_tmp/occupied.err")" +if grep -Fx $'refresh\tedge' "$log_file" >/dev/null; then + fail "live dev validates checkout path before changing packages" "$(cat "$log_file")" +fi +pass "live dev refuses occupied non-checkout paths before package changes" + +if run_channel edge >"$test_tmp/edge.out" 2>"$test_tmp/edge.err"; then + fail "edge is no longer accepted as a channel" +fi + +grep -q "Unknown channel: edge" "$test_tmp/edge.err" || fail "edge rejection explains the invalid channel" "$(cat "$test_tmp/edge.err")" +pass "edge is no longer accepted as a channel" + +current_channel() { + OMARCHY_TEST_VERSION_CHANNEL="$1" \ + OMARCHY_TEST_PACKAGES="$2" \ + OMARCHY_CONFIG_FILE="${3:-/dev/null}" \ + OMARCHY_PATH="${4:-/usr/share/omarchy}" \ + PATH="$stub_bin:$ROOT/bin:$PATH" \ + "$ROOT/bin/omarchy-channel-current" +} + +[[ $(current_channel stable stable) == "stable" ]] || fail "current channel detects stable" +pass "current channel detects stable" + +[[ $(current_channel rc dev) == "rc" ]] || fail "current channel detects rc" +pass "current channel detects rc" + +[[ $(current_channel edge dev /dev/null /usr/share/omarchy) == "dev" ]] || fail "current channel detects package-backed dev" +pass "current channel detects package-backed dev" + +[[ $(current_channel edge dev /dev/null "$test_tmp/live-checkout") == "live-dev" ]] || fail "current channel detects live dev from OMARCHY_PATH" +pass "current channel detects live dev from OMARCHY_PATH" + +conf="$test_tmp/omarchy.conf" +printf 'export OMARCHY_PATH="%s"\n' "$test_tmp/live-checkout" >"$conf" +[[ $(current_channel edge dev "$conf" /usr/share/omarchy) == "live-dev" ]] || fail "current channel detects live dev from configured path" +pass "current channel detects live dev from configured path"