Author SHA1 Message Date
Gabriel Brown 5e6dc9e533 Release stale Caffeine inhibitors 2026-08-18 07:43:15 -04:00
Gabriel Brown ca64d4b9b6 Harden Panama launcher actions 2026-08-18 07:31:11 -04:00
Gabriel Brown b28edcd01f Add Panama commands to the launcher 2026-08-18 07:31:11 -04:00
Gabriel Brown 9c574f9eb7 Add a light mode that keeps the same identity
Light is Tokyo Night Day, the palette's own light variant, rather than
one invented to merely not be dark. Both share the same hues at
different lightness, which is what lets the Prism signature survive the
switch: blue still leads into orchid, it is simply a darker blue on a
lighter ground.

Every colour token became a binding on one boolean, so flipping it
repaints the whole shell without any component needing to know it
happened. That only worked because nothing outside Theme.qml defines a
colour; the two places that did are fixed here.

Surface alphas differ by scheme. The 0.34 that reads as glass over a
dark desktop reads as haze over a light one, and text stops being
legible on it.

Two things that draw on this desktop do not read Panama's store: GTK
applications, which read gsettings, and the compositor, which draws
window borders. A toolbar or a border still wearing the other scheme is
more jarring than either scheme on its own, so ColorScheme pushes the
choice to both. It also pushes at startup, since a scheme chosen in a
previous session would otherwise be in effect only for the shell.

The unfocused window border follows too. It is a flat neutral, and a
dark neutral is invisible against a light desktop. The focused border is
the prism gradient and needs no variant.

