1 Commits
Author SHA1 Message Date
Gabriel Brown a973ae49ef Add Panama commands to the launcher 2026-08-18 07:12:53 -04:00
17 changed files with 70 additions and 416 deletions
+1 -5
View File
@@ -29,11 +29,7 @@ hl.config({
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 }, active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
-- Unfocused windows get no colour at all. The gradient only means -- Unfocused windows get no colour at all. The gradient only means
-- something if exactly one window on screen is wearing it. -- something if exactly one window on screen is wearing it.
-- Follows the colour scheme: a dark neutral is invisible against a inactive_border = "rgba(3b426199)",
-- 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, resize_on_border = true,
@@ -386,26 +386,6 @@ Singleton {
detail: "A time earlier than the start simply means the next morning" 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 ────────────────────────────────────────────────────── // ── Typography ──────────────────────────────────────────────────────
// The single largest thing in this desktop that used to be changeable // The single largest thing in this desktop that used to be changeable
// only by editing Theme.qml. // only by editing Theme.qml.
+26 -40
View File
@@ -18,47 +18,36 @@ import QtQuick
Singleton { Singleton {
id: root 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 ───────────────────────────────────────────────────────────── // ── Palette ─────────────────────────────────────────────────────────────
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly. // Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
readonly property color bg: root.dark ? "#222436" : "#e1e2e7" readonly property color bg: "#222436"
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de" readonly property color bgDark: "#1e2030"
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da" readonly property color bgHighlight: "#2f334d"
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface readonly property color bgPanel: "#2e2f3d" // dock glass surface
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background readonly property color bgPopover: "#21212f" // Openbar submenu background
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf" readonly property color fg: "#c8d3f5"
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0" readonly property color fgDim: "#828bb8"
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5" readonly property color fgMuted: "#636da6"
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb" readonly property color gutter: "#3b4261"
// The pair. `accent` is the primary and carries every state meaning // The pair. `accent` is the primary and carries every state meaning
// (focused, active, on). `accentSecondary` is the orchid from the tmux // (focused, active, on). `accentSecondary` is the orchid from the tmux
// theme — it never appears alone, only as the far end of a gradient. That // 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, // 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. // so the pink stops being special the moment it's used as a flat fill.
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue readonly property color accent: "#82aaff" // blue
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux readonly property color accentSecondary: "#b172b0" // orchid, from tmux
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
readonly property color cyan: root.dark ? "#86e1fc" : "#007197" readonly property color cyan: "#86e1fc"
readonly property color teal: root.dark ? "#4fd6be" : "#118c74" readonly property color teal: "#4fd6be"
readonly property color green: root.dark ? "#c3e88d" : "#587539" readonly property color green: "#c3e88d"
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e" readonly property color yellow: "#ffc777"
readonly property color orange: root.dark ? "#ff966c" : "#b15c00" readonly property color orange: "#ff966c"
readonly property color red: root.dark ? "#ff757f" : "#f52a65" readonly property color red: "#ff757f"
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343" readonly property color redDeep: "#c53b53"
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1" readonly property color magenta: "#c099ff"
readonly property color pink: root.dark ? "#fca7ea" : "#d20065" readonly property color pink: "#fca7ea"
// Semantic aliases — prefer these in widgets so intent survives a repaint. // Semantic aliases — prefer these in widgets so intent survives a repaint.
readonly property color ok: green readonly property color ok: green
@@ -70,14 +59,11 @@ Singleton {
// The dock sits on compositor blur, so its fill is deliberately very // The dock sits on compositor blur, so its fill is deliberately very
// translucent. Popovers are near-opaque because they carry text that must // translucent. Popovers are near-opaque because they carry text that must
// stay legible. The bar itself has no surface or alpha token. // stay legible. The bar itself has no surface or alpha token.
// Light surfaces need more opacity for the same sense of a solid panel: the readonly property real dockAlpha: 0.34
// same 0.34 that reads as glass over a dark desktop reads as haze over a readonly property real popoverAlpha: 0.92
// light one, and text on it stops being legible. readonly property real overlayAlpha: 0.55
readonly property real dockAlpha: root.dark ? 0.34 : 0.62 readonly property real hoverAlpha: 0.14
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97 readonly property real activeAlpha: 0.24
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 ──────────────────────────────────────────────────────────── // ── Geometry ────────────────────────────────────────────────────────────
readonly property int barHeight: 36 readonly property int barHeight: 36
@@ -73,15 +73,6 @@ 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 { SettingsCard {
title: "Typography" title: "Typography"
subtitle: Fonts.lastError !== "" subtitle: Fonts.lastError !== ""
@@ -50,13 +50,10 @@ Rectangle {
// Stands in for the wallpaper. Static: an animated gradient here would // Stands in for the wallpaper. Static: an animated gradient here would
// repaint forever behind a settings page. // 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 { gradient: Gradient {
orientation: Gradient.Vertical orientation: Gradient.Vertical
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" } GradientStop { position: 0.0; color: "#2b3050" }
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" } GradientStop { position: 1.0; color: "#241f33" }
} }
// The bar, so the preview reads as this desktop and not a generic one. // The bar, so the preview reads as this desktop and not a generic one.
@@ -206,7 +203,7 @@ Rectangle {
shadowEnabled: true shadowEnabled: true
shadowColor: win.focused && root.glowOn shadowColor: win.focused && root.glowOn
? Theme.alpha(Theme.accent, 0.5) ? Theme.alpha(Theme.accent, 0.5)
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45) : Theme.alpha("#15161e", 0.85)
shadowBlur: win.focused && root.glowOn shadowBlur: win.focused && root.glowOn
? Math.min(1.0, root.px(root.glowRange) / 12) ? Math.min(1.0, root.px(root.glowRange) / 12)
: Math.min(1.0, root.px(root.shadowRange) / 24) : Math.min(1.0, root.px(root.shadowRange) / 24)
+19 -63
View File
@@ -7,12 +7,18 @@ set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
action="${1:-}" 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}"
osd_command=("$helper_dir/panama-osd") if command -v panama-osd >/dev/null 2>&1; then
gallery_command=("$helper_dir/panama-prism-gallery") 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
report_failure() { report_failure() {
local status=$? local status=$?
@@ -29,8 +35,8 @@ trap report_failure EXIT
show_state() { show_state() {
local state="$1" on_icon="$2" off_icon="$3" label="$4" local state="$1" on_icon="$2" off_icon="$3" label="$4"
case "$state" in case "$state" in
true) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$on_icon" "$label On" ;; true) "${osd_command[@]}" message "$on_icon" "$label On" ;;
false) PANAMA_OSD_STRICT=1 "${osd_command[@]}" message "$off_icon" "$label Off" ;; false) "${osd_command[@]}" message "$off_icon" "$label Off" ;;
*) *)
printf 'Panama action returned an invalid state: %s\n' "$state" >&2 printf 'Panama action returned an invalid state: %s\n' "$state" >&2
return 1 return 1
@@ -38,55 +44,12 @@ show_state() {
esac 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
}
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() { toggle_state() {
local target="$1" on_icon="$2" off_icon="$3" label="$4" state local target="$1" on_icon="$2" off_icon="$3" label="$4" state
state="$(qs ipc call "$target" toggle)" state="$(qs ipc call "$target" toggle)"
case "$state" in true|false) ;; *) show_state "$state" "$on_icon" "$off_icon" "$label"; return ;; esac
wait_for_confirmed_state "$target" "$state"
show_state "$state" "$on_icon" "$off_icon" "$label" 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 case "$action" in
control-center) qs ipc call quicksettings open ;; control-center) qs ipc call quicksettings open ;;
notifications) qs ipc call notifications open ;; notifications) qs ipc call notifications open ;;
@@ -106,30 +69,23 @@ case "$action" in
toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light" toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light"
;; ;;
focus-start) focus-start)
state="$(qs ipc call focus start)" qs ipc call focus start
[[ $state == true ]] "${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
;; ;;
focus-end) focus-end)
state="$(qs ipc call focus end)" qs ipc call focus end
if [[ $state == true ]]; then "${osd_command[@]}" message media-playback-stop-symbolic "Focus Session Ended"
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 ;; capture) qs ipc call capture open ;;
intelligence) qs ipc call screen-intelligence open ;; intelligence) qs ipc call screen-intelligence open ;;
screenshot) qs ipc call capture screenNow ;; screenshot) qs ipc call capture screenNow ;;
microphone) PANAMA_OSD_STRICT=1 "${osd_command[@]}" microphone toggle ;; microphone) "${osd_command[@]}" microphone toggle ;;
gallery) "${gallery_command[@]}" ;; gallery) "${gallery_command[@]}" ;;
restart-shell) restart-shell)
qs kill >/dev/null 2>&1 || true qs kill
quickshell --daemonize quickshell --daemonize
wait_for_shell
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message view-refresh-symbolic "Panama Restarted"
;; ;;
*) *)
printf 'Usage: panama-action {%s}\n' \ printf 'Usage: panama-action {%s}\n' \
+2 -12
View File
@@ -2,22 +2,12 @@
set -u set -u
strict_delivery() {
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
}
show_progress() { show_progress() {
if ! qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1; then qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1 || true
strict_delivery && return 1
fi
return 0
} }
show_message() { show_message() {
if ! qs ipc call osd message "$1" "$2" >/dev/null 2>&1; then qs ipc call osd message "$1" "$2" >/dev/null 2>&1 || true
strict_delivery && return 1
fi
return 0
} }
volume_state() { volume_state() {
@@ -1,97 +0,0 @@
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);
}
}
+1 -18
View File
@@ -26,7 +26,6 @@ Singleton {
id: root id: root
property bool initialized: false property bool initialized: false
property bool suppressStatusEvents: false
// The manual switch. Ignored while `automatic` is on. // The manual switch. Ignored while `automatic` is on.
property bool enabled: Settings.nightLightEnabledByDefault property bool enabled: Settings.nightLightEnabledByDefault
@@ -54,22 +53,6 @@ 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 // The window wraps midnight (17:00 → 10:00), so the comparison flips when
// `from` is later in the day than `to`. // `from` is later in the day than `to`.
function inWindow(hour: real): bool { function inWindow(hour: real): bool {
@@ -135,7 +118,7 @@ Singleton {
} }
onActiveChanged: { onActiveChanged: {
if (!root.initialized || root.suppressStatusEvents) if (!root.initialized)
return; return;
StatusEvents.publish({ StatusEvents.publish({
key: "display-night-light", key: "display-night-light",
+3 -22
View File
@@ -114,10 +114,7 @@ ShellRoot {
IpcHandler { IpcHandler {
target: "focus" target: "focus"
function start(): bool { function start(): void { FocusSession.startDefault(); }
FocusSession.startDefault();
return FocusSession.active;
}
function reveal(): void { FocusSession.reveal(); } function reveal(): void { FocusSession.reveal(); }
function dismiss(): void { FocusSession.dismiss(); } function dismiss(): void { FocusSession.dismiss(); }
function pause(): void { FocusSession.pauseOrResume(); } function pause(): void { FocusSession.pauseOrResume(); }
@@ -126,12 +123,7 @@ ShellRoot {
ShellState.openOverview(FocusSession.workspaceId); ShellState.openOverview(FocusSession.workspaceId);
FocusSession.dismiss(); FocusSession.dismiss();
} }
function end(): bool { function end(): void { FocusSession.end(false); }
if (!FocusSession.active)
return false;
FocusSession.end(false);
return !FocusSession.active;
}
function status(): string { function status(): string {
return JSON.stringify({ return JSON.stringify({
active: FocusSession.active, active: FocusSession.active,
@@ -280,21 +272,10 @@ ShellRoot {
IpcHandler { IpcHandler {
target: "night-light" target: "night-light"
function toggle(): bool { function toggle(): bool {
NightLight.toggleQuietly(); NightLight.toggle();
return NightLight.active; return NightLight.active;
} }
function status(): bool { 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 { IpcHandler {
+2 -3
View File
@@ -21,9 +21,8 @@ fi
gsettings set org.gnome.desktop.screensaver lock-enabled false gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0 gsettings set org.gnome.desktop.session idle-delay 0
# Run each setup stage in its own process. This keeps strict-shell options and # Run scripts
# helper variables local to the script that owns them. for script in ~/.local/share/Panama/setup/scripts/*; do source $script; done
for script in ~/.local/share/Panama/setup/scripts/*; do "$script"; done
# Revert to normal idle settings # Revert to normal idle settings
gsettings set org.gnome.desktop.screensaver lock-enabled true gsettings set org.gnome.desktop.screensaver lock-enabled true
+5
View File
@@ -88,6 +88,11 @@ else
log "Linked Tokyo Night Moon theme → $VICINAE_THEME" log "Linked Tokyo Night Moon theme → $VICINAE_THEME"
fi 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 # Panama-native applications live in the user data directory so launchers can
# discover them alongside system desktop entries. Keep each authored file in # discover them alongside system desktop entries. Keep each authored file in
# the repository and expose it with a narrow per-file symlink. # the repository and expose it with a narrow per-file symlink.
+2 -15
View File
@@ -11,27 +11,14 @@ vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
source_dir="$panama_path/config/local/share/vicinae/scripts" source_dir="$panama_path/config/local/share/vicinae/scripts"
scripts_dir="$vicinae_data_dir/scripts" scripts_dir="$vicinae_data_dir/scripts"
target_dir="$scripts_dir/panama" target_dir="$scripts_dir/panama"
backup_root="$vicinae_data_dir/backups" backup_dir="$scripts_dir/panama.pre-panama"
backup_dir="$backup_root/panama.pre-panama"
legacy_backup="$scripts_dir/panama.pre-panama"
[[ -d "$source_dir" ]] || { [[ -d "$source_dir" ]] || {
printf 'Panama command source is missing: %s\n' "$source_dir" >&2 printf 'Panama command source is missing: %s\n' "$source_dir" >&2
exit 1 exit 1
} }
mkdir -p "$scripts_dir" "$backup_root" mkdir -p "$scripts_dir"
# 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 if [[ -L "$target_dir" ]]; then
rm "$target_dir" rm "$target_dir"
+1 -14
View File
@@ -47,16 +47,12 @@ cat >"$scratch/bin/qs" <<'SH'
printf 'qs' >>"$OSD_TEST_LOG" printf 'qs' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG" printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG" printf '\n' >>"$OSD_TEST_LOG"
[[ ${OSD_TEST_FAIL_QS:-false} != true ]]
SH SH
chmod +x "$scratch/bin/"* chmod +x "$scratch/bin/"*
run_helper() { run_helper() {
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \ PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" "$helper" "$@"
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
"$helper" "$@"
} }
assert_line() { assert_line() {
@@ -99,13 +95,4 @@ assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
run_helper message notifications-disabled-symbolic 'Do Not Disturb On' run_helper message notifications-disabled-symbolic 'Do Not Disturb On'
assert_line 'qs <ipc> <call> <osd> <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' printf 'osd helper contract: PASS\n'
+3 -58
View File
@@ -37,13 +37,7 @@ case "$*" in
'ipc call notifications dnd'|'ipc call caffeine toggle'|'ipc call night-light toggle') 'ipc call notifications dnd'|'ipc call caffeine toggle'|'ipc call night-light toggle')
printf '%s\n' "${PANAMA_ACTION_TEST_STATE:-true}" 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 esac
if [[ ${PANAMA_ACTION_TEST_FAIL_KILL:-false} == true && ${1:-} == kill ]]; then
exit 1
fi
[[ ${PANAMA_ACTION_TEST_FAIL_QS:-false} != true ]] [[ ${PANAMA_ACTION_TEST_FAIL_QS:-false} != true ]]
EOF EOF
@@ -59,7 +53,6 @@ make_recorder panama-osd <<'EOF'
printf 'panama-osd' >>"$PANAMA_ACTION_TEST_LOG" printf 'panama-osd' >>"$PANAMA_ACTION_TEST_LOG"
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG" printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
printf '\n' >>"$PANAMA_ACTION_TEST_LOG" printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
[[ ${PANAMA_ACTION_TEST_FAIL_OSD:-false} != true ]]
EOF EOF
make_recorder panama-prism-gallery <<'EOF' make_recorder panama-prism-gallery <<'EOF'
@@ -78,29 +71,9 @@ printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
printf '\n' >>"$PANAMA_ACTION_TEST_LOG" printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
EOF EOF
make_recorder systemd-inhibit <<'EOF'
#!/usr/bin/env bash
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 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() { run_action() {
: >"$command_log" : >"$command_log"
HOME="$work/home" PATH="$fake_bin:$PATH" \ 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" \ PANAMA_ACTION_TEST_LOG="$command_log" \
"$dispatcher" "$1" "$dispatcher" "$1"
} }
@@ -146,9 +119,6 @@ assert_commands $'qs <ipc> <call> <focus> <start>\npanama-osd <message> <prefere
run_action focus-end run_action focus-end
assert_commands $'qs <ipc> <call> <focus> <end>\npanama-osd <message> <media-playback-stop-symbolic> <Focus Session Ended>' 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 run_action capture
assert_commands 'qs <ipc> <call> <capture> <open>' assert_commands 'qs <ipc> <call> <capture> <open>'
@@ -165,43 +135,18 @@ run_action gallery
assert_commands 'panama-prism-gallery' assert_commands 'panama-prism-gallery'
run_action restart-shell run_action restart-shell
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>' assert_commands $'qs <kill>\nquickshell <--daemonize>'
PANAMA_ACTION_TEST_FAIL_KILL=true run_action restart-shell if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_TEST_LOG="$command_log" \
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 "$dispatcher" definitely-not-an-action >/dev/null 2>&1; then
fail 'unknown action was accepted' fail 'unknown action was accepted'
fi fi
: >"$command_log" : >"$command_log"
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \ if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
PANAMA_ACTION_HELPER_DIR="$fake_bin" PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \ PANAMA_ACTION_TEST_LOG="$command_log" "$dispatcher" control-center >/dev/null 2>&1; then
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' fail 'failed shell action returned success'
fi 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.>' 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' printf 'Panama action contract: PASS\n'
@@ -9,7 +9,6 @@ fail() {
active_target="" active_target=""
original_state="" original_state=""
night_light_snapshot=""
restore_target() { restore_target() {
local target="$1" expected="$2" current local target="$1" expected="$2" current
@@ -23,12 +22,6 @@ cleanup() {
if [[ -n $active_target ]]; then if [[ -n $active_target ]]; then
restore_target "$active_target" "$original_state" restore_target "$active_target" "$original_state"
fi 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 trap cleanup EXIT
@@ -58,22 +51,6 @@ rg -q '^target caffeine$' <<<"$ipc" || fail 'caffeine IPC target is missing'
rg -q '^target night-light$' <<<"$ipc" || fail 'night-light IPC target is missing' rg -q '^target night-light$' <<<"$ipc" || fail 'night-light IPC target is missing'
exercise_toggle caffeine 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 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' printf 'Panama action IPC contract: PASS\n'
@@ -4,8 +4,6 @@ set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
installer="$repo_dir/setup/scripts/link-vicinae-scripts" 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)" work="$(mktemp -d /tmp/panama-command-install.XXXXXX)"
data_dir="$work/share/vicinae" data_dir="$work/share/vicinae"
fake_bin="$work/bin" fake_bin="$work/bin"
@@ -22,11 +20,6 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
[[ -x "$installer" ]] || fail 'Vicinae script installer is missing or not executable' [[ -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" mkdir -p "$data_dir/scripts/panama" "$fake_bin"
printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh" printf 'user-owned\n' >"$data_dir/scripts/panama/keep.sh"
@@ -44,14 +37,12 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
target="$data_dir/scripts/panama" target="$data_dir/scripts/panama"
backup="$data_dir/backups/panama.pre-panama" backup="$data_dir/scripts/panama.pre-panama"
[[ -L "$target" ]] || fail 'Panama command directory was not linked' [[ -L "$target" ]] || fail 'Panama command directory was not linked'
[[ "$(readlink -f "$target")" == "$(readlink -f "$repo_dir/config/local/share/vicinae/scripts")" ]] \ [[ "$(readlink -f "$target")" == "$(readlink -f "$repo_dir/config/local/share/vicinae/scripts")" ]] \
|| fail 'runtime command link points outside the tracked source' || fail 'runtime command link points outside the tracked source'
[[ -f "$backup/keep.sh" ]] || fail 'pre-existing user command directory was not preserved' [[ -f "$backup/keep.sh" ]] || fail 'pre-existing user command directory was not preserved'
[[ "$(cat "$backup/keep.sh")" == user-owned ]] || fail 'preserved user command changed' [[ "$(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>' expected_calls=$'vicinae <ping>\nvicinae <cmd> <launch> <core:reload-scripts>'
[[ "$(cat "$vicinae_log")" == "$expected_calls" ]] \ [[ "$(cat "$vicinae_log")" == "$expected_calls" ]] \
@@ -64,7 +55,7 @@ PATH="$fake_bin:$PATH" PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_dir" \
PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null PANAMA_VICINAE_TEST_LOG="$vicinae_log" "$installer" >/dev/null
[[ -L "$target" ]] || fail 'second install did not leave the command link intact' [[ -L "$target" ]] || fail 'second install did not leave the command link intact'
[[ -f "$backup/keep.sh" ]] || fail 'second install disturbed the original backup' [[ -f "$backup/keep.sh" ]] || fail 'second install disturbed the original backup'
[[ ! -e "$data_dir/backups/panama.pre-panama.pre-panama" ]] \ [[ ! -e "$data_dir/scripts/panama.pre-panama.pre-panama" ]] \
|| fail 'second install created a duplicate backup' || fail 'second install created a duplicate backup'
printf 'Panama command install contract: PASS\n' printf 'Panama command install contract: PASS\n'