Polish clipboard history display
This commit is contained in:
@@ -34,9 +34,10 @@ Item {
|
|||||||
property int contentMargin: Style.spacing.panelPadding
|
property int contentMargin: Style.spacing.panelPadding
|
||||||
property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2)
|
property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2)
|
||||||
property int contentSpacing: Style.spacing.md
|
property int contentSpacing: Style.spacing.md
|
||||||
property int cardWidth: Math.min(Style.space(800), panel.width - Style.gapsOut * 2)
|
property int cardWidth: Math.min(Style.space(875), panel.width - Style.gapsOut * 2)
|
||||||
property int cardHeight: Math.min(Style.space(600), panel.height - Style.gapsOut * 2)
|
property int cardHeight: Math.min(Style.space(600), panel.height - Style.gapsOut * 2)
|
||||||
property int rowHeight: Math.max(Style.space(50), Style.font.body + Style.font.caption + Style.spacing.rowPaddingX * 2)
|
property int rowHeight: Math.max(Style.space(50), Style.font.body + Style.font.caption + Style.spacing.rowPaddingX * 2)
|
||||||
|
property int historyLimit: 300
|
||||||
|
|
||||||
function open(payloadJson) {
|
function open(payloadJson) {
|
||||||
root.opened = true
|
root.opened = true
|
||||||
@@ -71,14 +72,14 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveHistory() {
|
function saveHistory() {
|
||||||
historyFile.setText(JSON.stringify(root.history.slice(0, 100), null, 2) + "\n")
|
historyFile.setText(JSON.stringify(root.history.slice(0, root.historyLimit), null, 2) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
function addClipboardEntry(entry) {
|
function addClipboardEntry(entry) {
|
||||||
var normalized = ClipboardHistory.normalizeEntry(entry)
|
var normalized = ClipboardHistory.normalizeEntry(entry)
|
||||||
if (!normalized) return
|
if (!normalized) return
|
||||||
|
|
||||||
root.history = ClipboardHistory.addEntry(root.history, normalized, 100)
|
root.history = ClipboardHistory.addEntry(root.history, normalized, root.historyLimit)
|
||||||
root.saveHistory()
|
root.saveHistory()
|
||||||
if (root.opened) root.rebuildDisplay()
|
if (root.opened) root.rebuildDisplay()
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,7 @@ Item {
|
|||||||
if (index < 0 || index >= displayModel.count) return
|
if (index < 0 || index >= displayModel.count) return
|
||||||
|
|
||||||
var row = displayModel.get(index)
|
var row = displayModel.get(index)
|
||||||
root.history = ClipboardHistory.removeEntryAt(root.history, row.index)
|
root.history = ClipboardHistory.removeEntryAt(root.history, row.historyIndex)
|
||||||
root.saveHistory()
|
root.saveHistory()
|
||||||
|
|
||||||
if (displayModel.count <= 1) {
|
if (displayModel.count <= 1) {
|
||||||
@@ -138,7 +139,7 @@ Item {
|
|||||||
previewImage: row.previewImage ? Util.fileUrl(row.previewImage) : "",
|
previewImage: row.previewImage ? Util.fileUrl(row.previewImage) : "",
|
||||||
path: row.path,
|
path: row.path,
|
||||||
mime: row.mime,
|
mime: row.mime,
|
||||||
index: row.index
|
historyIndex: row.index
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ Item {
|
|||||||
function setFilter(nextFilter) {
|
function setFilter(nextFilter) {
|
||||||
root.filterText = nextFilter
|
root.filterText = nextFilter
|
||||||
root.selectedIndex = 0
|
root.selectedIndex = 0
|
||||||
root.cursorActive = false
|
root.cursorActive = true
|
||||||
root.rebuildDisplay()
|
root.rebuildDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ Item {
|
|||||||
if (row.entryType === "image") {
|
if (row.entryType === "image") {
|
||||||
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-file", row.mime, row.path])
|
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-file", row.mime, row.path])
|
||||||
} else if (row.fullText) {
|
} else if (row.fullText) {
|
||||||
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-text", "--shift-insert", "--history-index", String(row.index)])
|
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-text", "--shift-insert", "--history-index", String(row.historyIndex)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +198,7 @@ Item {
|
|||||||
if (row.entryType === "image") {
|
if (row.entryType === "image") {
|
||||||
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-file", "--copy-only", row.mime, row.path])
|
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-file", "--copy-only", row.mime, row.path])
|
||||||
} else if (row.fullText) {
|
} else if (row.fullText) {
|
||||||
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-text", "--copy-only", "--history-index", String(row.index)])
|
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-text", "--copy-only", "--history-index", String(row.historyIndex)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ Item {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: initProc
|
id: initProc
|
||||||
command: ["bash", "-c", "mkdir -p ~/.local/state/omarchy\nscript=$1\nfor pid in $(pgrep -x wl-paste || true); do\n cmdline=$(tr '\\0' ' ' <\"/proc/$pid/cmdline\" 2>/dev/null || true)\n if [[ $cmdline == *\"wl-paste --watch $script \"* || $cmdline == *\"wl-paste --type \"*\" --watch $script \"* ]]; then\n kill \"$pid\" 2>/dev/null || true\n fi\ndone", "clipboard-init", root.captureScript]
|
command: ["bash", "-c", "mkdir -p ~/.local/state/omarchy\nscript=$1\nfor pid in $(pgrep -x wl-paste || true); do\n cmdline=$(tr '\\0' ' ' <\"/proc/$pid/cmdline\" 2>/dev/null || true)\n if [[ $cmdline == *\"wl-paste --watch $script \"* || $cmdline == *\"wl-paste --type \"*\" --watch $script \"* || $cmdline == *\"/shell/plugins/clipboard/capture.sh\"* ]]; then\n kill \"$pid\" 2>/dev/null || true\n fi\ndone", "clipboard-init", root.captureScript]
|
||||||
onExited: {
|
onExited: {
|
||||||
currentProc.command = [root.captureScript]
|
currentProc.command = [root.captureScript]
|
||||||
currentProc.running = true
|
currentProc.running = true
|
||||||
@@ -372,84 +373,89 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: root.contentSpacing
|
spacing: 0
|
||||||
|
|
||||||
ListView {
|
Item {
|
||||||
id: resultList
|
width: parent.width / 2
|
||||||
width: parent.width / 2 - root.contentSpacing / 2
|
|
||||||
height: parent.height
|
height: parent.height
|
||||||
model: displayModel
|
|
||||||
clip: true
|
clip: true
|
||||||
spacing: Style.space(4)
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
ListView {
|
||||||
required property int index
|
id: resultList
|
||||||
required property string entryType
|
anchors.fill: parent
|
||||||
required property string previewText
|
anchors.rightMargin: root.contentMargin
|
||||||
required property string fullText
|
model: displayModel
|
||||||
required property string previewImage
|
clip: true
|
||||||
|
spacing: Style.space(4)
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
|
delegate: Rectangle {
|
||||||
|
required property int index
|
||||||
|
required property string entryType
|
||||||
|
required property string previewText
|
||||||
|
required property string fullText
|
||||||
|
required property string previewImage
|
||||||
|
|
||||||
width: ListView.view.width
|
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
|
||||||
height: root.rowHeight
|
|
||||||
radius: root.cornerRadius
|
|
||||||
color: hasCursor ? root.selectedBackground : "transparent"
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
width: ListView.view.width
|
||||||
anchors.fill: parent
|
height: root.rowHeight
|
||||||
anchors.leftMargin: Style.space(12)
|
radius: root.cornerRadius
|
||||||
anchors.rightMargin: Style.space(12)
|
color: hasCursor ? root.selectedBackground : "transparent"
|
||||||
anchors.topMargin: Style.space(8)
|
border.width: 0
|
||||||
anchors.bottomMargin: Style.space(8)
|
|
||||||
spacing: Style.space(10)
|
|
||||||
|
|
||||||
Image {
|
Row {
|
||||||
visible: parent.parent.entryType === "image"
|
anchors.fill: parent
|
||||||
width: visible ? parent.height : 0
|
anchors.leftMargin: Style.space(12)
|
||||||
height: parent.height
|
anchors.rightMargin: Style.space(12)
|
||||||
source: parent.parent.previewImage
|
anchors.topMargin: Style.space(8)
|
||||||
fillMode: Image.PreserveAspectFit
|
anchors.bottomMargin: Style.space(8)
|
||||||
asynchronous: true
|
spacing: Style.space(10)
|
||||||
smooth: true
|
|
||||||
|
Image {
|
||||||
|
visible: parent.parent.entryType === "image"
|
||||||
|
width: visible ? parent.height : 0
|
||||||
|
height: parent.height
|
||||||
|
source: parent.parent.previewImage
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width - (parent.parent.entryType === "image" ? parent.height + parent.spacing : 0)
|
||||||
|
height: parent.height
|
||||||
|
text: parent.parent.previewText
|
||||||
|
color: parent.parent.hasCursor ? root.selectedText : root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.title
|
||||||
|
opacity: parent.parent.entryType === "image" ? 0.72 : 1.0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
MouseArea {
|
||||||
width: parent.width - (parent.parent.entryType === "image" ? parent.height + parent.spacing : 0)
|
anchors.fill: parent
|
||||||
height: parent.height
|
hoverEnabled: true
|
||||||
text: parent.parent.previewText
|
cursorShape: Qt.PointingHandCursor
|
||||||
color: parent.parent.hasCursor ? root.selectedText : root.foreground
|
onContainsMouseChanged: if (containsMouse) {
|
||||||
font.family: root.fontFamily
|
root.cursorActive = true
|
||||||
font.pixelSize: Style.font.title
|
root.selectedIndex = index
|
||||||
font.italic: parent.parent.entryType === "image"
|
}
|
||||||
opacity: parent.parent.entryType === "image" ? 0.72 : 1.0
|
onClicked: {
|
||||||
elide: Text.ElideRight
|
root.cursorActive = true
|
||||||
wrapMode: Text.NoWrap
|
root.selectedIndex = index
|
||||||
verticalAlignment: Text.AlignVCenter
|
root.activateIndex(index)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onContainsMouseChanged: if (containsMouse) {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectedIndex = index
|
|
||||||
}
|
|
||||||
onClicked: {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectedIndex = index
|
|
||||||
root.activateIndex(index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width / 2 - root.contentSpacing / 2
|
width: parent.width / 2
|
||||||
height: parent.height
|
height: parent.height
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
@@ -466,7 +472,10 @@ Item {
|
|||||||
Text {
|
Text {
|
||||||
visible: parent.activeRow && parent.activeRow.entryType === "text"
|
visible: parent.activeRow && parent.activeRow.entryType === "text"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.space(16)
|
anchors.leftMargin: root.contentMargin
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.topMargin: 0
|
||||||
|
anchors.bottomMargin: 0
|
||||||
text: parent.activeRow ? parent.activeRow.fullText : ""
|
text: parent.activeRow ? parent.activeRow.fullText : ""
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
@@ -479,7 +488,10 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
visible: parent.activeRow && parent.activeRow.entryType === "image"
|
visible: parent.activeRow && parent.activeRow.entryType === "image"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.space(16)
|
anchors.leftMargin: root.contentMargin
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.topMargin: 0
|
||||||
|
anchors.bottomMargin: 0
|
||||||
source: parent.activeRow ? parent.activeRow.previewImage : ""
|
source: parent.activeRow ? parent.activeRow.previewImage : ""
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
verticalAlignment: Image.AlignTop
|
verticalAlignment: Image.AlignTop
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ function normalizeEntry(value) {
|
|||||||
if (type === "image") {
|
if (type === "image") {
|
||||||
var path = String(value.path || "")
|
var path = String(value.path || "")
|
||||||
if (!path) return null
|
if (!path) return null
|
||||||
return {
|
var entry = {
|
||||||
type: "image",
|
type: "image",
|
||||||
path: path,
|
path: path,
|
||||||
mime: String(value.mime || "image/png")
|
mime: String(value.mime || "image/png")
|
||||||
}
|
}
|
||||||
|
if (value.capturedAt !== undefined && value.capturedAt !== null)
|
||||||
|
entry.capturedAt = String(value.capturedAt)
|
||||||
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -88,13 +91,21 @@ function parseEntryJson(line) {
|
|||||||
|
|
||||||
function searchableText(entry) {
|
function searchableText(entry) {
|
||||||
if (!entry) return ""
|
if (!entry) return ""
|
||||||
if (entry.type === "image") return "image " + String(entry.mime || "")
|
if (entry.type === "image") return "image screenshot " + String(entry.mime || "") + " " + String(entry.capturedAt || "")
|
||||||
return String(entry.text || "")
|
return String(entry.text || "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imagePreviewText(entry) {
|
||||||
|
var timestamp = String(entry && entry.capturedAt || "")
|
||||||
|
if (!timestamp) return "Image"
|
||||||
|
|
||||||
|
var label = String(entry && entry.mime || "") === "image/png" ? "Screenshot" : "Image"
|
||||||
|
return label + " " + timestamp
|
||||||
|
}
|
||||||
|
|
||||||
function previewText(entry) {
|
function previewText(entry) {
|
||||||
if (!entry) return ""
|
if (!entry) return ""
|
||||||
if (entry.type === "image") return "Image"
|
if (entry.type === "image") return imagePreviewText(entry)
|
||||||
return String(entry.text || "").replace(/\s+/g, " ")
|
return String(entry.text || "").replace(/\s+/g, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +151,7 @@ if (typeof module !== "undefined") {
|
|||||||
parseEntryJson: parseEntryJson,
|
parseEntryJson: parseEntryJson,
|
||||||
searchableText: searchableText,
|
searchableText: searchableText,
|
||||||
previewText: previewText,
|
previewText: previewText,
|
||||||
|
imagePreviewText: imagePreviewText,
|
||||||
displayRows: displayRows
|
displayRows: displayRows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ emit_image() {
|
|||||||
mv "$tmp" "$file"
|
mv "$tmp" "$file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq -cn --arg mime "$mime" --arg path "$file" '{type:"image", mime:$mime, path:$path}'
|
jq -cn --arg mime "$mime" --arg path "$file" --arg captured_at "$(date +'%A %H:%M')" \
|
||||||
|
'{type:"image", mime:$mime, path:$path, capturedAt:$captured_at}'
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_image_stream() {
|
emit_image_stream() {
|
||||||
@@ -54,7 +55,8 @@ emit_image_stream() {
|
|||||||
mv "$tmp" "$file"
|
mv "$tmp" "$file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq -cn --arg mime "$mime" --arg path "$file" '{type:"image", mime:$mime, path:$path}'
|
jq -cn --arg mime "$mime" --arg path "$file" --arg captured_at "$(date +'%A %H:%M')" \
|
||||||
|
'{type:"image", mime:$mime, path:$path, capturedAt:$captured_at}'
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${OMARCHY_CLIPBOARD_WATCH_MIME:-}" in
|
case "${OMARCHY_CLIPBOARD_WATCH_MIME:-}" in
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ assertDeepEqual(
|
|||||||
'clipboard normalizes image entries with default mime'
|
'clipboard normalizes image entries with default mime'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
clipboard.normalizeEntry({ type: 'image', path: '/tmp/a.png', mime: 'image/png', capturedAt: 'Friday 14:42' }),
|
||||||
|
{ type: 'image', path: '/tmp/a.png', mime: 'image/png', capturedAt: 'Friday 14:42' },
|
||||||
|
'clipboard keeps image capture timestamps'
|
||||||
|
)
|
||||||
|
|
||||||
assertDeepEqual(
|
assertDeepEqual(
|
||||||
clipboard.parseHistory(JSON.stringify(['one', '', { type: 'text', text: 'two' }, { type: 'image', path: '/tmp/a.jpg', mime: 'image/jpeg' }])),
|
clipboard.parseHistory(JSON.stringify(['one', '', { type: 'text', text: 'two' }, { type: 'image', path: '/tmp/a.jpg', mime: 'image/jpeg' }])),
|
||||||
[
|
[
|
||||||
@@ -65,6 +71,18 @@ assertDeepEqual(
|
|||||||
'clipboard display rows search image metadata'
|
'clipboard display rows search image metadata'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
clipboard.displayRows([{ type: 'image', path: '/tmp/a.png', mime: 'image/png', capturedAt: 'Friday 14:42' }], '', 50)[0].previewText,
|
||||||
|
'Screenshot Friday 14:42',
|
||||||
|
'clipboard labels timestamped png image entries as screenshots'
|
||||||
|
)
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
clipboard.displayRows([{ type: 'image', path: '/tmp/a.jpg', mime: 'image/jpeg', capturedAt: 'Friday 14:42' }], '', 50)[0].previewText,
|
||||||
|
'Image Friday 14:42',
|
||||||
|
'clipboard labels timestamped non-png image entries as images'
|
||||||
|
)
|
||||||
|
|
||||||
assertDeepEqual(
|
assertDeepEqual(
|
||||||
clipboard.displayRows(history, 'image', 50).map(row => row.index),
|
clipboard.displayRows(history, 'image', 50).map(row => row.index),
|
||||||
[2],
|
[2],
|
||||||
|
|||||||
Reference in New Issue
Block a user