The live preview follows the scheme as well. A preview that stayed dark
while the shell around it went light did not read as "your desktop", it
read as a screenshot of someone else's.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 07:20:35 -04:00
17 changed files with 452 additions and 70 deletions
+5 -1
View File
@@ -29,7 +29,11 @@ hl.config({
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
-- Unfocused windows get no colour at all. The gradient only means
-- something if exactly one window on screen is wearing it.
inactive_border = "rgba(3b426199)",
-- Follows the colour scheme: a dark neutral is invisible against a
-- light desktop. services/ColorScheme.qml applies changes live;
-- this is the value a fresh session starts from.
inactive_border = prefs.get("colorScheme", "dark") == "light"
and "rgba(a8aecb99)" or "rgba(3b426199)",
},
resize_on_border = true,
@@ -386,6 +386,26 @@ Singleton {
detail: "A time earlier than the start simply means the next morning"
},
// ── Colour scheme ───────────────────────────────────────────────────
// Light is Tokyo Night Day, the official light variant, rather than a
// palette invented to merely not be dark. Both share the same hues at
// different lightness, which is what keeps the Prism identity intact
// across the switch.
//
// services/ColorScheme.qml pushes the choice to GTK and to the
// compositor's border colours, because an application toolbar or a
// window border still wearing the other scheme is more jarring than
// either scheme on its own.
{
key: "colorScheme", type: "enum", def: "dark", group: "appearance",
label: "Appearance",
detail: "Light and dark share one identity, not two themes",
options: [
{ value: "dark", label: "Dark" },
{ value: "light", label: "Light" }
]
},
// ── Typography ──────────────────────────────────────────────────────
// The single largest thing in this desktop that used to be changeable
// only by editing Theme.qml.
+40 -26
View File
@@ -18,36 +18,47 @@ import QtQuick
Singleton {
id: root
// ── Colour scheme ───────────────────────────────────────────────────────
// Tokyo Night ships an official light variant (Day), so light mode is that
// rather than a palette invented to merely not be dark. The two share the
// same hues at different lightness, which is what lets the Prism identity
// survive the switch: blue still leads into orchid, it is simply a darker
// blue on a lighter ground.
//
// Every token below is a binding on this, so flipping it repaints the whole
// shell without anything needing to know it happened.
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
// ── Palette ─────────────────────────────────────────────────────────────
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
readonly property color bg: "#222436"
readonly property color bgDark: "#1e2030"
readonly property color bgHighlight: "#2f334d"
readonly property color bgPanel: "#2e2f3d" // dock glass surface
readonly property color bgPopover: "#21212f" // Openbar submenu background
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
readonly property color fg: "#c8d3f5"
readonly property color fgDim: "#828bb8"
readonly property color fgMuted: "#636da6"
readonly property color gutter: "#3b4261"
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
// The pair. `accent` is the primary and carries every state meaning
// (focused, active, on). `accentSecondary` is the orchid from the tmux
// theme — it never appears alone, only as the far end of a gradient. That
// restraint is the whole point: the two colours meeting is the signature,
// so the pink stops being special the moment it's used as a flat fill.
readonly property color accent: "#82aaff" // blue
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
readonly property color cyan: "#86e1fc"
readonly property color teal: "#4fd6be"
readonly property color green: "#c3e88d"
readonly property color yellow: "#ffc777"
readonly property color orange: "#ff966c"
readonly property color red: "#ff757f"
readonly property color redDeep: "#c53b53"
readonly property color magenta: "#c099ff"
readonly property color pink: "#fca7ea"
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
readonly property color green: root.dark ? "#c3e88d" : "#587539"
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
// Semantic aliases — prefer these in widgets so intent survives a repaint.
readonly property color ok: green
@@ -59,11 +70,14 @@ Singleton {
// The dock sits on compositor blur, so its fill is deliberately very
// translucent. Popovers are near-opaque because they carry text that must
// stay legible. The bar itself has no surface or alpha token.
readonly property real dockAlpha: 0.34
readonly property real popoverAlpha: 0.92
readonly property real overlayAlpha: 0.55
readonly property real hoverAlpha: 0.14
readonly property real activeAlpha: 0.24
// Light surfaces need more opacity for the same sense of a solid panel: the
// same 0.34 that reads as glass over a dark desktop reads as haze over a
// light one, and text on it stops being legible.
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
readonly property real overlayAlpha: root.dark ? 0.55 : 0.40
readonly property real hoverAlpha: root.dark ? 0.14 : 0.10
readonly property real activeAlpha: root.dark ? 0.24 : 0.18
// ── Geometry ────────────────────────────────────────────────────────────
readonly property int barHeight: 36
@@ -73,6 +73,15 @@ SettingsPage {
}
}
SettingsCard {
title: "Colour scheme"
subtitle: ColorScheme.lastError !== ""
? ColorScheme.lastError
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
ChoiceRow { setting: "colorScheme"; divider: false }
}
SettingsCard {
title: "Typography"
subtitle: Fonts.lastError !== ""
@@ -50,10 +50,13 @@ Rectangle {
// Stands in for the wallpaper. Static: an animated gradient here would
// repaint forever behind a settings page.
// Follows the colour scheme. A preview that stays dark while the shell
// around it is light does not read as "your desktop" -- it reads as a
// screenshot of someone else's.
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "#2b3050" }
GradientStop { position: 1.0; color: "#241f33" }
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" }
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" }
}
// The bar, so the preview reads as this desktop and not a generic one.
@@ -203,7 +206,7 @@ Rectangle {
shadowEnabled: true
shadowColor: win.focused && root.glowOn
? Theme.alpha(Theme.accent, 0.5)
: Theme.alpha("#15161e", 0.85)
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45)
shadowBlur: win.focused && root.glowOn
? Math.min(1.0, root.px(root.glowRange) / 12)
: Math.min(1.0, root.px(root.shadowRange) / 24)
+78 -19
View File
@@ -7,18 +7,13 @@ set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
action="${1:-}"
helper_dir="${PANAMA_ACTION_HELPER_DIR:-$script_dir}"
confirm_attempts="${PANAMA_ACTION_CONFIRM_ATTEMPTS:-20}"
confirm_delay="${PANAMA_ACTION_CONFIRM_DELAY:-0.1}"
kill_command="${PANAMA_ACTION_KILL_COMMAND:-/usr/bin/kill}"
if command -v panama-osd >/dev/null 2>&1; then
osd_command=(panama-osd)
else
osd_command=("$script_dir/panama-osd")
fi
if command -v panama-prism-gallery >/dev/null 2>&1; then
gallery_command=(panama-prism-gallery)
else
gallery_command=("$script_dir/panama-prism-gallery")
fi
osd_command=("$helper_dir/panama-osd")
gallery_command=("$helper_dir/panama-prism-gallery")
report_failure() {
local status=$?
@@ -35,8 +30,8 @@ trap report_failure EXIT
show_state() {
local state="$1" on_icon="$2" off_icon="$3" label="$4"
case "$state" in
true) "${osd_command[@]}" message "$on_icon" "$label On" ;;
false) "${osd_command[@]}" message "$off_icon" "$label Off" ;;
true) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$on_icon" "$label On" ;;
false) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$off_icon" "$label Off" ;;
*)
printf 'Panama action returned an invalid state: %s\n' "$state" >&2
return 1
@@ -44,12 +39,69 @@ show_state() {
esac
}
state_is_active() {
local target="$1"
case "$target" in
caffeine)
systemd-inhibit --list --no-pager --no-legend 2>/dev/null \
| awk '$1 == "Panama" && $(NF - 1) == "Caffeine" && $NF == "block" { found = 1 } END { exit !found }'
;;
night-light)
pgrep -u "$(id -u)" -x hyprsunset >/dev/null 2>&1
;;
*) return 2 ;;
esac
}
release_caffeine_inhibitors() {
local inhibitors pid uid
uid="$(id -u)"
inhibitors="$(systemd-inhibit --list --no-pager --no-legend 2>/dev/null \
| awk -v uid="$uid" '$1 == "Panama" && $2 == uid && $(NF - 1) == "Caffeine" && $NF == "block" { print $4 }')"
while IFS= read -r pid; do
[[ $pid =~ ^[0-9]+$ ]] || continue
"$kill_command" "$pid" 2>/dev/null || true
done <<<"$inhibitors"
}
wait_for_confirmed_state() {
local target="$1" expected="$2" attempt
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
if [[ $expected == true ]] && state_is_active "$target"; then
return 0
fi
if [[ $expected == false ]] && ! state_is_active "$target"; then
return 0
fi
sleep "$confirm_delay"
done
printf 'Panama action could not confirm %s reached state %s\n' "$target" "$expected" >&2
return 1
}
toggle_state() {
local target="$1" on_icon="$2" off_icon="$3" label="$4" state
state="$(qs ipc call "$target" toggle)"
case "$state" in true|false) ;; *) show_state "$state" "$on_icon" "$off_icon" "$label"; return ;; esac
if [[ $target == caffeine && $state == false ]]; then
release_caffeine_inhibitors
fi
wait_for_confirmed_state "$target" "$state"
show_state "$state" "$on_icon" "$off_icon" "$label"
}
wait_for_shell() {
local attempt
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
if qs ipc show >/dev/null 2>&1; then
return 0
fi
sleep "$confirm_delay"
done
printf 'Panama shell did not become ready after restart\n' >&2
return 1
}
case "$action" in
control-center) qs ipc call quicksettings open ;;
notifications) qs ipc call notifications open ;;
@@ -69,23 +121,30 @@ case "$action" in
toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light"
;;
focus-start)
qs ipc call focus start
"${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
state="$(qs ipc call focus start)"
[[ $state == true ]]
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
;;
focus-end)
qs ipc call focus end
"${osd_command[@]}" message media-playback-stop-symbolic "Focus Session Ended"
state="$(qs ipc call focus end)"
if [[ $state == true ]]; then
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message media-playback-stop-symbolic "Focus Session Ended"
else
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message dialog-information-symbolic "No Focus Session Running"
fi
;;
capture) qs ipc call capture open ;;
intelligence) qs ipc call screen-intelligence open ;;
screenshot) qs ipc call capture screenNow ;;
microphone) "${osd_command[@]}" microphone toggle ;;
microphone) PANAMA_OSD_STRICT=1 "${osd_command[@]}" microphone toggle ;;
gallery) "${gallery_command[@]}" ;;
restart-shell)
qs kill
qs kill >/dev/null 2>&1 || true
quickshell --daemonize
wait_for_shell
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message view-refresh-symbolic "Panama Restarted"
;;
*)
printf 'Usage: panama-action {%s}\n' \
+12 -2
View File
@@ -2,12 +2,22 @@
set -u
strict_delivery() {
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
}
show_progress() {
qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1 || true
if ! qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1; then
strict_delivery && return 1
fi
return 0
}
show_message() {
qs ipc call osd message "$1" "$2" >/dev/null 2>&1 || true
if ! qs ipc call osd message "$1" "$2" >/dev/null 2>&1; then
strict_delivery && return 1
fi
return 0
}
volume_state() {
@@ -0,0 +1,97 @@
pragma Singleton
// Keeps everything that draws a window agreeing about light or dark.
//
// The shell repaints itself from Theme.qml the moment the preference changes,
// because every token there is a binding. Two other things draw on this desktop
// and do not read Panama's store:
//
// GTK applications read gsettings colour-scheme and gtk-theme
// the compositor draws window borders and shadows
//
// A toolbar or a window border still wearing the other scheme is more jarring
// than either scheme on its own, which is why this exists rather than the
// setting simply being a Theme property.
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
Singleton {
id: root
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
property string lastError: ""
// Applied one command at a time: Process runs a single command, and several
// of these are separate programs.
property var pending: []
Process {
id: runner
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
root.lastError = "The colour scheme could not be applied everywhere.";
root.drain();
}
}
function drain(): void {
if (runner.running || root.pending.length === 0)
return;
const next = root.pending[0];
root.pending = root.pending.slice(1);
runner.exec(next);
}
function enqueue(commands: var): void {
root.pending = root.pending.concat(commands);
root.drain();
}
// Pushed once at startup as well as on change: gsettings and the compositor
// do not read Panama's store, so a scheme chosen in a previous session is
// only in effect for the shell until this runs.
Component.onCompleted: settle.restart()
Timer {
id: settle
interval: 1200
onTriggered: root.apply()
}
Connections {
target: DesktopPreferences
function onRevisionChanged(): void { coalesce.restart(); }
}
Timer {
id: coalesce
interval: 250
onTriggered: root.apply()
}
function apply(): void {
root.lastError = "";
const scheme = root.dark ? "prefer-dark" : "prefer-light";
// Adwaita's light and dark are the same theme; only the preference and
// the -dark suffix differ, so applications that honour either agree.
const gtkTheme = root.dark ? "Adwaita-dark" : "Adwaita";
const commands = [
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
];
// Unfocused window borders. The focused border is the prism gradient and
// is already scheme-independent; the inactive one is a flat neutral that
// would be invisible against the opposite background.
const inactive = root.dark ? "rgba(3b426199)" : "rgba(a8aecb99)";
commands.push(["hyprctl", "eval",
`hl.config({ general = { col = { inactive_border = "${inactive}" } } })`]);
root.enqueue(commands);
}
}
+18 -1
View File
@@ -26,6 +26,7 @@ Singleton {
id: root
property bool initialized: false
property bool suppressStatusEvents: false
// The manual switch. Ignored while `automatic` is on.
property bool enabled: Settings.nightLightEnabledByDefault
@@ -53,6 +54,22 @@ Singleton {
}
}
// Launcher actions already provide immediate Prism OSD feedback. Keep the
// ambient Signal Glass channel quiet for that path so one choice produces
// one confirmation instead of two competing transients.
function toggleQuietly(): void {
root.suppressStatusEvents = true;
root.toggle();
root.suppressStatusEvents = false;
}
function restore(manualEnabled: bool, automaticEnabled: bool): void {
root.suppressStatusEvents = true;
root.enabled = manualEnabled;
root.automatic = automaticEnabled;
root.suppressStatusEvents = false;
}
// The window wraps midnight (17:00 → 10:00), so the comparison flips when
// `from` is later in the day than `to`.
function inWindow(hour: real): bool {
@@ -118,7 +135,7 @@ Singleton {
}
onActiveChanged: {
if (!root.initialized)
if (!root.initialized || root.suppressStatusEvents)
return;
StatusEvents.publish({
key: "display-night-light",
+22 -3
View File
@@ -114,7 +114,10 @@ ShellRoot {
IpcHandler {
target: "focus"
function start(): void { FocusSession.startDefault(); }
function start(): bool {
FocusSession.startDefault();
return FocusSession.active;
}
function reveal(): void { FocusSession.reveal(); }
function dismiss(): void { FocusSession.dismiss(); }
function pause(): void { FocusSession.pauseOrResume(); }
@@ -123,7 +126,12 @@ ShellRoot {
ShellState.openOverview(FocusSession.workspaceId);
FocusSession.dismiss();
}
function end(): void { FocusSession.end(false); }
function end(): bool {
if (!FocusSession.active)
return false;
FocusSession.end(false);
return !FocusSession.active;
}
function status(): string {
return JSON.stringify({
active: FocusSession.active,
@@ -272,10 +280,21 @@ ShellRoot {
IpcHandler {
target: "night-light"
function toggle(): bool {
NightLight.toggle();
NightLight.toggleQuietly();
return NightLight.active;
}
function status(): bool { return NightLight.active; }
function settings(): string {
return JSON.stringify({
enabled: NightLight.enabled,
automatic: NightLight.automatic,
active: NightLight.active
});
}
function restore(enabled: bool, automatic: bool): bool {
NightLight.restore(enabled, automatic);
return NightLight.active;
}
}
IpcHandler {
+3 -2
View File
@@ -21,8 +21,9 @@ fi
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
# Run scripts
for script in ~/.local/share/Panama/setup/scripts/*; do source $script; done
# Run each setup stage in its own process. This keeps strict-shell options and
# helper variables local to the script that owns them.
for script in ~/.local/share/Panama/setup/scripts/*; do "$script"; done
# Revert to normal idle settings
gsettings set org.gnome.desktop.screensaver lock-enabled true
-5
View File
@@ -88,11 +88,6 @@ else
log "Linked Tokyo Night Moon theme → $VICINAE_THEME"
fi
# Install the Panama command collection into Vicinae's XDG data directory.
# This is intentionally separate from ~/.config/vicinae: script commands are
# user data in Vicinae 0.26, just like custom themes.
"$PANAMA_PATH/setup/scripts/link-vicinae-scripts"
# Panama-native applications live in the user data directory so launchers can
# discover them alongside system desktop entries. Keep each authored file in
# the repository and expose it with a narrow per-file symlink.
+15 -2
View File
@@ -11,14 +11,27 @@ vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
source_dir="$panama_path/config/local/share/vicinae/scripts"
scripts_dir="$vicinae_data_dir/scripts"
target_dir="$scripts_dir/panama"
backup_dir="$scripts_dir/panama.pre-panama"
backup_root="$vicinae_data_dir/backups"
backup_dir="$backup_root/panama.pre-panama"
legacy_backup="$scripts_dir/panama.pre-panama"
[[ -d "$source_dir" ]] || {
printf 'Panama command source is missing: %s\n' "$source_dir" >&2
exit 1
}
mkdir -p "$scripts_dir"
mkdir -p "$scripts_dir" "$backup_root"
# Older Panama builds preserved this directory alongside searchable commands.
# Move it out before reloading so those scripts cannot appear twice.
if [[ -e "$legacy_backup" ]]; then
if [[ -e "$backup_dir" ]]; then
printf 'Refusing to move %s; backup already exists at %s\n' \
"$legacy_backup" "$backup_dir" >&2
exit 1
fi
mv "$legacy_backup" "$backup_dir"
fi
if [[ -L "$target_dir" ]]; then
rm "$target_dir"
+14 -1
View File
@@ -47,12 +47,16 @@ cat >"$scratch/bin/qs" <<'SH'
printf 'qs' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
[[ ${OSD_TEST_FAIL_QS:-false} != true ]]
SH
chmod +x "$scratch/bin/"*
run_helper() {
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" "$helper" "$@"
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
"$helper" "$@"
}
assert_line() {
@@ -95,4 +99,13 @@ assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
run_helper message notifications-disabled-symbolic 'Do Not Disturb On'
assert_line 'qs <ipc> <call> <osd> <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
: >"$log"
if OSD_TEST_FAIL_QS=true PANAMA_OSD_STRICT=1 run_helper message dialog-error-symbolic 'Strict failure'; then
printf 'osd helper contract: strict delivery swallowed an IPC failure\n' >&2
exit 1
fi
: >"$log"
OSD_TEST_FAIL_QS=true run_helper message dialog-information-symbolic 'Best effort'
printf 'osd helper contract: PASS\n'
+79 -3
View File
@@ -37,7 +37,13 @@ case "$*" in
'ipc call notifications dnd'|'ipc call caffeine toggle'|'ipc call night-light toggle')
printf '%s\n' "${PANAMA_ACTION_TEST_STATE:-true}"
;;
'ipc call focus start'|'ipc call focus end')
printf '%s\n' "${PANAMA_ACTION_TEST_FOCUS_STATE:-true}"
;;
esac
if [[ ${PANAMA_ACTION_TEST_FAIL_KILL:-false} == true && ${1:-} == kill ]]; then
exit 1
fi
[[ ${PANAMA_ACTION_TEST_FAIL_QS:-false} != true ]]
EOF
@@ -53,6 +59,7 @@ make_recorder panama-osd <<'EOF'
printf 'panama-osd' >>"$PANAMA_ACTION_TEST_LOG"
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
[[ ${PANAMA_ACTION_TEST_FAIL_OSD:-false} != true ]]
EOF
make_recorder panama-prism-gallery <<'EOF'
@@ -71,9 +78,46 @@ printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
EOF
make_recorder systemd-inhibit <<'EOF'
#!/usr/bin/env bash
if [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} && -s $PANAMA_ACTION_TEST_INHIBITOR_FILE ]]; then
while IFS= read -r pid; do
printf 'Panama 1000 gib %s systemd-inhibit sleep:idle Caffeine block\n' "$pid"
done <"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
exit 0
fi
if [[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]; then
printf 'Panama 1000 gib 42 systemd-inhibit sleep:idle Caffeine block\n'
fi
EOF
make_recorder kill <<'EOF'
#!/usr/bin/env bash
printf 'kill' >>"$PANAMA_ACTION_TEST_LOG"
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
if [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} ]]; then
: >"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
fi
EOF
make_recorder pgrep <<'EOF'
#!/usr/bin/env bash
[[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]
EOF
make_recorder id <<'EOF'
#!/usr/bin/env bash
printf '1000\n'
EOF
run_action() {
: >"$command_log"
HOME="$work/home" PATH="$fake_bin:$PATH" \
PANAMA_ACTION_HELPER_DIR="$fake_bin" \
PANAMA_ACTION_KILL_COMMAND="$fake_bin/kill" \
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
PANAMA_ACTION_CONFIRM_DELAY=0 \
PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" "$1"
}
@@ -110,6 +154,10 @@ assert_commands $'qs <ipc> <call> <notifications> <dnd>\npanama-osd <message> <n
PANAMA_ACTION_TEST_STATE=false run_action caffeine
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
printf '4242\n' >"$work/inhibitors"
PANAMA_ACTION_TEST_STATE=false PANAMA_ACTION_TEST_INHIBITOR_FILE="$work/inhibitors" run_action caffeine
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\nkill <4242>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
run_action night-light
assert_commands $'qs <ipc> <call> <night-light> <toggle>\npanama-osd <message> <night-light-symbolic> <Night Light On>'
@@ -119,6 +167,9 @@ assert_commands $'qs <ipc> <call> <focus> <start>\npanama-osd <message> <prefere
run_action focus-end
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <media-playback-stop-symbolic> <Focus Session Ended>'
PANAMA_ACTION_TEST_FOCUS_STATE=false run_action focus-end
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <dialog-information-symbolic> <No Focus Session Running>'
run_action capture
assert_commands 'qs <ipc> <call> <capture> <open>'
@@ -135,18 +186,43 @@ run_action gallery
assert_commands 'panama-prism-gallery'
run_action restart-shell
assert_commands $'qs <kill>\nquickshell <--daemonize>'
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_TEST_LOG="$command_log" \
PANAMA_ACTION_TEST_FAIL_KILL=true run_action restart-shell
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_HELPER_DIR="$fake_bin" \
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" definitely-not-an-action >/dev/null 2>&1; then
fail 'unknown action was accepted'
fi
: >"$command_log"
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
PANAMA_ACTION_TEST_LOG="$command_log" "$dispatcher" control-center >/dev/null 2>&1; then
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" control-center >/dev/null 2>&1; then
fail 'failed shell action returned success'
fi
assert_commands $'qs <ipc> <call> <quicksettings> <open>\nnotify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “control center” action could not be completed.>'
: >"$command_log"
if PANAMA_ACTION_TEST_FAIL_OSD=true HOME="$work/home" PATH="$fake_bin:$PATH" \
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" microphone >/dev/null 2>&1; then
fail 'failed Prism OSD delivery returned success'
fi
assert_commands $'panama-osd <microphone> <toggle>\nnotify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “microphone” action could not be completed.>'
: >"$command_log"
if PANAMA_ACTION_TEST_CONFIRMED_STATE=false HOME="$work/home" PATH="$fake_bin:$PATH" \
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" caffeine >/dev/null 2>&1; then
fail 'unconfirmed Caffeine state returned success'
fi
grep -Fqx 'notify-send <-a> <Panama> <-i> <dialog-error-symbolic> <Panama action failed> <The “caffeine” action could not be completed.>' "$command_log" \
|| fail 'unconfirmed state did not send a failure notification'
printf 'Panama action contract: PASS\n'
@@ -9,6 +9,7 @@ fail() {
active_target=""
original_state=""
night_light_snapshot=""
restore_target() {
local target="$1" expected="$2" current
@@ -22,6 +23,12 @@ cleanup() {
if [[ -n $active_target ]]; then
restore_target "$active_target" "$original_state"
fi
if [[ -n $night_light_snapshot ]]; then
local enabled automatic
enabled="$(jq -r '.enabled' <<<"$night_light_snapshot")"
automatic="$(jq -r '.automatic' <<<"$night_light_snapshot")"
qs ipc call night-light restore "$enabled" "$automatic" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
@@ -51,6 +58,22 @@ rg -q '^target caffeine$' <<<"$ipc" || fail 'caffeine IPC target is missing'
rg -q '^target night-light$' <<<"$ipc" || fail 'night-light IPC target is missing'
exercise_toggle caffeine
night_light_snapshot="$(qs ipc call night-light settings)"
jq -e '.enabled | type == "boolean"' <<<"$night_light_snapshot" >/dev/null \
|| fail 'night-light settings omitted enabled state'
jq -e '.automatic | type == "boolean"' <<<"$night_light_snapshot" >/dev/null \
|| fail 'night-light settings omitted automatic state'
exercise_toggle night-light
original_enabled="$(jq -r '.enabled' <<<"$night_light_snapshot")"
original_automatic="$(jq -r '.automatic' <<<"$night_light_snapshot")"
qs ipc call night-light restore "$original_enabled" "$original_automatic" >/dev/null
restored_snapshot="$(qs ipc call night-light settings)"
[[ "$(jq -r '.enabled' <<<"$restored_snapshot")" == "$original_enabled" ]] \
|| fail 'night-light enabled state was not restored'
[[ "$(jq -r '.automatic' <<<"$restored_snapshot")" == "$original_automatic" ]] \
|| fail 'night-light automatic schedule was not restored'
night_light_snapshot=""
printf 'Panama action IPC contract: PASS\n'
@@ -4,6 +4,8 @@ set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
installer="$repo_dir/setup/scripts/link-vicinae-scripts"
dotfile_installer="$repo_dir/setup/scripts/link-dotfiles"
top_level_installer="$repo_dir/install"
work="$(mktemp -d /tmp/panama-command-install.XXXXXX)"
data_dir="$work/share/vicinae"
fake_bin="$work/bin"
@@ -20,6 +22,11 @@ cleanup() {
trap cleanup EXIT
[[ -x "$installer" ]] || fail 'Vicinae script installer is missing or not executable'
if rg -q 'setup/scripts/link-vicinae-scripts' "$dotfile_installer"; then
fail 'link-dotfiles also invokes the command installer'
fi
rg -Fq 'do "$script"; done' "$top_level_installer" \
|| fail 'top-level installer sources setup scripts into one shared shell'
mkdir -p "$data_dir/scripts/panama" "$fake_bin"
printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh"
@@ -37,12 +44,14 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
target="$data_dir/scripts/panama"
backup="$data_dir/scripts/panama.pre-panama"
backup="$data_dir/backups/panama.pre-panama"
[[ -L "$target" ]] || fail 'Panama command directory was not linked'
[[ "$(readlink -f "$target")" == "$(readlink -f "$repo_dir/config/local/share/vicinae/scripts")" ]] \
|| fail 'runtime command link points outside the tracked source'
[[ -f "$backup/keep.sh" ]] || fail 'pre-existing user command directory was not preserved'
[[ "$(cat "$backup/keep.sh")" == user-owned ]] || fail 'preserved user command changed'
[[ ! -e "$data_dir/scripts/panama.pre-panama" ]] \
|| fail 'preserved commands remained inside Vicinae search paths'
expected_calls=$'vicinae <ping>\nvicinae <cmd> <launch> <core:reload-scripts>'
[[ "$(cat "$vicinae_log")" == "$expected_calls" ]] \
@@ -55,7 +64,7 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
[[ -L "$target" ]] || fail 'second install did not leave the command link intact'
[[ -f "$backup/keep.sh" ]] || fail 'second install disturbed the original backup'
[[ ! -e "$data_dir/scripts/panama.pre-panama.pre-panama" ]] \
[[ ! -e "$data_dir/backups/panama.pre-panama.pre-panama" ]] \
|| fail 'second install created a duplicate backup'
printf 'Panama command install contract: PASS\n'