Luigit
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 };
}