add crypto

This commit is contained in:
zack 2025-07-22 20:21:21 -04:00
parent 90cbe489f6
commit af6a3bce3e
Signed by: zoey
GPG key ID: 81FB9FECDD6A33E2
120 changed files with 24616 additions and 462 deletions

View file

@ -0,0 +1,515 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Quickshell
import Quickshell.Io
import "root:/Data" as Data
// Clipboard history manager with cliphist integration
Item {
id: root
required property var shell
property string selectedWidget: "cliphist"
property bool isVisible: false
property real bgOpacity: 0.0
transformOrigin: Item.Center
function show() { showAnimation.start() }
function hide() { hideAnimation.start() }
function toggle() { isVisible ? hide() : show() }
// Smooth show/hide animations
ParallelAnimation {
id: showAnimation
PropertyAction { target: root; property: "isVisible"; value: true }
PropertyAnimation { target: root; property: "opacity"; from: 0.0; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
PropertyAnimation { target: root; property: "scale"; from: 0.9; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
}
ParallelAnimation {
id: hideAnimation
PropertyAnimation { target: root; property: "opacity"; to: 0.0; duration: 150; easing.type: Easing.InCubic }
PropertyAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.InCubic }
PropertyAction { target: root; property: "isVisible"; value: false }
}
ColumnLayout {
id: contentColumn
anchors.fill: parent
spacing: 12
// Header
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 30
Label {
text: "Clipboard History"
font.pixelSize: 16
font.weight: Font.Medium
color: Data.ThemeManager.fgColor
Layout.fillWidth: true
}
Button {
id: clearButton
text: "Clear"
implicitWidth: 60
implicitHeight: 25
background: Rectangle {
radius: 12
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.8)
}
contentItem: Label {
text: parent.text
font.pixelSize: 11
color: Data.ThemeManager.fgColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
clearClipboardHistory()
clickScale.target = clearButton
clickScale.start()
}
}
}
// Scrollable clipboard history list
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
interactive: true
visible: cliphistList.contentHeight > cliphistList.height
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: parent.pressed ? Data.ThemeManager.accentColor
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
}
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: cliphistList
model: cliphistModel
spacing: 6
cacheBuffer: 50 // Memory optimization
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 1500
// Smooth scrolling behavior
property real targetY: contentY
Behavior on targetY {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
onTargetYChanged: {
if (!moving && !dragging) {
contentY = targetY
}
}
delegate: Rectangle {
width: cliphistList.width
height: Math.max(50, contentText.contentHeight + 20)
radius: 8
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 1
// View optimization - only render visible items
visible: y + height > cliphistList.contentY - height &&
y < cliphistList.contentY + cliphistList.height + height
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// Content type icon
Label {
text: model.type === "image" ? "🖼️" : model.type === "url" ? "🔗" : "📝"
font.pixelSize: 16
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Label {
id: contentText
text: model.type === "image" ? "[Image Data]" :
(model.content.length > 100 ? model.content.substring(0, 100) + "..." : model.content)
font.pixelSize: 12
color: Data.ThemeManager.fgColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
elide: Text.ElideRight
maximumLineCount: 4
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
Label {
text: model.type === "image" ? "Image" : (model.content.length + " chars")
font.pixelSize: 10
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
copyToClipboard(model.id, model.type)
clickScale.target = parent
clickScale.start()
}
}
}
}
// Empty state message
Label {
anchors.centerIn: parent
text: "No clipboard history\nCopy something to get started"
font.pixelSize: 14
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
horizontalAlignment: Text.AlignHCenter
visible: cliphistList.count === 0
opacity: 0.7
}
}
}
}
}
// Click feedback animation
NumberAnimation {
id: clickScale
property Item target
properties: "scale"
from: 0.95
to: 1.0
duration: 150
easing.type: Easing.OutCubic
}
ListModel { id: cliphistModel }
property var currentEntries: []
// Main cliphist process for fetching clipboard history
Process {
id: cliphistProcess
command: ["cliphist", "list"]
running: false
property var tempEntries: []
onRunningChanged: {
if (running) {
tempEntries = []
} else {
// Process completed, apply smart diff update
updateModelIfChanged(tempEntries)
}
}
stdout: SplitParser {
onRead: data => {
try {
const line = data.toString().trim()
// Skip empty lines and error messages
if (line === "" || line.includes("ERROR") || line.includes("WARN") ||
line.includes("error:") || line.includes("warning:")) {
return
}
// Parse cliphist output format: ID + spaces + content
const match = line.match(/^(\d+)\s+(.+)$/)
if (match) {
const id = match[1]
const content = match[2]
cliphistProcess.tempEntries.push({
id: id,
content: content,
type: detectContentType(content)
})
} else {
console.log("Failed to parse line:", line)
}
} catch (e) {
console.error("Error parsing cliphist line:", e)
}
}
}
}
// Clear entire clipboard history
Process {
id: clearCliphistProcess
command: ["cliphist", "wipe"]
running: false
onRunningChanged: {
if (!running) {
cliphistModel.clear()
currentEntries = []
console.log("Clipboard history cleared")
}
}
stderr: SplitParser {
onRead: data => {
console.error("Clear clipboard error:", data.toString())
}
}
}
// Delete specific clipboard entry
Process {
id: deleteEntryProcess
property string entryId: ""
command: ["cliphist", "delete-query", entryId]
running: false
onRunningChanged: {
if (!running && entryId !== "") {
// Remove deleted entry from model
for (let i = 0; i < cliphistModel.count; i++) {
if (cliphistModel.get(i).id === entryId) {
cliphistModel.remove(i)
currentEntries = currentEntries.filter(entry => entry.id !== entryId)
break
}
}
console.log("Deleted entry:", entryId)
entryId = ""
}
}
stderr: SplitParser {
onRead: data => {
console.error("Delete entry error:", data.toString())
}
}
}
// Copy plain text to clipboard
Process {
id: copyTextProcess
property string textToCopy: ""
command: ["wl-copy", textToCopy]
running: false
stderr: SplitParser {
onRead: data => {
console.error("wl-copy error:", data.toString())
}
}
}
// Copy from clipboard history
Process {
id: copyHistoryProcess
property string entryId: ""
command: ["sh", "-c", "printf '%s' '" + entryId + "' | cliphist decode | wl-copy"]
running: false
stderr: SplitParser {
onRead: data => {
console.error("Copy history error:", data.toString())
}
}
}
// Periodic refresh timer (disabled by default)
Timer {
id: refreshTimer
interval: 30000
running: false // Only enable when needed
repeat: true
onTriggered: {
if (!cliphistProcess.running && root.isVisible) {
refreshClipboardHistory()
}
}
}
// Component initialization
Component.onCompleted: {
refreshClipboardHistory()
}
onIsVisibleChanged: {
if (isVisible && cliphistModel.count === 0) {
refreshClipboardHistory()
}
}
// Smart model update - only changes when content differs
function updateModelIfChanged(newEntries) {
// Quick length check
if (newEntries.length !== currentEntries.length) {
updateModel(newEntries)
return
}
// Compare content for changes
let hasChanges = false
for (let i = 0; i < newEntries.length; i++) {
if (i >= currentEntries.length ||
newEntries[i].id !== currentEntries[i].id ||
newEntries[i].content !== currentEntries[i].content) {
hasChanges = true
break
}
}
if (hasChanges) {
updateModel(newEntries)
}
}
// Efficient model update with scroll position preservation
function updateModel(newEntries) {
const scrollPos = cliphistList.contentY
// Remove obsolete items
for (let i = cliphistModel.count - 1; i >= 0; i--) {
const modelItem = cliphistModel.get(i)
const found = newEntries.some(entry => entry.id === modelItem.id)
if (!found) {
cliphistModel.remove(i)
}
}
// Add or update items
for (let i = 0; i < newEntries.length; i++) {
const newEntry = newEntries[i]
let found = false
// Check if item exists and update position
for (let j = 0; j < cliphistModel.count; j++) {
const modelItem = cliphistModel.get(j)
if (modelItem.id === newEntry.id) {
if (modelItem.content !== newEntry.content) {
cliphistModel.set(j, newEntry)
}
if (j !== i && i < cliphistModel.count) {
cliphistModel.move(j, i, 1)
}
found = true
break
}
}
// Add new item
if (!found) {
if (i < cliphistModel.count) {
cliphistModel.insert(i, newEntry)
} else {
cliphistModel.append(newEntry)
}
}
}
// Restore scroll position
cliphistList.contentY = scrollPos
currentEntries = newEntries.slice()
}
// Content type detection based on patterns
function detectContentType(content) {
// Binary/image data detection
if (content.includes('\x00') || content.startsWith('\x89PNG') || content.startsWith('\xFF\xD8\xFF')) {
return "image"
}
if (content.includes('[[ binary data ') || content.includes('<selection>')) {
return "image"
}
// URL detection
if (/^https?:\/\/\S+$/.test(content.trim())) return "url"
// Code detection
if (content.includes('\n') && (content.includes('{') || content.includes('function') || content.includes('=>'))) return "code"
// Command detection
if (content.startsWith('sudo ') || content.startsWith('pacman ') || content.startsWith('apt ')) return "command"
return "text"
}
function formatTimestamp(timestamp) {
const now = new Date()
const entryDate = new Date(parseInt(timestamp))
const diff = (now - entryDate) / 1000
if (diff < 60) return "Just now"
if (diff < 3600) return Math.floor(diff / 60) + " min ago"
if (diff < 86400) return Math.floor(diff / 3600) + " hour" + (Math.floor(diff / 3600) === 1 ? "" : "s") + " ago"
return Qt.formatDateTime(entryDate, "MMM d h:mm AP")
}
function clearClipboardHistory() {
clearCliphistProcess.running = true
}
function deleteClipboardEntry(entryId) {
deleteEntryProcess.entryId = entryId
deleteEntryProcess.running = true
}
function refreshClipboardHistory() {
cliphistProcess.running = true
}
// Copy handler - chooses appropriate method based on content type
function copyToClipboard(entryIdOrText, contentType) {
if (contentType === "image" || typeof entryIdOrText === "string" && entryIdOrText.match(/^\d+$/)) {
// Use cliphist decode for binary data and numbered entries
copyHistoryProcess.entryId = entryIdOrText
copyHistoryProcess.running = true
} else {
// Use wl-copy for plain text
copyTextProcess.textToCopy = entryIdOrText
copyTextProcess.running = true
}
}
// Clean up all processes on destruction
Component.onDestruction: {
cliphistProcess.running = false
clearCliphistProcess.running = false
deleteEntryProcess.running = false
copyTextProcess.running = false
copyHistoryProcess.running = false
}
}