Build the Panama Hyprland desktop

This commit is contained in:
Gabriel Brown
2026-08-17 10:32:55 -04:00
parent 67033f2a31
commit 5248883e4b
190 changed files with 18554 additions and 34 deletions
@@ -0,0 +1,171 @@
// One row of clipboard history: what was copied, where it came from, and how
// long ago. Clicking it puts the entry back on the clipboard.
//
// Rows are cards on an already-glass surface, so they get no prism edge — the
// hairline marks a pane of glass, not every item sitting on one.
import QtQuick
import qs.config
import qs.services
Rectangle {
id: root
required property var entry
signal activated
implicitHeight: Math.max(52, body.implicitHeight + 18)
radius: Theme.cardRadius
// border.width: 0 works around QTBUG-137166, where transparent rounded
// rectangles can render with holes at the corners.
border.width: 0
color: {
if (mouse.pressed)
return Theme.alpha(Theme.accent, Theme.activeAlpha);
if (mouse.containsMouse)
return Theme.alpha(Theme.fg, Theme.hoverAlpha);
return "transparent";
}
// Fast in, relaxed out — the row should feel like it answers the pointer
// rather than fading after it.
Behavior on color {
ColorAnimation {
duration: mouse.containsMouse ? Theme.durFast : Theme.durNormal
easing.type: Easing.OutQuad
}
}
// Nerd Font glyph for the kind of thing that was copied. Text is the common
// case and gets the quietest icon, so the odd link or image stands out.
readonly property string kindGlyph: {
switch (root.entry.kind) {
case Clipboard.kindLink:
return "\u{F0C1}"; // nf-fa-link
case Clipboard.kindImage:
return "\u{F03E}"; // nf-fa-picture_o
case Clipboard.kindFile:
return "\u{F016}"; // nf-fa-file_o
default:
return "\u{F15C}"; // nf-fa-file_text_o
}
}
// Where it came from. Vicinae leaves `source` empty for clients it cannot
// attribute (anything pasted by a script, for one), in which case a link's
// host is more informative than nothing.
readonly property string origin: root.entry.source || root.entry.host || ""
Text {
id: glyph
anchors.left: parent.left
anchors.leftMargin: 12
anchors.top: parent.top
anchors.topMargin: 11
text: root.entry.encrypted ? "\u{F023}" : root.kindGlyph
// fontMono is the Nerd Font, used here only to draw an icon.
font.family: Theme.fontMono
font.pixelSize: 14
color: root.entry.pinned ? Theme.accent : Theme.fgMuted
}
Column {
id: body
anchors.left: glyph.right
anchors.leftMargin: 10
anchors.right: timeLabel.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.entry.encrypted ? "Concealed by Vicinae" : root.entry.preview
color: root.entry.encrypted ? Theme.fgMuted : Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.italic: root.entry.encrypted
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
Row {
spacing: 6
visible: chip.visible || originLabel.visible
// Images and file drags have no text worth reading, so the type is
// spelled out rather than left to the glyph alone.
Rectangle {
id: chip
visible: !root.entry.textual
width: chipLabel.implicitWidth + 12
height: chipLabel.implicitHeight + 4
radius: 4
border.width: 0
color: Theme.alpha(Theme.fg, 0.10)
Text {
id: chipLabel
anchors.centerIn: parent
text: root.entry.typeLabel
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
}
Text {
id: originLabel
anchors.verticalCenter: chip.verticalCenter
visible: root.origin !== ""
text: root.origin
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
}
Row {
id: timeLabel
anchors.right: parent.right
anchors.rightMargin: 12
anchors.top: parent.top
anchors.topMargin: 11
spacing: 5
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.entry.pinned
text: "\u{F0403}" // nf-md-pin
font.family: Theme.fontMono
font.pixelSize: 11
color: Theme.accent
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Clipboard.relativeTime(root.entry.ts)
color: Theme.fgMuted
font.family: Theme.fontFamily
// The list is full of these stacked in a column; tabular figures
// keep them from stepping sideways row to row.
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activated()
}
}
@@ -0,0 +1,230 @@
// Clipboard history, backed by Vicinae's existing database. Opening the panel
// performs one read-only query; nothing polls or repaints while it is closed.
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.config
import qs.services
import qs.widgets
import qs.modules.quicksettings
PanelWindow {
id: root
visible: ShellState.clipboardOpen
color: "transparent"
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight + Theme.barGap * 2
right: Theme.barSideMargin
}
exclusiveZone: 0
implicitWidth: 430
implicitHeight: 550
WlrLayershell.namespace: "qs-popover-clipboard"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
property string filter: ""
readonly property var filteredEntries: Clipboard.entries.filter(entry => {
const needle = root.filter.trim().toLowerCase();
if (!needle)
return true;
return [entry.preview, entry.source, entry.host, entry.typeLabel]
.join(" ").toLowerCase().includes(needle);
})
function close(): void {
ShellState.close();
}
function activateFirst(): void {
if (root.filteredEntries.length === 0)
return;
Clipboard.copyEntry(root.filteredEntries[0].id);
root.close();
}
onVisibleChanged: {
if (root.visible) {
root.filter = "";
search.clear();
Clipboard.refresh();
openAnim.restart();
Qt.callLater(search.grab);
}
}
HyprlandFocusGrab {
windows: [root]
active: root.visible
onCleared: root.close()
}
Rectangle {
id: surface
anchors.fill: parent
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
left: parent.left
right: parent.right
topMargin: 1
}
inset: parent.radius
}
transform: Scale {
id: openScale
origin.x: surface.width
origin.y: 0
xScale: 1
yScale: 1
}
Column {
anchors.fill: parent
anchors.margins: Theme.popoverPadding
spacing: Theme.sectionSpacing
Item {
width: parent.width
height: 30
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 9
ThemedIcon {
anchors.verticalCenter: parent.verticalCenter
size: 17
icon: "edit-paste-symbolic"
iconFallback: "edit-copy-symbolic"
tint: Theme.accent
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Clipboard"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.DemiBold
}
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Clipboard.entries.length === 1
? "1 item"
: Clipboard.entries.length + " items"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
SearchField {
id: search
width: parent.width
placeholder: "Search clipboard history"
onTextChanged: root.filter = text
onAccepted: root.activateFirst()
onDismissed: root.close()
}
Rectangle {
width: parent.width
height: 1
border.width: 0
color: Theme.alpha(Theme.fg, 0.1)
}
Item {
width: parent.width
height: parent.height - y
Text {
anchors.centerIn: parent
visible: Clipboard.loading
text: "Loading clipboard history…"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Text {
anchors.centerIn: parent
width: parent.width - 40
visible: !Clipboard.loading && !Clipboard.available
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
text: "Clipboard history is unavailable. Start Vicinae to restore it."
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Text {
anchors.centerIn: parent
visible: !Clipboard.loading && Clipboard.available
&& root.filteredEntries.length === 0
text: root.filter ? "No matching clipboard items" : "Nothing copied yet"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
ScrollColumn {
anchors.fill: parent
maxHeight: parent.height
spacing: 4
visible: !Clipboard.loading && Clipboard.available
&& root.filteredEntries.length > 0
Repeater {
model: root.filteredEntries
ClipboardEntry {
required property var modelData
width: parent.width
entry: modelData
onActivated: {
Clipboard.copyEntry(modelData.id);
root.close();
}
}
}
}
}
}
}
NumberAnimation {
id: openAnim
target: openScale
property: "yScale"
from: 0.96
to: 1
duration: Theme.durFast
easing.type: Easing.OutCubic
}
}
@@ -0,0 +1,24 @@
// The bar entry for clipboard history. The icon is intentionally familiar and
// quiet: the history itself is a utility, not another permanent status readout.
import QtQuick
import qs.config
import qs.services
import qs.widgets
Pill {
id: root
implicitWidth: 30
implicitHeight: 28
onActivated: ShellState.toggle("clipboard")
ThemedIcon {
anchors.verticalCenter: parent.verticalCenter
size: 16
icon: "edit-paste-symbolic"
iconFallback: "edit-copy-symbolic"
tint: ShellState.clipboardOpen ? Theme.accent : Theme.fg
}
}
@@ -0,0 +1,86 @@
// The filter box at the top of the clipboard popover. Nothing is submitted:
// the text is read live and narrows the list that is already loaded, which is
// how GNOME's own search entries behave.
import QtQuick
import qs.config
Rectangle {
id: root
property alias text: input.text
property string placeholder: "Search"
signal accepted
signal dismissed
implicitHeight: 32
radius: Theme.pillRadius
// border.width is set below rather than left at 0, so the focus ring has
// something to change. See QTBUG-137166 for why 0 matters elsewhere.
color: Theme.alpha(Theme.fg, 0.07)
border.width: 1
border.color: input.activeFocus ? Theme.alpha(Theme.accent, 0.55) : "transparent"
Behavior on border.color {
ColorAnimation {
duration: Theme.durFast
}
}
function clear(): void {
input.text = "";
}
function grab(): void {
input.forceActiveFocus();
}
Text {
id: glyph
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "\u{F002}" // nf-fa-search — fontMono is the Nerd Font, icon only
font.family: Theme.fontMono
font.pixelSize: 12
color: input.activeFocus ? Theme.accent : Theme.fgMuted
}
TextInput {
id: input
anchors.left: glyph.right
anchors.leftMargin: 9
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
color: Theme.fg
selectionColor: Theme.alpha(Theme.accent, 0.5)
selectedTextColor: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
clip: true
onAccepted: root.accepted()
// Escape clears a non-empty filter first and only closes the popover on
// a second press — losing a typed query to a stray Escape is worse than
// one extra keystroke.
Keys.onEscapePressed: {
if (input.text !== "")
input.text = "";
else
root.dismissed();
}
Text {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
visible: input.text === ""
text: root.placeholder
color: Theme.fgMuted
font: input.font
}
}
}
@@ -0,0 +1,5 @@
module qs.modules.clipboard
ClipboardEntry 1.0 ClipboardEntry.qml
ClipboardPanel 1.0 ClipboardPanel.qml
ClipboardWidget 1.0 ClipboardWidget.qml
SearchField 1.0 SearchField.qml