Compare commits
3
Commits
a973ae49ef
...
ca64d4b9b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca64d4b9b6 | ||
|
|
b28edcd01f | ||
|
|
9c574f9eb7 |
@@ -29,7 +29,11 @@ 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.
|
||||||
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,
|
resize_on_border = true,
|
||||||
|
|||||||
@@ -386,6 +386,26 @@ 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.
|
||||||
|
|||||||
@@ -18,36 +18,47 @@ 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: "#222436"
|
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
|
||||||
readonly property color bgDark: "#1e2030"
|
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
|
||||||
readonly property color bgHighlight: "#2f334d"
|
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
|
||||||
readonly property color bgPanel: "#2e2f3d" // dock glass surface
|
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
|
||||||
readonly property color bgPopover: "#21212f" // Openbar submenu background
|
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
|
||||||
|
|
||||||
readonly property color fg: "#c8d3f5"
|
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
|
||||||
readonly property color fgDim: "#828bb8"
|
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
|
||||||
readonly property color fgMuted: "#636da6"
|
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||||
readonly property color gutter: "#3b4261"
|
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||||
|
|
||||||
// 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: "#82aaff" // blue
|
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
||||||
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
|
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
||||||
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
|
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||||
readonly property color cyan: "#86e1fc"
|
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||||
readonly property color teal: "#4fd6be"
|
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||||
readonly property color green: "#c3e88d"
|
readonly property color green: root.dark ? "#c3e88d" : "#587539"
|
||||||
readonly property color yellow: "#ffc777"
|
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
|
||||||
readonly property color orange: "#ff966c"
|
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
|
||||||
readonly property color red: "#ff757f"
|
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
|
||||||
readonly property color redDeep: "#c53b53"
|
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
|
||||||
readonly property color magenta: "#c099ff"
|
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
|
||||||
readonly property color pink: "#fca7ea"
|
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
|
||||||
|
|
||||||
// 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
|
||||||
@@ -59,11 +70,14 @@ 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.
|
||||||
readonly property real dockAlpha: 0.34
|
// Light surfaces need more opacity for the same sense of a solid panel: the
|
||||||
readonly property real popoverAlpha: 0.92
|
// same 0.34 that reads as glass over a dark desktop reads as haze over a
|
||||||
readonly property real overlayAlpha: 0.55
|
// light one, and text on it stops being legible.
|
||||||
readonly property real hoverAlpha: 0.14
|
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
|
||||||
readonly property real activeAlpha: 0.24
|
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 ────────────────────────────────────────────────────────────
|
// ── Geometry ────────────────────────────────────────────────────────────
|
||||||
readonly property int barHeight: 36
|
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 {
|
SettingsCard {
|
||||||
title: "Typography"
|
title: "Typography"
|
||||||
subtitle: Fonts.lastError !== ""
|
subtitle: Fonts.lastError !== ""
|
||||||
|
|||||||
@@ -50,10 +50,13 @@ 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: "#2b3050" }
|
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" }
|
||||||
GradientStop { position: 1.0; color: "#241f33" }
|
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -203,7 +206,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("#15161e", 0.85)
|
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45)
|
||||||
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)
|
||||||
|
|||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# One stable command boundary for launcher actions. Vicinae scripts stay tiny,
|
||||||
|
# while the actual mapping to Quickshell IPC remains testable in one place.
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
osd_command=("$helper_dir/panama-osd")
|
||||||
|
gallery_command=("$helper_dir/panama-prism-gallery")
|
||||||
|
|
||||||
|
report_failure() {
|
||||||
|
local status=$?
|
||||||
|
trap - EXIT
|
||||||
|
if (( status != 0 )) && command -v notify-send >/dev/null 2>&1; then
|
||||||
|
notify-send -a Panama -i dialog-error-symbolic \
|
||||||
|
"Panama action failed" \
|
||||||
|
"The “${action//-/ }” action could not be completed." || true
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap report_failure EXIT
|
||||||
|
|
||||||
|
show_state() {
|
||||||
|
local state="$1" on_icon="$2" off_icon="$3" label="$4"
|
||||||
|
case "$state" in
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
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() {
|
||||||
|
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
|
||||||
|
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 ;;
|
||||||
|
calendar) qs ipc call calendar-agenda open ;;
|
||||||
|
clipboard) qs ipc call clipboard open ;;
|
||||||
|
overview) qs ipc call overview open ;;
|
||||||
|
settings) qs ipc call settings open ;;
|
||||||
|
|
||||||
|
dnd)
|
||||||
|
state="$(qs ipc call notifications dnd)"
|
||||||
|
show_state "$state" notifications-disabled-symbolic notifications-symbolic "Do Not Disturb"
|
||||||
|
;;
|
||||||
|
caffeine)
|
||||||
|
toggle_state caffeine weather-clear-symbolic weather-clear-night-symbolic Caffeine
|
||||||
|
;;
|
||||||
|
night-light)
|
||||||
|
toggle_state night-light night-light-symbolic night-light-disabled-symbolic "Night Light"
|
||||||
|
;;
|
||||||
|
focus-start)
|
||||||
|
state="$(qs ipc call focus start)"
|
||||||
|
[[ $state == true ]]
|
||||||
|
PANAMA_OSD_STRICT=1 "${osd_command[@]}" message preferences-system-time-symbolic "Focus Session Started"
|
||||||
|
;;
|
||||||
|
focus-end)
|
||||||
|
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) PANAMA_OSD_STRICT=1 "${osd_command[@]}" microphone toggle ;;
|
||||||
|
|
||||||
|
gallery) "${gallery_command[@]}" ;;
|
||||||
|
restart-shell)
|
||||||
|
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' \
|
||||||
|
'control-center|notifications|calendar|clipboard|overview|settings|dnd|caffeine|night-light|focus-start|focus-end|capture|intelligence|screenshot|microphone|gallery|restart-shell' >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -2,12 +2,22 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
strict_delivery() {
|
||||||
|
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
show_progress() {
|
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() {
|
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() {
|
volume_state() {
|
||||||
@@ -100,8 +110,18 @@ case "${1:-}" in
|
|||||||
microphone) shift; adjust_microphone "$@" ;;
|
microphone) shift; adjust_microphone "$@" ;;
|
||||||
brightness) shift; adjust_brightness "$@" ;;
|
brightness) shift; adjust_brightness "$@" ;;
|
||||||
media) shift; media_action "$@" ;;
|
media) shift; media_action "$@" ;;
|
||||||
|
message)
|
||||||
|
shift
|
||||||
|
kind="${1:-}"
|
||||||
|
[[ -n $kind && $# -ge 2 ]] || {
|
||||||
|
printf 'Usage: panama-osd message ICON LABEL\n' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
shift
|
||||||
|
show_message "$kind" "$*"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
printf 'Usage: panama-osd volume|microphone|brightness|media ACTION [step]\n' >&2
|
printf 'Usage: panama-osd volume|microphone|brightness|media|message ACTION [value]\n' >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ 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
|
||||||
@@ -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
|
// 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 {
|
||||||
@@ -118,7 +135,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!root.initialized)
|
if (!root.initialized || root.suppressStatusEvents)
|
||||||
return;
|
return;
|
||||||
StatusEvents.publish({
|
StatusEvents.publish({
|
||||||
key: "display-night-light",
|
key: "display-night-light",
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ ShellRoot {
|
|||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "focus"
|
target: "focus"
|
||||||
|
|
||||||
function start(): void { FocusSession.startDefault(); }
|
function start(): bool {
|
||||||
|
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(); }
|
||||||
@@ -123,7 +126,12 @@ ShellRoot {
|
|||||||
ShellState.openOverview(FocusSession.workspaceId);
|
ShellState.openOverview(FocusSession.workspaceId);
|
||||||
FocusSession.dismiss();
|
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 {
|
function status(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
active: FocusSession.active,
|
active: FocusSession.active,
|
||||||
@@ -257,6 +265,38 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small stateful controls used by launcher commands. Returning the state
|
||||||
|
// after mutation lets callers give accurate OSD feedback without keeping a
|
||||||
|
// second copy of service state in a shell script.
|
||||||
|
IpcHandler {
|
||||||
|
target: "caffeine"
|
||||||
|
function toggle(): bool {
|
||||||
|
Caffeine.toggle();
|
||||||
|
return Caffeine.enabled;
|
||||||
|
}
|
||||||
|
function status(): bool { return Caffeine.enabled; }
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "night-light"
|
||||||
|
function toggle(): bool {
|
||||||
|
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 {
|
IpcHandler {
|
||||||
target: "kdeconnect"
|
target: "kdeconnect"
|
||||||
function fixture(name: string): void { KdeConnect.applyFixture(name); }
|
function fixture(name: string): void { KdeConnect.applyFixture(name); }
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Screenshot and Recording
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open the polished screenshot and screen-recording picker.
|
||||||
|
# @vicinae.keywords ["capture", "record", "selection", "window", "screen"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" capture
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: End Focus Session
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description End the current focus timer and restore its previous states.
|
||||||
|
# @vicinae.keywords ["stop timer", "deep work", "pomodoro", "restore"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" focus-end
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Calendar
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open the calendar, synchronized agenda, and ongoing activity.
|
||||||
|
# @vicinae.keywords ["agenda", "events", "appointments", "date", "nextcloud"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" calendar
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Clipboard
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open Panama's compact clipboard history surface.
|
||||||
|
# @vicinae.keywords ["copy", "paste", "history", "text"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" clipboard
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Control Center
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open Wi-Fi, Bluetooth, audio, phone, and Home controls.
|
||||||
|
# @vicinae.keywords ["quick settings", "wifi", "bluetooth", "audio", "home", "phone"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" control-center
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Mission Control
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description See every workspace, tiled window, and scratchpad window.
|
||||||
|
# @vicinae.keywords ["overview", "workspaces", "windows", "expose", "scratchpad"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" overview
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Notifications
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open notification history and the unified date center.
|
||||||
|
# @vicinae.keywords ["alerts", "messages", "history", "date center"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" notifications
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Prism Gallery
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Open the living gallery of Panama's production components.
|
||||||
|
# @vicinae.keywords ["design system", "theme", "components", "tokyo night", "style"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" gallery
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Open Settings
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Configure the Panama desktop from its native Settings app.
|
||||||
|
# @vicinae.keywords ["preferences", "configure", "desktop", "system"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" settings
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Capture Entire Screen
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Save an immediate screenshot of the complete desktop.
|
||||||
|
# @vicinae.keywords ["quick screenshot", "full screen", "capture", "grim"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" screenshot
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Restart Desktop Shell
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Restart Quickshell if a desktop surface becomes unresponsive.
|
||||||
|
# @vicinae.keywords ["reload", "repair", "quickshell", "restart", "recover"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" restart-shell
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Screen Intelligence
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Select part of the screen for OCR, QR, or code recognition.
|
||||||
|
# @vicinae.keywords ["ocr", "text", "qr", "barcode", "recognize", "scan"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" intelligence
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Start Focus Session
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Start the configured focus timer with DND and Caffeine.
|
||||||
|
# @vicinae.keywords ["timer", "deep work", "pomodoro", "concentrate", "workspace"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" focus-start
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Toggle Caffeine
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Keep the display awake and block automatic sleep.
|
||||||
|
# @vicinae.keywords ["awake", "sleep", "inhibit", "presentation", "idle"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" caffeine
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Toggle Do Not Disturb
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Pause or resume notification banners, with OSD confirmation.
|
||||||
|
# @vicinae.keywords ["dnd", "notifications", "quiet", "silence", "alerts"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" dnd
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Toggle Microphone Mute
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Mute or unmute the default microphone with OSD feedback.
|
||||||
|
# @vicinae.keywords ["mic", "audio input", "privacy", "mute", "unmute"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" microphone
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Toggle Night Light
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Toggle the warm display color temperature.
|
||||||
|
# @vicinae.keywords ["warm", "temperature", "blue light", "display", "hyprsunset"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" night-light
|
||||||
@@ -14,12 +14,12 @@ Panama is its own desktop experience, but it should learn from strong open-sourc
|
|||||||
- `shell/plugins/osd/Osd.qml` and `OsdModel.js`: inspired the separation between a pure presentation model and a single, click-through per-monitor OSD surface. Panama rebuilt the behavior around freedesktop symbolic icons, Tokyo Night Moon tokens, its Prism glass material, Hyprland's focused monitor, and the existing `wpctl`/`brightnessctl`/`playerctl` keybindings.
|
- `shell/plugins/osd/Osd.qml` and `OsdModel.js`: inspired the separation between a pure presentation model and a single, click-through per-monitor OSD surface. Panama rebuilt the behavior around freedesktop symbolic icons, Tokyo Night Moon tokens, its Prism glass material, Hyprland's focused monitor, and the existing `wpctl`/`brightnessctl`/`playerctl` keybindings.
|
||||||
- `test/shell.d/osd-test.sh` and Omarchy's broader shell contract suite: reinforced Panama's use of fast model and integration contracts alongside live compositor verification.
|
- `test/shell.d/osd-test.sh` and Omarchy's broader shell contract suite: reinforced Panama's use of fast model and integration contracts alongside live compositor verification.
|
||||||
- Omarchy's small shared visual primitives: reinforced the decision to maintain `prism-gallery.qml`, which renders Panama's production widgets and Theme tokens instead of duplicating mock-only controls.
|
- Omarchy's small shared visual primitives: reinforced the decision to maintain `prism-gallery.qml`, which renders Panama's production widgets and Theme tokens instead of duplicating mock-only controls.
|
||||||
|
- `shell/plugins/menu/Menu.qml`: inspired a searchable desktop-action inventory. Panama keeps Vicinae as its one launcher and exposes native script commands backed by a single `panama-action` dispatcher, rather than adding Omarchy's separate shell menu or plugin host.
|
||||||
|
|
||||||
No Omarchy user-facing layout or styling was transplanted. Panama keeps its GNOME-derived information architecture, Prism material, Tokyo Night Moon palette, app model, and established interaction choices.
|
No Omarchy user-facing layout or styling was transplanted. Panama keeps its GNOME-derived information architecture, Prism material, Tokyo Night Moon palette, app model, and established interaction choices.
|
||||||
|
|
||||||
### Candidates still under evaluation
|
### Candidates still under evaluation
|
||||||
|
|
||||||
- A command palette for shell actions, with Vicinae remaining the application launcher.
|
|
||||||
- A plugin boundary for optional bar widgets and services once Panama has enough third-party modules to justify one.
|
- A plugin boundary for optional bar widgets and services once Panama has enough third-party modules to justify one.
|
||||||
- A small first-run health check for missing desktop dependencies and broken service integrations.
|
- A small first-run health check for missing desktop dependencies and broken service integrations.
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ 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 scripts
|
# Run each setup stage in its own process. This keeps strict-shell options and
|
||||||
for script in ~/.local/share/Panama/setup/scripts/*; do source $script; done
|
# 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
|
# Revert to normal idle settings
|
||||||
gsettings set org.gnome.desktop.screensaver lock-enabled true
|
gsettings set org.gnome.desktop.screensaver lock-enabled true
|
||||||
|
|||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Install Panama's searchable launcher actions as one narrow directory link.
|
||||||
|
# Keeping the runtime path stable lets Vicinae watch it while every command
|
||||||
|
# remains authored and reviewed in the Panama repository.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
panama_path="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
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_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" "$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"
|
||||||
|
elif [[ -e "$target_dir" ]]; then
|
||||||
|
if [[ -e "$backup_dir" ]]; then
|
||||||
|
printf 'Refusing to replace %s; backup already exists at %s\n' \
|
||||||
|
"$target_dir" "$backup_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mv "$target_dir" "$backup_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -s "$source_dir" "$target_dir"
|
||||||
|
|
||||||
|
# The server also rescans periodically, but an explicit reload makes a setup
|
||||||
|
# run deterministic. If Vicinae is not active yet, its startup scan is enough.
|
||||||
|
if command -v vicinae >/dev/null 2>&1 && vicinae ping >/dev/null 2>&1; then
|
||||||
|
vicinae cmd launch core:reload-scripts >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
@@ -47,12 +47,16 @@ 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" "$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() {
|
assert_line() {
|
||||||
@@ -91,4 +95,17 @@ run_helper media next
|
|||||||
assert_line 'playerctl <next>'
|
assert_line 'playerctl <next>'
|
||||||
assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
|
assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
|
||||||
|
|
||||||
|
: >"$log"
|
||||||
|
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'
|
printf 'osd helper contract: PASS\n'
|
||||||
|
|||||||
Executable
+207
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
dispatcher="$repo_dir/config/dot/quickshell/scripts/panama-action"
|
||||||
|
work="$(mktemp -d /tmp/panama-action.XXXXXX)"
|
||||||
|
fake_bin="$work/bin"
|
||||||
|
command_log="$work/commands.log"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Panama action contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
[[ -x "$dispatcher" ]] || fail 'dispatcher is missing or not executable'
|
||||||
|
|
||||||
|
mkdir -p "$fake_bin" "$work/home/.config/quickshell/scripts"
|
||||||
|
|
||||||
|
make_recorder() {
|
||||||
|
local command_name="$1"
|
||||||
|
cp /dev/stdin "$fake_bin/$command_name"
|
||||||
|
chmod +x "$fake_bin/$command_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_recorder qs <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'qs' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
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
|
||||||
|
|
||||||
|
make_recorder quickshell <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'quickshell' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
make_recorder panama-osd <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
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'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'panama-prism-gallery' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
if (( $# > 0 )); then
|
||||||
|
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
fi
|
||||||
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
make_recorder notify-send <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'notify-send' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||||
|
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() {
|
||||||
|
: >"$command_log"
|
||||||
|
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" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_commands() {
|
||||||
|
local expected="$1"
|
||||||
|
local actual
|
||||||
|
actual="$(cat "$command_log")"
|
||||||
|
[[ "$actual" == "$expected" ]] \
|
||||||
|
|| fail "wrong command sequence; expected [$expected], got [$actual]"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_action control-center
|
||||||
|
assert_commands 'qs <ipc> <call> <quicksettings> <open>'
|
||||||
|
|
||||||
|
run_action notifications
|
||||||
|
assert_commands 'qs <ipc> <call> <notifications> <open>'
|
||||||
|
|
||||||
|
run_action calendar
|
||||||
|
assert_commands 'qs <ipc> <call> <calendar-agenda> <open>'
|
||||||
|
|
||||||
|
run_action clipboard
|
||||||
|
assert_commands 'qs <ipc> <call> <clipboard> <open>'
|
||||||
|
|
||||||
|
run_action overview
|
||||||
|
assert_commands 'qs <ipc> <call> <overview> <open>'
|
||||||
|
|
||||||
|
run_action settings
|
||||||
|
assert_commands 'qs <ipc> <call> <settings> <open>'
|
||||||
|
|
||||||
|
run_action dnd
|
||||||
|
assert_commands $'qs <ipc> <call> <notifications> <dnd>\npanama-osd <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
||||||
|
|
||||||
|
PANAMA_ACTION_TEST_STATE=false run_action caffeine
|
||||||
|
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\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>'
|
||||||
|
|
||||||
|
run_action focus-start
|
||||||
|
assert_commands $'qs <ipc> <call> <focus> <start>\npanama-osd <message> <preferences-system-time-symbolic> <Focus Session Started>'
|
||||||
|
|
||||||
|
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>'
|
||||||
|
|
||||||
|
run_action intelligence
|
||||||
|
assert_commands 'qs <ipc> <call> <screen-intelligence> <open>'
|
||||||
|
|
||||||
|
run_action screenshot
|
||||||
|
assert_commands 'qs <ipc> <call> <capture> <screenNow>'
|
||||||
|
|
||||||
|
run_action microphone
|
||||||
|
assert_commands 'panama-osd <microphone> <toggle>'
|
||||||
|
|
||||||
|
run_action gallery
|
||||||
|
assert_commands 'panama-prism-gallery'
|
||||||
|
|
||||||
|
run_action restart-shell
|
||||||
|
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
|
||||||
|
|
||||||
|
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_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'
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Panama action IPC contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
active_target=""
|
||||||
|
original_state=""
|
||||||
|
night_light_snapshot=""
|
||||||
|
|
||||||
|
restore_target() {
|
||||||
|
local target="$1" expected="$2" current
|
||||||
|
current="$(qs ipc call "$target" status 2>/dev/null || true)"
|
||||||
|
if [[ $current != "$expected" && ( $expected == true || $expected == false ) ]]; then
|
||||||
|
qs ipc call "$target" toggle >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
exercise_toggle() {
|
||||||
|
local target="$1" before after restored
|
||||||
|
before="$(qs ipc call "$target" status)"
|
||||||
|
active_target="$target"
|
||||||
|
original_state="$before"
|
||||||
|
|
||||||
|
[[ $before == true || $before == false ]] \
|
||||||
|
|| fail "$target returned a non-boolean status: $before"
|
||||||
|
|
||||||
|
after="$(qs ipc call "$target" toggle)"
|
||||||
|
[[ $after != "$before" ]] \
|
||||||
|
|| fail "$target did not change state: before=$before after=$after"
|
||||||
|
|
||||||
|
restored="$(qs ipc call "$target" toggle)"
|
||||||
|
[[ $restored == "$before" ]] \
|
||||||
|
|| fail "$target did not restore its original state: before=$before restored=$restored"
|
||||||
|
|
||||||
|
active_target=""
|
||||||
|
original_state=""
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc="$(qs ipc show)"
|
||||||
|
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'
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
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"
|
||||||
|
vicinae_log="$work/vicinae.log"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Panama command install contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
|
||||||
|
cat >"$fake_bin/vicinae" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'vicinae' >>"$PANAMA_VICINAE_TEST_LOG"
|
||||||
|
printf ' <%s>' "$@" >>"$PANAMA_VICINAE_TEST_LOG"
|
||||||
|
printf '\n' >>"$PANAMA_VICINAE_TEST_LOG"
|
||||||
|
[[ ${1:-} == ping || ${1:-} == cmd ]]
|
||||||
|
EOF
|
||||||
|
chmod +x "$fake_bin/vicinae"
|
||||||
|
|
||||||
|
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/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" ]] \
|
||||||
|
|| fail "Vicinae reload sequence was wrong: $(cat "$vicinae_log")"
|
||||||
|
|
||||||
|
# A second setup pass must update the same narrow link without creating more
|
||||||
|
# backups or disturbing the preserved directory.
|
||||||
|
: >"$vicinae_log"
|
||||||
|
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/backups/panama.pre-panama.pre-panama" ]] \
|
||||||
|
|| fail 'second install created a duplicate backup'
|
||||||
|
|
||||||
|
printf 'Panama command install contract: PASS\n'
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
commands_dir="$repo_dir/config/local/share/vicinae/scripts"
|
||||||
|
work="$(mktemp -d /tmp/panama-commands.XXXXXX)"
|
||||||
|
dispatch_log="$work/dispatch.log"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Panama commands contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
declare -A expected=(
|
||||||
|
[open-control-center.sh]=control-center
|
||||||
|
[open-notifications.sh]=notifications
|
||||||
|
[open-calendar.sh]=calendar
|
||||||
|
[open-clipboard.sh]=clipboard
|
||||||
|
[open-mission-control.sh]=overview
|
||||||
|
[open-settings.sh]=settings
|
||||||
|
[toggle-dnd.sh]=dnd
|
||||||
|
[toggle-caffeine.sh]=caffeine
|
||||||
|
[toggle-night-light.sh]=night-light
|
||||||
|
[start-focus.sh]=focus-start
|
||||||
|
[end-focus.sh]=focus-end
|
||||||
|
[capture.sh]=capture
|
||||||
|
[screen-intelligence.sh]=intelligence
|
||||||
|
[quick-screenshot.sh]=screenshot
|
||||||
|
[toggle-microphone.sh]=microphone
|
||||||
|
[open-prism-gallery.sh]=gallery
|
||||||
|
[restart-shell.sh]=restart-shell
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$work/home/.config/quickshell/scripts"
|
||||||
|
cat >"$work/home/.config/quickshell/scripts/panama-action" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"$PANAMA_COMMAND_TEST_LOG"
|
||||||
|
EOF
|
||||||
|
chmod +x "$work/home/.config/quickshell/scripts/panama-action"
|
||||||
|
|
||||||
|
[[ -d "$commands_dir" ]] || fail 'Vicinae command directory is missing'
|
||||||
|
|
||||||
|
mapfile -t actual_files < <(find "$commands_dir" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' | sort)
|
||||||
|
[[ ${#actual_files[@]} -eq ${#expected[@]} ]] \
|
||||||
|
|| fail "expected ${#expected[@]} commands, found ${#actual_files[@]}"
|
||||||
|
|
||||||
|
declare -A seen_titles=()
|
||||||
|
for script_name in "${!expected[@]}"; do
|
||||||
|
script="$commands_dir/$script_name"
|
||||||
|
[[ -x "$script" ]] || fail "$script_name is missing or not executable"
|
||||||
|
|
||||||
|
vicinae script check "$script" >/dev/null \
|
||||||
|
|| fail "$script_name is rejected by Vicinae"
|
||||||
|
|
||||||
|
title="$(sed -n 's/^# @vicinae.title //p' "$script")"
|
||||||
|
[[ $title == 'Panama: '* ]] || fail "$script_name has an ungrouped title: $title"
|
||||||
|
[[ -z ${seen_titles[$title]+x} ]] || fail "duplicate launcher title: $title"
|
||||||
|
seen_titles[$title]=1
|
||||||
|
|
||||||
|
grep -Fxq '# @vicinae.mode silent' "$script" \
|
||||||
|
|| fail "$script_name does not close quietly after success"
|
||||||
|
grep -Fq '# @vicinae.description ' "$script" \
|
||||||
|
|| fail "$script_name has no searchable description"
|
||||||
|
grep -Fq '# @vicinae.keywords ' "$script" \
|
||||||
|
|| fail "$script_name has no search vocabulary"
|
||||||
|
grep -Fxq '# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg' "$script" \
|
||||||
|
|| fail "$script_name does not use the Panama application identity"
|
||||||
|
|
||||||
|
: >"$dispatch_log"
|
||||||
|
HOME="$work/home" PANAMA_COMMAND_TEST_LOG="$dispatch_log" "$script"
|
||||||
|
dispatched="$(cat "$dispatch_log")"
|
||||||
|
[[ $dispatched == "${expected[$script_name]}" ]] \
|
||||||
|
|| fail "$script_name dispatched [$dispatched], expected [${expected[$script_name]}]"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf 'Panama commands contract: PASS (%d commands)\n' "${#expected[@]}"
|
||||||
Reference in New Issue
Block a user