add crypto
This commit is contained in:
parent
90cbe489f6
commit
af6a3bce3e
120 changed files with 24616 additions and 462 deletions
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal file
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue