summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2025-08-16 16:23:20 +0800
committerlittleblack111 <littleblack11111@gmail.com>2025-08-16 16:23:20 +0800
commit4fe12bbabfe2bb4c468c66348afedb34aee0b783 (patch)
tree904ce21bf2b39d988483d91a585a23ede5da424b
parentc4eca807dacd407af939c2200055acb4326ccfe0 (diff)
feat(Clip): metadatas(app it's taken from, timestamp)
Persistent and Directories are partially copied from end4-dots which is GPL Licensed
-rw-r--r--launcher/components/Clip.qml26
-rw-r--r--services/Clip.qml78
-rw-r--r--services/Persistent.qml27
-rw-r--r--services/TimeDate.qml48
-rw-r--r--utils/Directories.qml60
5 files changed, 233 insertions, 6 deletions
diff --git a/launcher/components/Clip.qml b/launcher/components/Clip.qml
index 568d508..29b5959 100644
--- a/launcher/components/Clip.qml
+++ b/launcher/components/Clip.qml
@@ -94,11 +94,29 @@ IComponent {
anchors.margins: Launcher.innerMargin * 2
spacing: Launcher.innerMargin * 2
- IText {
- text: modelData?.isImage ? "Image" : "Text" || ""
+ IconImage {
+ source: modelData?.appIcon
+ implicitSize: parent.height
}
- IText {
- text: modelData?.data || ""
+
+ Column {
+ IText {
+ text: modelData?.data || ""
+ font.pixelSize: Style.font.size.largerr
+ }
+ Row {
+ spacing: Launcher.innerMargin
+ IText {
+ text: modelData.isImage ? "Image" : "Text"
+ color: Colors.foreground3
+ font.pixelSize: Style.font.size.normal
+ }
+ IText {
+ text: TimeDate.sinceWhen(modelData?.timestamp) || ""
+ color: Colors.foreground3
+ font.pixelSize: Style.font.size.normal
+ }
+ }
}
}
diff --git a/services/Clip.qml b/services/Clip.qml
index d03976c..59c460b 100644
--- a/services/Clip.qml
+++ b/services/Clip.qml
@@ -2,8 +2,11 @@ pragma Singleton
import Quickshell
import Quickshell.Io
-import QtQuick
+import Quickshell.Wayland
+
import qs.components
+import qs.utils
+import qs.services
Searchable {
id: root
@@ -15,6 +18,8 @@ Searchable {
property list<string> _clipImg: []
property list<string> _clipImgIds: _clipImg.map(p => p.split("/").pop().split(".").shift())
+ property var _clipMetadata: ({})
+
list: {
if (!_clipHist?.length)
return [];
@@ -41,16 +46,45 @@ Searchable {
const idStr = idNum >= 0 ? "" + idNum : "";
const imgPath = idStr ? imgMap[idStr] || "" : "";
const isImg = !!imgPath;
+ const metadata = _clipMetadata[idStr] || {};
return {
index: idNum,
isImage: isImg,
data: isImg ? imgPath : payload,
- raw: raw
+ raw: raw,
+ timestamp: metadata.timestamp || "",
+ appId: metadata.appId || "",
+ appTitle: metadata.appTitle || "",
+ appIcon: Quickshell.iconPath(AppSearch.guessIcon(metadata.appId), "image-missing")
};
});
}
+ Persistent {
+ id: clipMetaStore
+ filePath: Directories.clipHistMetaDataPath
+ adapter: JsonAdapter {
+ property var metadata: ({})
+ }
+ fileView.onLoaded: {
+ const meta = adapter.metadata;
+ for (const k in meta) {
+ if (meta[k].timestamp && typeof meta[k].timestamp === "string") {
+ meta[k].timestamp = new Date(meta[k].timestamp);
+ }
+ }
+ root._clipMetadata = meta;
+ }
+ // fileView.onAdapterUpdated: {
+ // root._clipMetadata = adapter.metadata;
+ // }
+ }
+
+ on_ClipMetadataChanged: {
+ clipMetaStore.adapter.metadata = _clipMetadata;
+ }
+
function transformSearch(search) {
return search.toLowerCase();
}
@@ -68,18 +102,57 @@ Searchable {
imgProc.running = true;
}
+ function _captureMetadata() {
+ const now = new Date();
+ const activeToplevel = ToplevelManager.activeToplevel;
+
+ Qt.callLater(() => {
+ clipProc.running = true;
+ clipProc.stdout.onceFinished = () => {
+ const lines = clipProc.stdout.text.split(/\r?\n/);
+ const firstLine = lines[0] || "";
+ const tab = firstLine.indexOf("\t");
+
+ if (tab > 0) {
+ const idStr = firstLine.slice(0, tab);
+ const n = parseInt(idStr, 10);
+ if (!Number.isNaN(n)) {
+ var newMetadata = {};
+ for (var k in _clipMetadata) {
+ if (_clipMetadata.hasOwnProperty(k))
+ newMetadata[k] = _clipMetadata[k];
+ }
+
+ newMetadata[idStr] = {
+ timestamp: now,
+ appId: activeToplevel ? activeToplevel.appId : "",
+ appTitle: activeToplevel ? activeToplevel.title : ""
+ };
+ _clipMetadata = newMetadata;
+ }
+ }
+ };
+ });
+ }
+
Process {
id: clipProc
running: true
command: ["cliphist", "-preview-width", 9 ** 9, "-max-items", 9 ** 9, "list"]
stdout: StdioCollector {
property bool pending: false
+ property var onceFinished: null
+
onStreamFinished: () => {
pending = true;
Qt.callLater(() => {
if (pending) {
pending = false;
_clipHist = this.text.split(/\r?\n/);
+ if (onceFinished) {
+ onceFinished();
+ onceFinished = null;
+ }
}
});
}
@@ -113,6 +186,7 @@ Searchable {
stdout: SplitParser {
onRead: line => {
+ _captureMetadata();
_update();
}
}
diff --git a/services/Persistent.qml b/services/Persistent.qml
new file mode 100644
index 0000000..3a911bf
--- /dev/null
+++ b/services/Persistent.qml
@@ -0,0 +1,27 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.utils
+
+Scope {
+ id: root
+ property alias adapter: fileView.adapter
+ property alias fileView: fileView
+ property string fileDir: Directories.state
+ property string fileName: "states.json"
+ property string filePath: `${root.fileDir}/${root.fileName}`
+
+ FileView {
+ id: fileView
+ path: root.filePath
+ watchChanges: true
+ onFileChanged: reload()
+ onAdapterUpdated: writeAdapter()
+ onLoadFailed: error => {
+ console.log("Failed to load persistent states file:", error);
+ if (error == FileViewError.FileNotFound) {
+ writeAdapter();
+ }
+ }
+ }
+}
diff --git a/services/TimeDate.qml b/services/TimeDate.qml
index fab2beb..f066f46 100644
--- a/services/TimeDate.qml
+++ b/services/TimeDate.qml
@@ -27,6 +27,54 @@ Singleton {
readonly property string date: Qt.formatDateTime(clock.date, "dddd dd/MM")
+ function sinceWhen(d) {
+ if (typeof d === "string")
+ return;
+
+ var now = clock.date;
+
+ function startOfDay(x) {
+ return new Date(x.getFullYear(), x.getMonth(), x.getDate(), 0, 0, 0, 0);
+ }
+
+ function startOfWeek(x) {
+ var dow = x.getDay();
+ var mondayOffset = (dow + 6) % 7;
+ var sod = startOfDay(x);
+ sod.setDate(sod.getDate() - mondayOffset);
+ return sod;
+ }
+
+ function fmt12h(dateObj) {
+ var h24 = dateObj.getHours();
+ var m = dateObj.getMinutes();
+ var h12 = h24 % 12;
+ if (h12 === 0)
+ h12 = 12;
+ var ampm = h24 < 12 ? "AM" : "PM";
+ return ((h12 < 10 ? "0" : "") + h12 + ":" + (m < 10 ? "0" : "") + m + " " + ampm);
+ }
+
+ function weekdayName(dateObj) {
+ return Qt.formatDateTime(dateObj, "dddd");
+ }
+
+ var nowSOD = startOfDay(now).getTime();
+ var dSOD = startOfDay(d).getTime();
+ if (nowSOD === dSOD) {
+ return fmt12h(d);
+ }
+
+ var nowSOW = startOfWeek(now).getTime();
+ var dSOW = startOfWeek(d).getTime();
+ if (nowSOW === dSOW) {
+ return weekdayName(d) + " " + fmt12h(d);
+ }
+
+ var ymd = Qt.formatDateTime(d, "yyyy-MM-dd");
+ return ymd + " at " + fmt12h(d);
+ }
+
SystemClock {
id: clock
precision: SystemClock.Seconds
diff --git a/utils/Directories.qml b/utils/Directories.qml
new file mode 100644
index 0000000..f643a78
--- /dev/null
+++ b/utils/Directories.qml
@@ -0,0 +1,60 @@
+pragma Singleton
+// pragma ComponentBehavior: Bound
+
+import Qt.labs.platform
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+
+Singleton {
+ /**
+ * Trims the File protocol off the input string
+ * @param {string} str
+ * @returns {string}
+ */
+ function trimFileProtocol(str) {
+ return str.startsWith("file://") ? str.slice(7) : str;
+ }
+
+ /**
+ * Extracts the file name from a file path
+ * @param {string} str
+ * @returns {string}
+ */
+ function fileNameForPath(str) {
+ if (typeof str !== "string")
+ return "";
+ const trimmed = trimFileProtocol(str);
+ return trimmed.split(/[\\/]/).pop();
+ }
+
+ /**
+ * Removes the file extension from a file path or name
+ * @param {string} str
+ * @returns {string}
+ */
+ function trimFileExt(str) {
+ if (typeof str !== "string")
+ return "";
+ const trimmed = trimFileProtocol(str);
+ const lastDot = trimmed.lastIndexOf(".");
+ if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
+ return trimmed.slice(0, lastDot);
+ }
+ return trimmed;
+ }
+
+ // XDG Dirs, with "file://"
+ readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
+ readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
+ readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
+ readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
+ readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
+
+ property string clipHistMetaDataPath: `${state}/cliphist.json`
+
+ // Cleanup on init
+ Component.onCompleted: {
+ Quickshell.execDetached(["touch", trimFileProtocol(clipHistMetaDataPath)]);
+ }
+}