Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6dc9e533 | ||
|
|
ca64d4b9b6 | ||
|
|
b28edcd01f | ||
|
|
9c574f9eb7 | ||
|
|
d1b2cd2083 | ||
|
|
8c4dee0ca8 | ||
|
|
71ee18048e | ||
|
|
668983e16d |
@@ -30,6 +30,9 @@ local sysmonitor = "flatpak run io.missioncenter.MissionCenter"
|
||||
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
||||
-- window against the already-running server (started in autostart.lua).
|
||||
local launcher = "vicinae toggle"
|
||||
local osd = function(action)
|
||||
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
||||
end
|
||||
|
||||
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
||||
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
||||
@@ -252,23 +255,23 @@ bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { descri
|
||||
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
||||
-- 6% steps match the GNOME volume-step setting.
|
||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 6%+"), { locked = true, repeating = true , description = "Volume up" })
|
||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 6%-"), { locked = true, repeating = true , description = "Volume down" })
|
||||
bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true , description = "Mute" })
|
||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true , description = "Mute microphone" })
|
||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 6")), { locked = true, repeating = true , description = "Volume up" })
|
||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 6")), { locked = true, repeating = true , description = "Volume down" })
|
||||
bind("XF86AudioMute", hl.dsp.exec_cmd(osd("volume toggle")), { locked = true , description = "Mute" })
|
||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd(osd("microphone toggle")), { locked = true , description = "Mute microphone" })
|
||||
|
||||
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
||||
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"), { locked = true, repeating = true , description = "Volume up (fine)" })
|
||||
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true , description = "Volume down (fine)" })
|
||||
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 1")), { locked = true, repeating = true , description = "Volume up (fine)" })
|
||||
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 1")), { locked = true, repeating = true , description = "Volume down (fine)" })
|
||||
|
||||
bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
||||
bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
||||
bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true , description = "Next track" })
|
||||
bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true , description = "Previous track" })
|
||||
bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true , description = "Stop playback" })
|
||||
bind("XF86AudioPlay", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||
bind("XF86AudioPause", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||
bind("XF86AudioNext", hl.dsp.exec_cmd(osd("media next")), { locked = true , description = "Next track" })
|
||||
bind("XF86AudioPrev", hl.dsp.exec_cmd(osd("media previous")), { locked = true , description = "Previous track" })
|
||||
bind("XF86AudioStop", hl.dsp.exec_cmd(osd("media stop")), { locked = true , description = "Stop playback" })
|
||||
|
||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true , description = "Brightness up" })
|
||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true , description = "Brightness down" })
|
||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
||||
|
||||
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||
|
||||
@@ -29,7 +29,11 @@ hl.config({
|
||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||
-- Unfocused windows get no colour at all. The gradient only means
|
||||
-- something if exactly one window on screen is wearing it.
|
||||
inactive_border = "rgba(3b426199)",
|
||||
-- Follows the colour scheme: a dark neutral is invisible against a
|
||||
-- light desktop. services/ColorScheme.qml applies changes live;
|
||||
-- this is the value a fresh session starts from.
|
||||
inactive_border = prefs.get("colorScheme", "dark") == "light"
|
||||
and "rgba(a8aecb99)" or "rgba(3b426199)",
|
||||
},
|
||||
|
||||
resize_on_border = true,
|
||||
@@ -115,7 +119,11 @@ hl.config({
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
|
||||
font_family = "Adwaita Sans",
|
||||
-- Same setting as Theme.fontFamily in the shell. If only the QML side
|
||||
-- followed the preference, the compositor and the shell would disagree
|
||||
-- about the interface font and nobody would notice until a tooltip
|
||||
-- rendered in a different typeface.
|
||||
font_family = prefs.get("interfaceFont", "Adwaita Sans"),
|
||||
|
||||
-- Variable refresh rate. 3 = enable only for fullscreen windows whose
|
||||
-- content type is "video" or "game" -- the tag rules.lua applies to
|
||||
|
||||
@@ -386,6 +386,56 @@ Singleton {
|
||||
detail: "A time earlier than the start simply means the next morning"
|
||||
},
|
||||
|
||||
// ── Colour scheme ───────────────────────────────────────────────────
|
||||
// Light is Tokyo Night Day, the official light variant, rather than a
|
||||
// palette invented to merely not be dark. Both share the same hues at
|
||||
// different lightness, which is what keeps the Prism identity intact
|
||||
// across the switch.
|
||||
//
|
||||
// services/ColorScheme.qml pushes the choice to GTK and to the
|
||||
// compositor's border colours, because an application toolbar or a
|
||||
// window border still wearing the other scheme is more jarring than
|
||||
// either scheme on its own.
|
||||
{
|
||||
key: "colorScheme", type: "enum", def: "dark", group: "appearance",
|
||||
label: "Appearance",
|
||||
detail: "Light and dark share one identity, not two themes",
|
||||
options: [
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "light", label: "Light" }
|
||||
]
|
||||
},
|
||||
|
||||
// ── Typography ──────────────────────────────────────────────────────
|
||||
// The single largest thing in this desktop that used to be changeable
|
||||
// only by editing Theme.qml.
|
||||
//
|
||||
// interfaceFont is every piece of text a person reads. iconFont is used
|
||||
// ONLY to draw glyphs -- workspace pills, the status cluster, search
|
||||
// icons -- so it must carry Nerd Font glyphs; a plain monospace family
|
||||
// there replaces every icon in the shell with tofu, which is why the
|
||||
// picker offers them as a separate, filtered list.
|
||||
{
|
||||
key: "interfaceFont", type: "string", def: "Adwaita Sans", group: "typography",
|
||||
// A family name, matched by fontconfig. Constrained because it also
|
||||
// reaches hl.config as a string in hypr/looks.lua.
|
||||
pattern: "^[A-Za-z0-9 ._-]{1,64}$",
|
||||
label: "Interface font",
|
||||
detail: "Used for every piece of text in the shell"
|
||||
},
|
||||
{
|
||||
key: "iconFont", type: "string", def: "VictorMono Nerd Font", group: "typography",
|
||||
pattern: "^[A-Za-z0-9 ._-]{1,64}$",
|
||||
label: "Icon font",
|
||||
detail: "Draws the shell's glyphs, so it must be a Nerd Font"
|
||||
},
|
||||
{
|
||||
key: "interfaceFontSize", type: "int", def: 13, min: 10, max: 18, step: 1,
|
||||
unit: "px", group: "typography",
|
||||
label: "Interface text size",
|
||||
detail: "The base size the rest of the shell's type scales from"
|
||||
},
|
||||
|
||||
// ── Accessibility ───────────────────────────────────────────────────
|
||||
// Backed by gsettings so GTK applications agree with the shell, and
|
||||
// pushed to the compositor as well where it has its own notion.
|
||||
@@ -534,13 +584,18 @@ Singleton {
|
||||
// A "json" value: the ordered list of desktop entry ids pinned to the
|
||||
// dock. Kept in the shared store so that reordering the dock is covered
|
||||
// by Restore defaults like everything else, rather than living in its
|
||||
// own file. The shipped order is the GNOME dash it replaced.
|
||||
// own file. The shipped order is the GNOME dash it replaced, with one
|
||||
// deliberate substitution: Panama Settings takes the first slot rather
|
||||
// than GNOME Settings. Panama now covers what GNOME Settings did for
|
||||
// this desktop and delegates the remainder to it by name, so pinning
|
||||
// the thing it delegates TO put the fallback in front of the real one.
|
||||
// GNOME Settings stays installed and searchable in the launcher.
|
||||
{
|
||||
key: "dockPinned", type: "json", group: "dock",
|
||||
label: "Pinned applications",
|
||||
detail: "Applications that stay in the Dock whether or not they are running",
|
||||
def: [
|
||||
"org.gnome.Settings", "kitty", "org.gnome.Nautilus",
|
||||
"panama-settings", "kitty", "org.gnome.Nautilus",
|
||||
"com.bitwarden.desktop", "org.gnome.Software", "helium",
|
||||
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
||||
"app.bluebubbles.BlueBubbles", "rustdesk",
|
||||
|
||||
@@ -18,36 +18,47 @@ import QtQuick
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// ── Colour scheme ───────────────────────────────────────────────────────
|
||||
// Tokyo Night ships an official light variant (Day), so light mode is that
|
||||
// rather than a palette invented to merely not be dark. The two share the
|
||||
// same hues at different lightness, which is what lets the Prism identity
|
||||
// survive the switch: blue still leads into orchid, it is simply a darker
|
||||
// blue on a lighter ground.
|
||||
//
|
||||
// Every token below is a binding on this, so flipping it repaints the whole
|
||||
// shell without anything needing to know it happened.
|
||||
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
|
||||
|
||||
// ── Palette ─────────────────────────────────────────────────────────────
|
||||
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
||||
readonly property color bg: "#222436"
|
||||
readonly property color bgDark: "#1e2030"
|
||||
readonly property color bgHighlight: "#2f334d"
|
||||
readonly property color bgPanel: "#2e2f3d" // dock glass surface
|
||||
readonly property color bgPopover: "#21212f" // Openbar submenu background
|
||||
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
|
||||
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
|
||||
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
|
||||
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
|
||||
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
|
||||
|
||||
readonly property color fg: "#c8d3f5"
|
||||
readonly property color fgDim: "#828bb8"
|
||||
readonly property color fgMuted: "#636da6"
|
||||
readonly property color gutter: "#3b4261"
|
||||
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
|
||||
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
|
||||
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||
|
||||
// The pair. `accent` is the primary and carries every state meaning
|
||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
||||
// theme — it never appears alone, only as the far end of a gradient. That
|
||||
// restraint is the whole point: the two colours meeting is the signature,
|
||||
// so the pink stops being special the moment it's used as a flat fill.
|
||||
readonly property color accent: "#82aaff" // blue
|
||||
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
|
||||
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
|
||||
readonly property color cyan: "#86e1fc"
|
||||
readonly property color teal: "#4fd6be"
|
||||
readonly property color green: "#c3e88d"
|
||||
readonly property color yellow: "#ffc777"
|
||||
readonly property color orange: "#ff966c"
|
||||
readonly property color red: "#ff757f"
|
||||
readonly property color redDeep: "#c53b53"
|
||||
readonly property color magenta: "#c099ff"
|
||||
readonly property color pink: "#fca7ea"
|
||||
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
||||
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
||||
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||
readonly property color green: root.dark ? "#c3e88d" : "#587539"
|
||||
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
|
||||
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
|
||||
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
|
||||
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
|
||||
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
|
||||
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
|
||||
|
||||
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
||||
readonly property color ok: green
|
||||
@@ -59,11 +70,14 @@ Singleton {
|
||||
// The dock sits on compositor blur, so its fill is deliberately very
|
||||
// translucent. Popovers are near-opaque because they carry text that must
|
||||
// stay legible. The bar itself has no surface or alpha token.
|
||||
readonly property real dockAlpha: 0.34
|
||||
readonly property real popoverAlpha: 0.92
|
||||
readonly property real overlayAlpha: 0.55
|
||||
readonly property real hoverAlpha: 0.14
|
||||
readonly property real activeAlpha: 0.24
|
||||
// Light surfaces need more opacity for the same sense of a solid panel: the
|
||||
// same 0.34 that reads as glass over a dark desktop reads as haze over a
|
||||
// light one, and text on it stops being legible.
|
||||
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
|
||||
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
|
||||
readonly property real overlayAlpha: root.dark ? 0.55 : 0.40
|
||||
readonly property real hoverAlpha: root.dark ? 0.14 : 0.10
|
||||
readonly property real activeAlpha: root.dark ? 0.24 : 0.18
|
||||
|
||||
// ── Geometry ────────────────────────────────────────────────────────────
|
||||
readonly property int barHeight: 36
|
||||
@@ -90,12 +104,12 @@ Singleton {
|
||||
// Adwaita Sans for everything the user reads as text. No exceptions: a
|
||||
// monospaced clock or percentage reads as a terminal readout pasted into a
|
||||
// UI, which is the opposite of the intent here.
|
||||
readonly property string fontFamily: "Adwaita Sans"
|
||||
readonly property string fontFamily: DesktopPreferences.get("interfaceFont")
|
||||
|
||||
// Nerd Font, used ONLY to draw icon glyphs — never for text. It is the
|
||||
// pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||
// hardcoded dark fill Qt will not recolour (see widgets/ThemedIcon.qml).
|
||||
readonly property string fontMono: "VictorMono Nerd Font"
|
||||
readonly property string fontMono: DesktopPreferences.get("iconFont")
|
||||
|
||||
// Apply to any text whose digits change in place — clocks, percentages,
|
||||
// elapsed timers, dimension readouts. Tabular figures all share one
|
||||
@@ -107,10 +121,13 @@ Singleton {
|
||||
readonly property var tabularFigures: ({
|
||||
"tnum": 1
|
||||
})
|
||||
readonly property int fontSize: 13
|
||||
readonly property int fontSizeSmall: 11
|
||||
readonly property int fontSizeLarge: 16
|
||||
readonly property int fontSizeTitle: 20
|
||||
// The four sizes are a scale, not four independent numbers: they move
|
||||
// together so the relationship between body, caption, heading and title
|
||||
// survives a change to the base rather than drifting apart.
|
||||
readonly property int fontSize: DesktopPreferences.get("interfaceFontSize")
|
||||
readonly property int fontSizeSmall: Math.max(9, root.fontSize - 2)
|
||||
readonly property int fontSizeLarge: root.fontSize + 3
|
||||
readonly property int fontSizeTitle: root.fontSize + 7
|
||||
|
||||
// ── Motion ──────────────────────────────────────────────────────────────
|
||||
// Event-driven only. Nothing in this shell animates while idle — no pulse,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
ShellRoot {
|
||||
// DesktopEntries populates asynchronously. Reading `applications.values`
|
||||
// here rather than calling byId() blind is the documented way to wait for
|
||||
// it -- and is exactly the trap noted in modules/settings/README.md.
|
||||
readonly property var ids: {
|
||||
const out = {};
|
||||
for (const entry of DesktopEntries.applications.values)
|
||||
out[entry.id] = entry;
|
||||
return out;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "dock-pin-test"
|
||||
|
||||
function count(): int { return DesktopEntries.applications.values.length; }
|
||||
|
||||
function resolve(): string {
|
||||
const pinned = DesktopPreferences.get("dockPinned");
|
||||
const missing = [];
|
||||
let first = null;
|
||||
for (const id of pinned) {
|
||||
const entry = DesktopEntries.byId(id);
|
||||
if (!entry) missing.push(id);
|
||||
if (first === null)
|
||||
first = { id: id, found: !!entry, name: entry ? entry.name : "", icon: entry ? entry.icon : "" };
|
||||
}
|
||||
return JSON.stringify({ total: pinned.length, first: first, missing: missing });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Compact, click-through system feedback. Repeated hardware-key presses update
|
||||
// this one surface in place, avoiding a stack of notification cards.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property var modelData: null
|
||||
readonly property var presentation: OsdState.state
|
||||
readonly property bool belongsHere: !OsdState.monitorName || !root.modelData
|
||||
|| root.modelData.name === OsdState.monitorName
|
||||
readonly property bool requestedVisible: OsdState.active && root.belongsHere
|
||||
readonly property int horizontalPadding: 18
|
||||
readonly property int contentGap: 14
|
||||
readonly property int progressWidth: 156
|
||||
readonly property int progressLabelWidth: 48
|
||||
readonly property int messageWidth: Math.min(300, Math.max(52, Math.ceil(labelMetrics.advanceWidth)))
|
||||
readonly property int desiredWidth: root.horizontalPadding * 2 + 24 + root.contentGap
|
||||
+ (root.presentation.progress
|
||||
? root.progressWidth + root.contentGap + root.progressLabelWidth
|
||||
: root.messageWidth)
|
||||
|
||||
screen: root.modelData
|
||||
anchors.bottom: true
|
||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: root.desiredWidth
|
||||
implicitHeight: 64
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-osd"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
property bool mapped: false
|
||||
visible: root.mapped
|
||||
|
||||
onRequestedVisibleChanged: {
|
||||
if (root.requestedVisible) {
|
||||
unmapTimer.stop();
|
||||
root.mapped = true;
|
||||
entrance.restart();
|
||||
} else if (root.mapped) {
|
||||
unmapTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.requestedVisible)
|
||||
root.mapped = true;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: unmapTimer
|
||||
interval: Theme.durNormal
|
||||
onTriggered: root.mapped = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, 0.9)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||
opacity: root.requestedVisible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
id: slide
|
||||
y: root.requestedVisible ? 0 : 8
|
||||
Behavior on y {
|
||||
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: root.contentGap
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 24
|
||||
icon: root.presentation.icon
|
||||
iconFallback: "dialog-information-symbolic"
|
||||
tint: Theme.fg
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.presentation.progress
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: visible ? root.progressWidth : 0
|
||||
height: 8
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: height / 2
|
||||
color: Theme.alpha(Theme.fg, 0.14)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * root.presentation.ratio
|
||||
radius: height / 2
|
||||
color: Theme.accent
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.presentation.progress ? root.progressLabelWidth : root.messageWidth
|
||||
text: root.presentation.label
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: root.presentation.progress ? Text.AlignRight : Text.AlignLeft
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: labelMetrics
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
text: root.presentation.label
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: entrance
|
||||
NumberAnimation {
|
||||
target: surface
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.durNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: slide
|
||||
property: "y"
|
||||
from: 8
|
||||
to: 0
|
||||
duration: Theme.durNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
module qs.modules.osd
|
||||
Osd 1.0 Osd.qml
|
||||
@@ -49,9 +49,18 @@ SettingsPage {
|
||||
: "Applied to every display. Panama looks in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
|
||||
|
||||
WallpaperPicker {
|
||||
id: wallpapers
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: wallpapers.hidden > 0 || wallpapers.expanded
|
||||
label: wallpapers.expanded ? "Showing every image" : wallpapers.hidden + " more available"
|
||||
detail: "The grid is kept short so the rest of this page stays reachable"
|
||||
action: wallpapers.expanded ? "Show fewer" : "Show all"
|
||||
onTriggered: wallpapers.expanded = !wallpapers.expanded
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
label: "Look for new images"
|
||||
detail: Wallpaper.scanning
|
||||
@@ -64,6 +73,56 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Colour scheme"
|
||||
subtitle: ColorScheme.lastError !== ""
|
||||
? ColorScheme.lastError
|
||||
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
|
||||
|
||||
ChoiceRow { setting: "colorScheme"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Typography"
|
||||
subtitle: Fonts.lastError !== ""
|
||||
? Fonts.lastError
|
||||
: "Every piece of text in the shell. Samples are drawn in the font they name."
|
||||
|
||||
SliderRow { setting: "interfaceFontSize" }
|
||||
|
||||
TextRow {
|
||||
label: "Interface font"
|
||||
detail: Fonts.interfaceMissing
|
||||
? "Not installed on this machine — fontconfig is substituting something else"
|
||||
: "Used for all shell text"
|
||||
value: Fonts.interfaceFont
|
||||
}
|
||||
|
||||
FontPicker {
|
||||
width: parent.width
|
||||
families: Fonts.interfaceFonts
|
||||
current: Fonts.interfaceFont
|
||||
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No interface fonts found"
|
||||
onPicked: family => Fonts.setInterface(family)
|
||||
}
|
||||
|
||||
TextRow {
|
||||
label: "Icon font"
|
||||
detail: Fonts.iconMissing
|
||||
? "Not installed — the shell's glyphs will not draw correctly"
|
||||
: "Draws the shell's glyphs, so only Nerd Fonts are offered"
|
||||
value: Fonts.iconFont
|
||||
}
|
||||
|
||||
FontPicker {
|
||||
width: parent.width
|
||||
families: Fonts.iconFonts
|
||||
current: Fonts.iconFont
|
||||
emptyText: "No Nerd Fonts installed"
|
||||
onPicked: family => Fonts.setIcon(family)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Windows"
|
||||
subtitle: "Spacing and shape of tiled windows. Each change is applied to the compositor and confirmed before it is saved."
|
||||
|
||||
@@ -50,10 +50,13 @@ Rectangle {
|
||||
|
||||
// Stands in for the wallpaper. Static: an animated gradient here would
|
||||
// repaint forever behind a settings page.
|
||||
// Follows the colour scheme. A preview that stays dark while the shell
|
||||
// around it is light does not read as "your desktop" -- it reads as a
|
||||
// screenshot of someone else's.
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
GradientStop { position: 0.0; color: "#2b3050" }
|
||||
GradientStop { position: 1.0; color: "#241f33" }
|
||||
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" }
|
||||
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" }
|
||||
}
|
||||
|
||||
// The bar, so the preview reads as this desktop and not a generic one.
|
||||
@@ -203,7 +206,7 @@ Rectangle {
|
||||
shadowEnabled: true
|
||||
shadowColor: win.focused && root.glowOn
|
||||
? Theme.alpha(Theme.accent, 0.5)
|
||||
: Theme.alpha("#15161e", 0.85)
|
||||
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45)
|
||||
shadowBlur: win.focused && root.glowOn
|
||||
? Math.min(1.0, root.px(root.glowRange) / 12)
|
||||
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Choosing a font family.
|
||||
//
|
||||
// Each candidate is rendered IN the font it names. A list of family names set
|
||||
// in the current font tells you nothing about what you are choosing, and the
|
||||
// whole point of picking a typeface is seeing it.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.modules.clipboard
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
property var families: []
|
||||
property string current: ""
|
||||
property string emptyText: "No fonts found"
|
||||
|
||||
signal picked(string family)
|
||||
|
||||
// Filtered rather than listing all 139: a scrolling wall of family names
|
||||
// inside a page that is already scrolling is worse than a search box.
|
||||
readonly property var matches: {
|
||||
const needle = filter.text.trim().toLowerCase();
|
||||
const list = root.families.filter(family =>
|
||||
needle === "" ? true : family.toLowerCase().indexOf(needle) >= 0);
|
||||
// Always show what is currently selected, even when it does not match.
|
||||
if (needle === "" && list.indexOf(root.current) >= 0)
|
||||
return [root.current].concat(list.filter(f => f !== root.current)).slice(0, 12);
|
||||
return list.slice(0, 12);
|
||||
}
|
||||
|
||||
SearchField {
|
||||
id: filter
|
||||
width: parent.width
|
||||
placeholder: "Search installed fonts"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.matches
|
||||
|
||||
SettingRow {
|
||||
id: candidate
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool selected: candidate.modelData === root.current
|
||||
|
||||
label: candidate.modelData
|
||||
detail: candidate.selected ? "Currently in use" : ""
|
||||
controlWidth: 210
|
||||
divider: candidate.index < root.matches.length - 1
|
||||
activatable: !candidate.selected
|
||||
onActivated: root.picked(candidate.modelData)
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 200
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideRight
|
||||
// The sample is drawn in the candidate family, which is the
|
||||
// entire reason this is a list rather than a text field.
|
||||
text: "Handgloves 0123"
|
||||
font.family: candidate.modelData
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: candidate.selected ? Theme.accent : Theme.fgDim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
visible: root.matches.length === 0
|
||||
label: root.emptyText
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,19 @@ Item {
|
||||
|
||||
implicitHeight: grid.implicitHeight
|
||||
|
||||
// Sixty tiles is two screens of wallpaper on a page that also holds
|
||||
// typography, window geometry and effects -- everything below it becomes
|
||||
// unreachable without a long scroll past pictures. Two rows by default,
|
||||
// all of them on request.
|
||||
property bool expanded: false
|
||||
readonly property int collapsedRows: 2
|
||||
|
||||
readonly property var shown: root.expanded
|
||||
? Wallpaper.available
|
||||
: Wallpaper.available.slice(0, root.columns * root.collapsedRows)
|
||||
|
||||
readonly property int hidden: Wallpaper.available.length - root.shown.length
|
||||
|
||||
readonly property int columns: Math.max(2, Math.floor(width / 190))
|
||||
readonly property real cellWidth: columns > 0 ? (width - (columns - 1) * 10) / columns : 160
|
||||
|
||||
@@ -30,7 +43,7 @@ Item {
|
||||
spacing: 10
|
||||
|
||||
Repeater {
|
||||
model: Wallpaper.available
|
||||
model: root.shown
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
|
||||
@@ -45,3 +45,4 @@ SoundDeviceList 1.0 SoundDeviceList.qml
|
||||
SoundDeviceRow 1.0 SoundDeviceRow.qml
|
||||
TimeOfDayRow 1.0 TimeOfDayRow.qml
|
||||
LocationPicker 1.0 LocationPicker.qml
|
||||
FontPicker 1.0 FontPicker.qml
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
// A living reference for Panama's shared visual language. This deliberately
|
||||
// renders the production widgets instead of lookalike mock components, so a
|
||||
// change to a control or Theme token is visible here immediately.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.config
|
||||
import qs.widgets
|
||||
|
||||
ShellRoot {
|
||||
FloatingWindow {
|
||||
id: galleryWindow
|
||||
|
||||
title: "Panama Prism Gallery"
|
||||
visible: true
|
||||
implicitWidth: 1120
|
||||
implicitHeight: 760
|
||||
minimumSize: Qt.size(920, 640)
|
||||
color: Theme.bg
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.bg
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 34
|
||||
spacing: 34
|
||||
|
||||
// Quiet editorial rail: enough context to make screenshots
|
||||
// self-explanatory without competing with the components.
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: 222
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 38
|
||||
Layout.preferredHeight: 38
|
||||
radius: 12
|
||||
color: Theme.alpha(Theme.fg, 0.07)
|
||||
|
||||
ThemedIcon {
|
||||
anchors.centerIn: parent
|
||||
size: 20
|
||||
icon: "applications-graphics-symbolic"
|
||||
tint: Theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.topMargin: 20
|
||||
text: "PRISM"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 26
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.4
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.topMargin: 5
|
||||
Layout.maximumWidth: 210
|
||||
text: "Panama's living interface reference. Calm by default, vivid with purpose."
|
||||
color: Theme.fgDim
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.25
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
|
||||
Item { Layout.preferredHeight: 36 }
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ label: "Tokyo Night Moon", color: Theme.bgHighlight },
|
||||
{ label: "Signal blue", color: Theme.accent },
|
||||
{ label: "Prism orchid", color: Theme.accentSecondary }
|
||||
]
|
||||
|
||||
RowLayout {
|
||||
required property var modelData
|
||||
Layout.topMargin: 10
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 12
|
||||
Layout.preferredHeight: 12
|
||||
radius: 6
|
||||
color: modelData.color
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.label
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Text {
|
||||
text: "REAL COMPONENTS · LIVE TOKENS"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 30
|
||||
contentWidth: width
|
||||
contentHeight: content.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
width: parent.width
|
||||
spacing: 28
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "Interface inventory"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Production controls in their canonical states"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Pill {
|
||||
interactive: false
|
||||
horizontalPadding: 12
|
||||
|
||||
ThemedIcon {
|
||||
size: 14
|
||||
icon: "emblem-ok-symbolic"
|
||||
tint: Theme.green
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "TOKYO NIGHT MOON"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "QUICK CONTROLS"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Toggle {
|
||||
id: wifiToggle
|
||||
Layout.fillWidth: true
|
||||
icon: "network-wireless-signal-excellent-symbolic"
|
||||
label: "Wi-Fi"
|
||||
sublabel: active ? "Connected" : "Off"
|
||||
active: true
|
||||
onToggled: active = !active
|
||||
}
|
||||
|
||||
Toggle {
|
||||
id: bluetoothToggle
|
||||
Layout.fillWidth: true
|
||||
icon: "bluetooth-active-symbolic"
|
||||
label: "Bluetooth"
|
||||
sublabel: active ? "2 devices" : "Off"
|
||||
active: false
|
||||
onToggled: active = !active
|
||||
}
|
||||
|
||||
Toggle {
|
||||
id: focusToggle
|
||||
Layout.fillWidth: true
|
||||
icon: "weather-clear-night-symbolic"
|
||||
label: "Focus"
|
||||
sublabel: active ? "Until 5:30" : "Available"
|
||||
active: false
|
||||
onToggled: active = !active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "CONTINUOUS VALUES"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 126
|
||||
radius: Theme.cardRadius
|
||||
color: Theme.alpha(Theme.fg, 0.045)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 18
|
||||
spacing: 12
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: "System audio"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Text {
|
||||
text: Math.round(volumeSlider.value * 100) + "%"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
ValueSlider {
|
||||
id: volumeSlider
|
||||
Layout.fillWidth: true
|
||||
value: 0.68
|
||||
icon: "audio-volume-high-symbolic"
|
||||
onMoved: newValue => volumeSlider.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "BAR MATERIAL"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 74
|
||||
radius: Theme.cardRadius
|
||||
color: Theme.alpha(Theme.bg, 0.62)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
Pill {
|
||||
ThemedIcon {
|
||||
size: 15
|
||||
icon: "view-grid-symbolic"
|
||||
tint: Theme.accent
|
||||
}
|
||||
Text {
|
||||
text: "1 2 3"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
ThemedIcon {
|
||||
size: 15
|
||||
icon: "edit-copy-symbolic"
|
||||
tint: Theme.fgDim
|
||||
}
|
||||
Text {
|
||||
text: "Clipboard"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
ThemedIcon {
|
||||
size: 15
|
||||
icon: "appointment-soon-symbolic"
|
||||
tint: Theme.fgDim
|
||||
}
|
||||
Text {
|
||||
text: "Tue Aug 18 · 4:46 AM"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/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}"
|
||||
kill_command="${PANAMA_ACTION_KILL_COMMAND:-/usr/bin/kill}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
release_caffeine_inhibitors() {
|
||||
local inhibitors pid uid
|
||||
uid="$(id -u)"
|
||||
inhibitors="$(systemd-inhibit --list --no-pager --no-legend 2>/dev/null \
|
||||
| awk -v uid="$uid" '$1 == "Panama" && $2 == uid && $(NF - 1) == "Caffeine" && $NF == "block" { print $4 }')"
|
||||
while IFS= read -r pid; do
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
"$kill_command" "$pid" 2>/dev/null || true
|
||||
done <<<"$inhibitors"
|
||||
}
|
||||
|
||||
wait_for_confirmed_state() {
|
||||
local target="$1" expected="$2" attempt
|
||||
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
|
||||
if [[ $expected == true ]] && state_is_active "$target"; then
|
||||
return 0
|
||||
fi
|
||||
if [[ $expected == false ]] && ! state_is_active "$target"; then
|
||||
return 0
|
||||
fi
|
||||
sleep "$confirm_delay"
|
||||
done
|
||||
printf 'Panama action could not confirm %s reached state %s\n' "$target" "$expected" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
toggle_state() {
|
||||
local target="$1" on_icon="$2" off_icon="$3" label="$4" state
|
||||
state="$(qs ipc call "$target" toggle)"
|
||||
case "$state" in true|false) ;; *) show_state "$state" "$on_icon" "$off_icon" "$label"; return ;; esac
|
||||
if [[ $target == caffeine && $state == false ]]; then
|
||||
release_caffeine_inhibitors
|
||||
fi
|
||||
wait_for_confirmed_state "$target" "$state"
|
||||
show_state "$state" "$on_icon" "$off_icon" "$label"
|
||||
}
|
||||
|
||||
wait_for_shell() {
|
||||
local attempt
|
||||
for ((attempt = 0; attempt < confirm_attempts; attempt++)); do
|
||||
if qs ipc show >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep "$confirm_delay"
|
||||
done
|
||||
printf 'Panama shell did not become ready after restart\n' >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$action" in
|
||||
control-center) qs ipc call quicksettings open ;;
|
||||
notifications) qs ipc call notifications open ;;
|
||||
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
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Lists installed font families, split by what they are usable for.
|
||||
#
|
||||
# Two separate questions get asked of fonts here, and they have different
|
||||
# answers:
|
||||
#
|
||||
# interface proportional families suitable for reading UI text
|
||||
# monospace fixed-pitch families
|
||||
# icons families carrying Nerd Font glyphs
|
||||
#
|
||||
# The icon distinction matters more than it looks. Theme.fontMono is used ONLY
|
||||
# to draw glyphs -- the workspace pills, the status cluster, the search icon --
|
||||
# and picking a monospace family without those glyphs replaces every icon in the
|
||||
# shell with tofu. So they are reported separately rather than lumped in with
|
||||
# monospace, and the picker can say so.
|
||||
#
|
||||
# fc-list reports one line per style, so families are deduplicated here. The
|
||||
# first comma-separated alias is the canonical name Qt will match.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
emit() {
|
||||
# $1: fontconfig pattern, $2: json key
|
||||
fc-list "$1" family 2>/dev/null \
|
||||
| sed 's/,.*//' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
|
||||
| grep -v '^$' \
|
||||
| sort -u
|
||||
}
|
||||
|
||||
json_array() {
|
||||
local first=true
|
||||
printf '['
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] || continue
|
||||
[[ "$first" == true ]] || printf ','
|
||||
first=false
|
||||
printf '%s' "$(printf '%s' "$line" | jq -Rs 'rtrimstr("\n")')"
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
|
||||
# spacing=100 is fontconfig's mono flag. Proportional is everything else.
|
||||
mono="$(emit ':spacing=100')"
|
||||
all="$(emit ':')"
|
||||
interface="$(comm -23 <(printf '%s\n' "$all") <(printf '%s\n' "$mono"))"
|
||||
icons="$(printf '%s\n' "$all" | grep -i 'nerd font' || true)"
|
||||
|
||||
printf '{"interface":'
|
||||
printf '%s\n' "$interface" | json_array
|
||||
printf ',"monospace":'
|
||||
printf '%s\n' "$mono" | json_array
|
||||
printf ',"icons":'
|
||||
printf '%s\n' "$icons" | json_array
|
||||
printf '}\n'
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
set -u
|
||||
|
||||
strict_delivery() {
|
||||
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
|
||||
}
|
||||
|
||||
show_progress() {
|
||||
qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1 || true
|
||||
if ! qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1; then
|
||||
strict_delivery && return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
show_message() {
|
||||
qs ipc call osd message "$1" "$2" >/dev/null 2>&1 || true
|
||||
if ! qs ipc call osd message "$1" "$2" >/dev/null 2>&1; then
|
||||
strict_delivery && return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
volume_state() {
|
||||
@@ -100,8 +110,18 @@ case "${1:-}" in
|
||||
microphone) shift; adjust_microphone "$@" ;;
|
||||
brightness) shift; adjust_brightness "$@" ;;
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
gallery="${XDG_CONFIG_HOME:-$HOME/.config}/quickshell/prism-gallery.qml"
|
||||
|
||||
if qs -p "$gallery" ipc show >/dev/null 2>&1; then
|
||||
qs -p "$gallery" kill
|
||||
else
|
||||
qs -p "$gallery" --daemonize
|
||||
fi
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
pragma Singleton
|
||||
|
||||
// Installed fonts, split by what they can actually be used for.
|
||||
//
|
||||
// Two different questions with different answers: which families are readable
|
||||
// as interface text, and which carry the Nerd Font glyphs the shell draws its
|
||||
// icons with. Offering one list for both is how you end up with a desktop full
|
||||
// of tofu, so they stay separate.
|
||||
//
|
||||
// Enumerated once on demand. Fonts do not appear while you are looking at a
|
||||
// settings page, and fc-list over a few hundred families is not free.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-fonts"
|
||||
|
||||
property var interfaceFonts: []
|
||||
property var monospaceFonts: []
|
||||
property var iconFonts: []
|
||||
property bool scanning: false
|
||||
property string lastError: ""
|
||||
|
||||
readonly property string interfaceFont: DesktopPreferences.get("interfaceFont")
|
||||
readonly property string iconFont: DesktopPreferences.get("iconFont")
|
||||
|
||||
readonly property bool loaded: root.interfaceFonts.length > 0
|
||||
|
||||
// True when the stored family is not installed. fontconfig silently
|
||||
// substitutes something else, so the shell keeps working and quietly stops
|
||||
// looking the way it was configured to -- worth saying out loud.
|
||||
readonly property bool interfaceMissing: root.loaded
|
||||
&& root.interfaceFonts.indexOf(root.interfaceFont) < 0
|
||||
readonly property bool iconMissing: root.iconFonts.length > 0
|
||||
&& root.iconFonts.indexOf(root.iconFont) < 0
|
||||
|
||||
Process {
|
||||
id: scan
|
||||
command: [root.helperPath]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.interfaceFonts = parsed.interface ?? [];
|
||||
root.monospaceFonts = parsed.monospace ?? [];
|
||||
root.iconFonts = parsed.icons ?? [];
|
||||
root.lastError = "";
|
||||
} catch (error) {
|
||||
root.lastError = "The installed fonts could not be read.";
|
||||
}
|
||||
root.scanning = false;
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.scanning = false;
|
||||
if (exitCode !== 0)
|
||||
root.lastError = "The installed fonts could not be read.";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
|
||||
function refresh(): void {
|
||||
if (scan.running)
|
||||
return;
|
||||
root.scanning = true;
|
||||
scan.running = true;
|
||||
}
|
||||
|
||||
// Only a family this machine reported is accepted, so a hand-edited
|
||||
// settings file cannot put arbitrary text where a font name belongs -- it
|
||||
// reaches hl.config as a string on the compositor side.
|
||||
function setInterface(family: string): bool {
|
||||
if (root.interfaceFonts.indexOf(family) < 0) {
|
||||
root.lastError = "That font is not installed.";
|
||||
return false;
|
||||
}
|
||||
return root.store("interfaceFont", family);
|
||||
}
|
||||
|
||||
function setIcon(family: string): bool {
|
||||
if (root.iconFonts.indexOf(family) < 0) {
|
||||
root.lastError = "That font does not carry icon glyphs.";
|
||||
return false;
|
||||
}
|
||||
return root.store("iconFont", family);
|
||||
}
|
||||
|
||||
function store(key: string, family: string): bool {
|
||||
if (!DesktopPreferences.set(key, family)) {
|
||||
root.lastError = "That font name could not be saved.";
|
||||
return false;
|
||||
}
|
||||
root.lastError = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property bool initialized: false
|
||||
property bool suppressStatusEvents: false
|
||||
|
||||
// The manual switch. Ignored while `automatic` is on.
|
||||
property bool enabled: Settings.nightLightEnabledByDefault
|
||||
@@ -53,6 +54,22 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Launcher actions already provide immediate Prism OSD feedback. Keep the
|
||||
// ambient Signal Glass channel quiet for that path so one choice produces
|
||||
// one confirmation instead of two competing transients.
|
||||
function toggleQuietly(): void {
|
||||
root.suppressStatusEvents = true;
|
||||
root.toggle();
|
||||
root.suppressStatusEvents = false;
|
||||
}
|
||||
|
||||
function restore(manualEnabled: bool, automaticEnabled: bool): void {
|
||||
root.suppressStatusEvents = true;
|
||||
root.enabled = manualEnabled;
|
||||
root.automatic = automaticEnabled;
|
||||
root.suppressStatusEvents = false;
|
||||
}
|
||||
|
||||
// The window wraps midnight (17:00 → 10:00), so the comparison flips when
|
||||
// `from` is later in the day than `to`.
|
||||
function inWindow(hour: real): bool {
|
||||
@@ -118,7 +135,7 @@ Singleton {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!root.initialized)
|
||||
if (!root.initialized || root.suppressStatusEvents)
|
||||
return;
|
||||
StatusEvents.publish({
|
||||
key: "display-night-light",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import "../modules/osd/OsdModel.js" as OsdModel
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property string monitorName: ""
|
||||
property var state: OsdModel.messageState("dialog-information-symbolic", "", 1400)
|
||||
|
||||
function present(next: var): void {
|
||||
root.state = next;
|
||||
root.monitorName = Hyprland.focusedMonitor?.name ?? "";
|
||||
root.active = true;
|
||||
if (next.duration > 0) {
|
||||
hideTimer.interval = next.duration;
|
||||
hideTimer.restart();
|
||||
} else {
|
||||
hideTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||
root.present(OsdModel.progressState(kind, value, maximum, label, 1400));
|
||||
}
|
||||
|
||||
function message(kind: string, label: string): void {
|
||||
root.present(OsdModel.messageState(kind, label, 1400));
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
hideTimer.stop();
|
||||
root.active = false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 1400
|
||||
onTriggered: root.active = false
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import qs.modules.datemenu
|
||||
import qs.modules.focus
|
||||
import qs.modules.signals
|
||||
import qs.modules.settings
|
||||
import qs.modules.osd
|
||||
|
||||
// Clipboard and datemenu ship explicit qmldir manifests because they were
|
||||
// added while this daily-driver shell was already running; that also keeps
|
||||
@@ -72,6 +73,11 @@ ShellRoot {
|
||||
SignalGlass {}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Osd {}
|
||||
}
|
||||
|
||||
// ── Single-instance overlays ────────────────────────────────────────────
|
||||
// These are always constructed but only *visible* when ShellState says so.
|
||||
// They're cheap while hidden, and keeping them alive means opening the
|
||||
@@ -108,7 +114,10 @@ ShellRoot {
|
||||
IpcHandler {
|
||||
target: "focus"
|
||||
|
||||
function start(): void { FocusSession.startDefault(); }
|
||||
function start(): bool {
|
||||
FocusSession.startDefault();
|
||||
return FocusSession.active;
|
||||
}
|
||||
function reveal(): void { FocusSession.reveal(); }
|
||||
function dismiss(): void { FocusSession.dismiss(); }
|
||||
function pause(): void { FocusSession.pauseOrResume(); }
|
||||
@@ -117,7 +126,12 @@ ShellRoot {
|
||||
ShellState.openOverview(FocusSession.workspaceId);
|
||||
FocusSession.dismiss();
|
||||
}
|
||||
function end(): void { FocusSession.end(false); }
|
||||
function end(): bool {
|
||||
if (!FocusSession.active)
|
||||
return false;
|
||||
FocusSession.end(false);
|
||||
return !FocusSession.active;
|
||||
}
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
active: FocusSession.active,
|
||||
@@ -194,6 +208,25 @@ ShellRoot {
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "osd"
|
||||
|
||||
function progress(kind: string, value: int, maximum: int, label: string): void {
|
||||
OsdState.progress(kind, value, maximum, label);
|
||||
}
|
||||
function message(kind: string, label: string): void {
|
||||
OsdState.message(kind, label);
|
||||
}
|
||||
function hide(): void { OsdState.hide(); }
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
active: OsdState.active,
|
||||
monitorName: OsdState.monitorName,
|
||||
state: OsdState.state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "activity"
|
||||
|
||||
@@ -232,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 {
|
||||
target: "kdeconnect"
|
||||
function fixture(name: string): void { KdeConnect.applyFixture(name); }
|
||||
|
||||
@@ -4,7 +4,7 @@ Name=Panama Settings
|
||||
GenericName=System Settings
|
||||
Comment=Configure the Panama Hyprland desktop
|
||||
Exec=qs ipc call settings open
|
||||
Icon=preferences-system-symbolic
|
||||
Icon=panama-settings
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
Categories=Settings;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Panama Settings.
|
||||
|
||||
A gear, because a settings icon has to be recognisable at 48px in a dock
|
||||
before it is anything else - but rendered in the Prism identity rather than
|
||||
the flat monochrome symbolic icon this replaces. Blue leads into orchid, the
|
||||
same pair that marks the focused window, the active workspace, and the
|
||||
hairline along every glass surface.
|
||||
|
||||
The gear is a real toothed outline. An earlier version drew a ring with radial
|
||||
strokes; at dock size the strokes merged into the ring and it read as an X.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<defs>
|
||||
<linearGradient id="prism" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#82aaff"/>
|
||||
<stop offset="55%" stop-color="#9b8ed8"/>
|
||||
<stop offset="100%" stop-color="#b172b0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2b2e45"/>
|
||||
<stop offset="100%" stop-color="#1e2030"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#82aaff" stop-opacity="0"/>
|
||||
<stop offset="22%" stop-color="#82aaff" stop-opacity="0.9"/>
|
||||
<stop offset="78%" stop-color="#b172b0" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#b172b0" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="4" y="4" width="120" height="120" rx="28" fill="url(#tile)"/>
|
||||
<rect x="4.5" y="4.5" width="119" height="119" rx="27.5" fill="none"
|
||||
stroke="#c8d3f5" stroke-opacity="0.10" stroke-width="1"/>
|
||||
<path d="M34 6.5 H94" stroke="url(#edge)" stroke-width="1.6" stroke-linecap="round"/>
|
||||
|
||||
<path d="M 52.53 29.88 L 56.45 18.62 L 71.55 18.62 L 75.47 29.88 L 80.02 31.76 L 90.75 26.57 L 101.43 37.25 L 96.24 47.98 L 98.12 52.53 L 109.38 56.45 L 109.38 71.55 L 98.12 75.47 L 96.24 80.02 L 101.43 90.75 L 90.75 101.43 L 80.02 96.24 L 75.47 98.12 L 71.55 109.38 L 56.45 109.38 L 52.53 98.12 L 47.98 96.24 L 37.25 101.43 L 26.57 90.75 L 31.76 80.02 L 29.88 75.47 L 18.62 71.55 L 18.62 56.45 L 29.88 52.53 L 31.76 47.98 L 26.57 37.25 L 37.25 26.57 L 47.98 31.76 Z" fill="url(#prism)" stroke="url(#prism)" stroke-width="5"
|
||||
stroke-linejoin="round"/>
|
||||
<circle cx="64" cy="64" r="15" fill="#1e2030"/>
|
||||
<circle cx="64" cy="64" r="15" fill="none" stroke="#c8d3f5" stroke-opacity="0.18" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Panama Settings.
|
||||
|
||||
A gear, because a settings icon has to be recognisable at 48px in a dock
|
||||
before it is clever — but rendered in the Prism identity rather than the flat
|
||||
monochrome of the symbolic icon this replaces. Blue leads into orchid across
|
||||
the gradient, the same pair that marks the focused window, the active
|
||||
workspace and the hairline on every glass surface.
|
||||
|
||||
Drawn on the dark tile (#222436) so it reads as an application icon among
|
||||
full-colour ones, rather than as a UI glyph that wandered out of a toolbar.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<defs>
|
||||
<linearGradient id="prism" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#82aaff"/>
|
||||
<stop offset="55%" stop-color="#9b8ed8"/>
|
||||
<stop offset="100%" stop-color="#b172b0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2b2e45"/>
|
||||
<stop offset="100%" stop-color="#1e2030"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#82aaff" stop-opacity="0"/>
|
||||
<stop offset="22%" stop-color="#82aaff" stop-opacity="0.85"/>
|
||||
<stop offset="78%" stop-color="#b172b0" stop-opacity="0.85"/>
|
||||
<stop offset="100%" stop-color="#b172b0" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="4" y="4" width="120" height="120" rx="28" fill="url(#tile)"/>
|
||||
<rect x="4.5" y="4.5" width="119" height="119" rx="27.5" fill="none"
|
||||
stroke="#c8d3f5" stroke-opacity="0.10" stroke-width="1"/>
|
||||
|
||||
<!-- The prism hairline, as it appears along the top edge of every glass
|
||||
surface in the shell. -->
|
||||
<path d="M32 6.5 H96" stroke="url(#edge)" stroke-width="1.5" stroke-linecap="round"/>
|
||||
|
||||
<!-- Gear: eight teeth, drawn as a ring with radial spokes so it stays legible
|
||||
when the icon is scaled down to a dock tile. -->
|
||||
<g transform="translate(64 66)">
|
||||
<g fill="none" stroke="url(#prism)" stroke-width="9" stroke-linecap="round">
|
||||
<circle r="24"/>
|
||||
<g>
|
||||
<path d="M0 -38 V-30"/>
|
||||
<path d="M0 38 V30"/>
|
||||
<path d="M-38 0 H-30"/>
|
||||
<path d="M38 0 H30"/>
|
||||
<path d="M-26.9 -26.9 L-21.2 -21.2"/>
|
||||
<path d="M26.9 26.9 L21.2 21.2"/>
|
||||
<path d="M-26.9 26.9 L-21.2 21.2"/>
|
||||
<path d="M26.9 -26.9 L21.2 -21.2"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle r="7.5" fill="#c8d3f5" fill-opacity="0.92"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
+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
|
||||
@@ -0,0 +1,26 @@
|
||||
# Upstream inspiration
|
||||
|
||||
Panama is its own desktop experience, but it should learn from strong open-source work without obscuring where an idea came from. This ledger records upstream projects that materially influence its architecture or interaction design.
|
||||
|
||||
## Omarchy
|
||||
|
||||
- Project: [basecamp/omarchy](https://github.com/basecamp/omarchy)
|
||||
- Reviewed revision: `f32ebbdb730c4e8fe11e4046cef4267e466264ea`
|
||||
- License: MIT, copyright David Heinemeier Hansson
|
||||
- Local reference at review time: `/home/gib/Documents/Code/Projects/omarchy`
|
||||
|
||||
### Adopted ideas, rebuilt for Panama
|
||||
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
### Candidates still under evaluation
|
||||
|
||||
- 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.
|
||||
|
||||
These are research directions, not commitments. They should only land when they improve the daily-driver experience without adding visible complexity or parallel configuration systems.
|
||||
@@ -21,8 +21,9 @@ fi
|
||||
gsettings set org.gnome.desktop.screensaver lock-enabled false
|
||||
gsettings set org.gnome.desktop.session idle-delay 0
|
||||
|
||||
# Run scripts
|
||||
for script in ~/.local/share/Panama/setup/scripts/*; do source $script; done
|
||||
# Run each setup stage in its own process. This keeps strict-shell options and
|
||||
# helper variables local to the script that owns them.
|
||||
for script in ~/.local/share/Panama/setup/scripts/*; do "$script"; done
|
||||
|
||||
# Revert to normal idle settings
|
||||
gsettings set org.gnome.desktop.screensaver lock-enabled true
|
||||
|
||||
@@ -91,6 +91,25 @@ fi
|
||||
# Panama-native applications live in the user data directory so launchers can
|
||||
# discover them alongside system desktop entries. Keep each authored file in
|
||||
# the repository and expose it with a narrow per-file symlink.
|
||||
# Panama ships its own application icons. Without these the desktop entries fall
|
||||
# back to a generic symbolic glyph, which is drawn for 16px toolbar use and
|
||||
# looks wrong beside full-colour application icons in the dock.
|
||||
PANAMA_ICON_DIR="$PANAMA_PATH/config/local/share/icons/hicolor/scalable/apps"
|
||||
USER_ICON_DIR="$HOME/.local/share/icons/hicolor/scalable/apps"
|
||||
mkdir -p "$USER_ICON_DIR"
|
||||
for icon_file in "$PANAMA_ICON_DIR"/*.svg; do
|
||||
[[ -e "$icon_file" ]] || continue
|
||||
icon_name="$(basename "$icon_file")"
|
||||
icon_target="$USER_ICON_DIR/$icon_name"
|
||||
if [[ -L "$icon_target" ]]; then
|
||||
rm "$icon_target"
|
||||
elif [[ -e "$icon_target" ]]; then
|
||||
mv "$icon_target" "$icon_target.bak"
|
||||
fi
|
||||
ln -s "$icon_file" "$icon_target"
|
||||
done
|
||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
||||
|
||||
PANAMA_APPLICATION_DIR="$PANAMA_PATH/config/local/share/applications"
|
||||
USER_APPLICATION_DIR="$HOME/.local/share/applications"
|
||||
mkdir -p "$USER_APPLICATION_DIR"
|
||||
|
||||
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
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Every pinned application must resolve to an installed desktop entry.
|
||||
#
|
||||
# DockBody drops a pin it cannot resolve rather than drawing a broken icon,
|
||||
# which is the right behaviour at runtime and a terrible one to debug: a typo in
|
||||
# the shipped list, or an application that changed its desktop id, simply
|
||||
# removes an icon from the dock with nothing logged.
|
||||
#
|
||||
# Also asserts the Settings entry resolves and carries Panama's own icon, since
|
||||
# it is the first pin and the one most likely to be wrong after a rename.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
harness="$repo_dir/config/dot/quickshell/dock-pin-harness.qml"
|
||||
config_home="$(mktemp -d /tmp/panama-dockpins.XXXXXX)"
|
||||
|
||||
fail() {
|
||||
printf 'dock pins contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"; }
|
||||
harness_pid=""
|
||||
|
||||
cleanup() {
|
||||
# By PID, never `pkill -f dock-pin-harness`: that pattern also matches the
|
||||
# shell running this script.
|
||||
[[ -n "$harness_pid" ]] && kill "$harness_pid" >/dev/null 2>&1 || true
|
||||
rm -rf "$config_home"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
XDG_CONFIG_HOME="$config_home" qs -p "$harness" --daemonize >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
run ipc show 2>/dev/null | rg -q '^target dock-pin-test$' && break
|
||||
sleep 0.1
|
||||
done
|
||||
run ipc show 2>/dev/null | rg -q '^target dock-pin-test$' || fail 'test IPC target did not start'
|
||||
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
|
||||
|
||||
# DesktopEntries populates asynchronously; asking before it has finished reports
|
||||
# every pin as missing, which is a false failure rather than a real one.
|
||||
loaded=0
|
||||
for _ in $(seq 1 60); do
|
||||
loaded="$(run ipc call dock-pin-test count 2>/dev/null || printf 0)"
|
||||
[[ "$loaded" =~ ^[0-9]+$ ]] && (( loaded > 0 )) && break
|
||||
sleep 0.25
|
||||
done
|
||||
(( loaded > 0 )) || fail 'no desktop entries were discovered at all'
|
||||
|
||||
state="$(run ipc call dock-pin-test resolve)"
|
||||
|
||||
missing="$(jq -r '.missing | join(", ")' <<<"$state")"
|
||||
[[ -z "$missing" ]] || fail "these pinned applications do not resolve and would vanish from the dock: $missing"
|
||||
|
||||
jq -e '.first.id == "panama-settings" and .first.found == true' <<<"$state" >/dev/null \
|
||||
|| fail "Panama Settings is not the first pin, or does not resolve: $(jq -c .first <<<"$state")"
|
||||
jq -e '.first.icon == "panama-settings"' <<<"$state" >/dev/null \
|
||||
|| fail "Settings is not using Panama's own icon: $(jq -r .first.icon <<<"$state")"
|
||||
|
||||
icon="$HOME/.local/share/icons/hicolor/scalable/apps/panama-settings.svg"
|
||||
[[ -r "$icon" ]] || fail "the icon it names is not installed at $icon"
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'dock pins contract: PASS (%s pins, all resolve)\n' "$(jq -r .total <<<"$state")"
|
||||
@@ -47,12 +47,16 @@ cat >"$scratch/bin/qs" <<'SH'
|
||||
printf 'qs' >>"$OSD_TEST_LOG"
|
||||
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
|
||||
printf '\n' >>"$OSD_TEST_LOG"
|
||||
[[ ${OSD_TEST_FAIL_QS:-false} != true ]]
|
||||
SH
|
||||
|
||||
chmod +x "$scratch/bin/"*
|
||||
|
||||
run_helper() {
|
||||
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" "$helper" "$@"
|
||||
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \
|
||||
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
|
||||
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
|
||||
"$helper" "$@"
|
||||
}
|
||||
|
||||
assert_line() {
|
||||
@@ -91,4 +95,17 @@ run_helper media next
|
||||
assert_line 'playerctl <next>'
|
||||
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'
|
||||
|
||||
Regular → Executable
+2
@@ -17,6 +17,8 @@ fail() {
|
||||
[[ -f "$osd" ]] || fail 'Prism OSD surface is missing'
|
||||
[[ -f "$state" ]] || fail 'OSD presentation state is missing'
|
||||
[[ -f "$qs_dir/modules/osd/qmldir" ]] || fail 'OSD module manifest is missing'
|
||||
rg -q '^import Quickshell$' "$state" \
|
||||
|| fail 'OSD singleton does not import the Quickshell Singleton type'
|
||||
|
||||
rg -Fq 'import qs.modules.osd' "$shell_file" || fail 'shell does not import the OSD module'
|
||||
[[ "$(rg -c '^[[:space:]]*Osd \{\}' "$shell_file")" -eq 1 ]] \
|
||||
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/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 [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} && -s $PANAMA_ACTION_TEST_INHIBITOR_FILE ]]; then
|
||||
while IFS= read -r pid; do
|
||||
printf 'Panama 1000 gib %s systemd-inhibit sleep:idle Caffeine block\n' "$pid"
|
||||
done <"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
|
||||
exit 0
|
||||
fi
|
||||
if [[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]; then
|
||||
printf 'Panama 1000 gib 42 systemd-inhibit sleep:idle Caffeine block\n'
|
||||
fi
|
||||
EOF
|
||||
|
||||
make_recorder kill <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf 'kill' >>"$PANAMA_ACTION_TEST_LOG"
|
||||
printf ' <%s>' "$@" >>"$PANAMA_ACTION_TEST_LOG"
|
||||
printf '\n' >>"$PANAMA_ACTION_TEST_LOG"
|
||||
if [[ -n ${PANAMA_ACTION_TEST_INHIBITOR_FILE:-} ]]; then
|
||||
: >"$PANAMA_ACTION_TEST_INHIBITOR_FILE"
|
||||
fi
|
||||
EOF
|
||||
|
||||
make_recorder pgrep <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
[[ ${PANAMA_ACTION_TEST_CONFIRMED_STATE:-${PANAMA_ACTION_TEST_STATE:-true}} == true ]]
|
||||
EOF
|
||||
|
||||
make_recorder id <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '1000\n'
|
||||
EOF
|
||||
|
||||
run_action() {
|
||||
: >"$command_log"
|
||||
HOME="$work/home" PATH="$fake_bin:$PATH" \
|
||||
PANAMA_ACTION_HELPER_DIR="$fake_bin" \
|
||||
PANAMA_ACTION_KILL_COMMAND="$fake_bin/kill" \
|
||||
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 \
|
||||
PANAMA_ACTION_CONFIRM_DELAY=0 \
|
||||
PANAMA_ACTION_TEST_LOG="$command_log" \
|
||||
"$dispatcher" "$1"
|
||||
}
|
||||
|
||||
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>'
|
||||
|
||||
printf '4242\n' >"$work/inhibitors"
|
||||
PANAMA_ACTION_TEST_STATE=false PANAMA_ACTION_TEST_INHIBITOR_FILE="$work/inhibitors" run_action caffeine
|
||||
assert_commands $'qs <ipc> <call> <caffeine> <toggle>\nkill <4242>\npanama-osd <message> <weather-clear-night-symbolic> <Caffeine Off>'
|
||||
|
||||
run_action night-light
|
||||
assert_commands $'qs <ipc> <call> <night-light> <toggle>\npanama-osd <message> <night-light-symbolic> <Night Light On>'
|
||||
|
||||
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[@]}"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
gallery="$repo_dir/config/dot/quickshell/prism-gallery.qml"
|
||||
launcher="$repo_dir/config/dot/quickshell/scripts/panama-prism-gallery"
|
||||
|
||||
fail() {
|
||||
printf 'Prism gallery contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$gallery" ]] || fail 'the real-component gallery is missing'
|
||||
[[ -x "$launcher" ]] || fail 'the gallery launcher is missing or not executable'
|
||||
|
||||
rg -Fq 'import qs.widgets' "$gallery" || fail 'gallery does not use the shared widget module'
|
||||
for component in Pill Toggle ValueSlider ThemedIcon PrismEdge; do
|
||||
rg -q "^[[:space:]]*${component}[[:space:]]*\\{" "$gallery" \
|
||||
|| fail "gallery does not render the real ${component} component"
|
||||
done
|
||||
|
||||
rg -Fq 'title: "Panama Prism Gallery"' "$gallery" \
|
||||
|| fail 'gallery does not expose a stable compositor window title'
|
||||
rg -Fq 'FloatingWindow {' "$gallery" \
|
||||
|| fail 'gallery must be a normal tiled application window'
|
||||
|
||||
if rg -q '#[0-9a-fA-F]{3,8}' "$gallery"; then
|
||||
fail 'gallery hardcodes a colour instead of using the Theme vocabulary'
|
||||
fi
|
||||
if rg -q 'loops:[[:space:]]*(Animation\.Infinite|-1)|running:[[:space:]]*true' "$gallery"; then
|
||||
fail 'gallery contains an always-running animation'
|
||||
fi
|
||||
|
||||
rg -Fq 'qs -p "$gallery" --daemonize' "$launcher" \
|
||||
|| fail 'launcher does not start the isolated Quickshell gallery config'
|
||||
rg -Fq 'qs -p "$gallery" kill' "$launcher" \
|
||||
|| fail 'launcher cannot close an already-open gallery instance'
|
||||
|
||||
printf 'Prism gallery contract: PASS\n'
|
||||
Reference in New Issue
Block a user