repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/clip.js
Raw// clip.js — pure clipboard logic: normalize, content-typing, sort, filter, shapers.
// NO I/O. Dual-mode: node `require` (tests) + QML import. Same pattern as caldav.js/sysinfo.js.
// --- Nerd Font glyphs (verified to render). Kept as \uXXXX escapes. ---
var ICON_IMAGE = "\uF03E"; // image
var ICON_COLOR = "\uF043"; // color (tint)
var ICON_URL = "\uF0C1"; // url (link)
var ICON_PATH = "\uF15B"; // path (file)
var ICON_TEXT = "\uF0F6"; // text (file-text)
// --- lowercase, null-safe (search/dedupe key) ---
function normalize(text) {
return String(text == null ? "" : text).toLowerCase();
}
// --- reject empty / oversized / mostly-control-character (binary) captures ---
function isReadableText(text) {
var value = String(text == null ? "" : text);
if (value.length === 0 || value.length > 65536) return false;
var bad = 0;
for (var i = 0; i < value.length; i++) {
var code = value.charCodeAt(i);
if ((code >= 0 && code < 9) || (code > 13 && code < 32) || code === 65533) bad++;
}
return bad <= Math.max(2, Math.floor(value.length / 100));
}
// --- FNV-1a-ish XOR+imul hash. Literals kept identical to the original QML so
// existing clip-<hash> dedupe IDs survive across restart. ---
function hashText(text) {
var value = String(text == null ? "" : text);
var hash = 2.16614e+09;
for (var i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i);
hash = Math.imul(hash, 1.67776e+07);
}
return (hash >>> 0).toString(16);
}
// --- first non-empty-trimmed line of a value ---
function firstLine(text) {
return String(text == null ? "" : text).split(/\r?\n/)[0].trim();
}
// --- a single-line path-like token (spaces allowed — screenshots have them) ---
function looksLikePath(value) {
if (/[\n\r\t]/.test(value)) return false;
return /^(\/|~\/|\.\/|\.\.\/|file:\/\/)/.test(value);
}
// --- temp /tmp image path OR any path ending in an image extension ---
function isImagePath(text) {
var value = String(text == null ? "" : text).trim();
if (!looksLikePath(value)) return false;
return /\.(png|jpg|jpeg|webp|gif)$/i.test(value);
}
// --- a single http(s) / www URL token ---
function isUrl(text) {
var value = String(text == null ? "" : text).trim();
if (/\s/.test(value)) return false;
return /^(https?:\/\/|www\.)\S+$/i.test(value);
}
// --- clamp an integer string into a 0..255 byte ---
function clampByte(s) {
var n = parseInt(s, 10);
if (!isFinite(n)) return 0;
return n < 0 ? 0 : (n > 255 ? 255 : n);
}
function byteHex(n) {
var h = n.toString(16);
return h.length < 2 ? "0" + h : h;
}
// --- parse #rgb / #rgba / #rrggbb / #rrggbbaa and rgb()/rgba() into "#rrggbb".
// Shorthand is expanded; alpha is dropped. Returns null on anything else. ---
function parseColor(text) {
var v = String(text == null ? "" : text).trim().toLowerCase();
var hex = /^#([0-9a-f]{3,8})$/.exec(v);
if (hex) {
var h = hex[1];
if (h.length === 3 || h.length === 4) {
h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; // expand, drop shorthand alpha
} else if (h.length === 6) {
// already #rrggbb
} else if (h.length === 8) {
h = h.slice(0, 6); // drop alpha
} else {
return null; // length 5 or 7 — not a valid color
}
return "#" + h;
}
var rgb = /^rgba?\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*(?:,\s*[0-9.]+\s*)?\)$/.exec(v);
if (rgb) {
return "#" + byteHex(clampByte(rgb[1])) + byteHex(clampByte(rgb[2])) + byteHex(clampByte(rgb[3]));
}
return null;
}
// --- content-type classification. Precedence: image, color, url, path, else text. ---
function detectKind(text) {
if (isImagePath(text)) return { kind: "image", icon: ICON_IMAGE };
if (parseColor(text) !== null) return { kind: "color", icon: ICON_COLOR };
if (isUrl(text)) return { kind: "url", icon: ICON_URL };
if (looksLikePath(text)) return { kind: "path", icon: ICON_PATH };
return { kind: "text", icon: ICON_TEXT };
}
// --- title for a row: "Image" for image paths, else the (truncated) first line ---
function rowTitle(text) {
if (isImagePath(text)) return "Image";
var line = firstLine(text);
if (line.length === 0) return "Clipboard";
return line.length > 64 ? line.slice(0, 61) + "…" : line;
}
// --- "just now" | "Nm" | "Nh" | "Nd" relative age ---
function ageLabel(createdAt, now) {
var t = Number(createdAt);
var ref = (now == null ? Date.now() : Number(now));
if (!isFinite(t) || !isFinite(ref)) return "just now";
var diff = ref - t;
if (diff < 60000) return "just now";
var minutes = Math.floor(diff / 60000);
if (minutes < 60) return minutes + "m";
var hours = Math.floor(diff / 3600000);
if (hours < 24) return hours + "h";
return Math.floor(diff / 86400000) + "d";
}
// --- pinned first, then createdAt descending (recency). Returns a new array. ---
function sortRows(rows) {
var list = (rows || []).slice();
list.sort(function (a, b) {
var pa = a && a.pinned ? 1 : 0;
var pb = b && b.pinned ? 1 : 0;
if (pa !== pb) return pb - pa;
return Number(b && b.createdAt || 0) - Number(a && a.createdAt || 0);
});
return list;
}
// --- substring match on title+body, then sortRows ---
function filterRows(rows, query) {
var q = normalize(query);
var matched = [];
var list = rows || [];
for (var i = 0; i < list.length; i++) {
var row = list[i];
if (q.length === 0 ||
normalize(row.title).indexOf(q) >= 0 ||
normalize(row.body).indexOf(q) >= 0) {
matched.push(row);
}
}
return sortRows(matched);
}
// --- view + launcher row shape. Launcher-compatible: id,title,body,pinned,provider,route
// (the service adds subtitle/keyboard). `now` is injectable for deterministic age. ---
function cloneRow(row, now) {
var src = row || {};
var body = String(src.body == null ? "" : src.body);
var kind = detectKind(body);
var isImage = kind.kind === "image";
var color = kind.kind === "color" ? parseColor(body) : null;
return {
id: String(src.id == null ? "" : src.id),
title: isImage ? "Image" : rowTitle(body),
body: body,
pinned: !!src.pinned,
kind: kind.kind,
icon: kind.icon,
isImage: isImage,
imagePath: isImage ? body.trim() : null,
swatch: color,
age: ageLabel(src.createdAt, now),
provider: "clipboard",
route: "clipboard"
};
}
// --- durable persistence shape (clipboard-history.json rows) ---
function persistedRow(row) {
var src = row || {};
return {
id: String(src.id == null ? "" : src.id),
title: String(src.title == null ? "Clipboard" : src.title),
body: String(src.body == null ? "" : src.body),
pinned: !!src.pinned,
createdAt: Number(src.createdAt == null ? Date.now() : src.createdAt)
};
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { normalize: normalize, isReadableText: isReadableText,
hashText: hashText, firstLine: firstLine, rowTitle: rowTitle,
isImagePath: isImagePath, parseColor: parseColor, detectKind: detectKind,
sortRows: sortRows, filterRows: filterRows, ageLabel: ageLabel,
cloneRow: cloneRow, persistedRow: persistedRow };
}