// launch.js — pure app-launcher logic: normalize, fuzzy rank, desktop-entry mapping. // NO I/O. Dual-mode: node `require` (tests) + QML import. var GLYPH_APP = "\uF135"; // rocket fallback when themed icon misses function normalize(text) { return String(text == null ? "" : text).trim().toLowerCase(); } function prefixIntent(raw) { return { intent: "app", query: String(raw == null ? "" : raw).trim() }; } function isWordChar(ch) { return /[a-z0-9]/.test(ch); } function subsequenceMatch(q, t) { var qi = 0; var positions = []; for (var ti = 0; ti < t.length && qi < q.length; ti++) { if (t.charAt(ti) === q.charAt(qi)) { positions.push(ti); qi++; } } if (qi < q.length) return { matched: false, firstIndex: -1, contiguous: 0, boundaryHits: 0 }; var contiguous = 0; var boundaryHits = 0; for (var i = 0; i < positions.length; i++) { var p = positions[i]; if (i > 0 && positions[i] === positions[i - 1] + 1) contiguous++; if (p === 0 || !isWordChar(t.charAt(p - 1))) boundaryHits++; } return { matched: true, firstIndex: positions[0], contiguous: contiguous, boundaryHits: boundaryHits }; } function fuzzyScore(query, title, boost) { var b = Number(boost) || 0; var q = normalize(query); var t = normalize(title); if (q.length === 0) return b; if (t === q) return b + 1000; if (t.indexOf(q) === 0) return b + 800 - t.length; var m = subsequenceMatch(q, t); if (m.matched) { var allBoundary = m.boundaryHits === q.length; var base = allBoundary ? 600 : 350; return b + base + m.contiguous * 10 + m.boundaryHits * 15 - m.firstIndex; } return -1; } function list(value) { var out = []; var src = value || []; for (var i = 0; i < src.length; i++) { var text = String(src[i] || "").trim(); if (text.length > 0) out.push(text); } return out; } function commandList(value) { var out = []; var src = value || []; for (var i = 0; i < src.length; i++) out.push(String(src[i])); return out; } function primaryCategory(categories) { var skip = { Qt: true, KDE: true, GNOME: true, GTK: true, Application: true, Core: true }; for (var i = 0; i < categories.length; i++) { var cat = categories[i]; if (!skip[cat]) return cat; } return categories[0] || "App"; } function buildDesktopActions(actions) { var rows = []; var src = actions || []; for (var i = 0; i < src.length; i++) { var a = src[i]; if (!a || !a.name) continue; rows.push({ id: String(a.id || a.name), title: String(a.name), icon: String(a.icon || ""), exec: String(a.execString || ""), execArgv: commandList(a.command) }); } return rows; } function buildApps(entries) { var rows = []; var src = entries || []; for (var i = 0; i < src.length; i++) { var e = src[i]; if (!e || !e.name) continue; var id = String(e.id || e.name); var categories = list(e.categories); var keywords = list(e.keywords); var genericName = String(e.genericName || ""); var comment = String(e.comment || ""); var kind = genericName || primaryCategory(categories); var category = primaryCategory(categories); var actions = buildDesktopActions(e.actions); rows.push({ id: "app:" + id, title: String(e.name), subtitle: kind + (category && category !== kind ? " · " + category : ""), genericName: genericName, comment: comment, desktopId: id, desktopPath: id + ".desktop", startupClass: String(e.startupClass || ""), exec: String(e.execString || ""), execArgv: commandList(e.command), workingDirectory: String(e.workingDirectory || ""), icon: String(e.icon || ""), terminal: !!e.runInTerminal, categories: categories, keywords: keywords, actions: actions, details: [ { label: "category", value: category }, { label: "keywords", value: keywords.slice(0, 4).join("; ") || "—" }, { label: "terminal", value: e.runInTerminal ? "yes" : "no" } ], searchText: [e.name, genericName, comment, id, e.startupClass].concat(categories).concat(keywords).join(" "), route: "desktop-entry" }); } rows.sort(function (a, b) { return String(a.title).localeCompare(String(b.title)); }); return rows; } function shapeRow(row, provider, score) { var next = Object.assign({}, row || {}); next.provider = "app"; next.iconRole = "desktop-entry"; next.verb = "open"; next.glyph = GLYPH_APP; next.score = score; next.keyboard = true; return next; } function rankResults(rawQuery, sources) { var apps = (sources || {}).apps || []; var intent = prefixIntent(rawQuery); var query = intent.query; var results = []; for (var i = 0; i < apps.length; i++) { var app = apps[i]; var score = Math.max( fuzzyScore(query, app.title, 900), fuzzyScore(query, app.searchText || app.title, 760) ); if (score >= 0) results.push(shapeRow(app, "app", score)); } results.sort(function (x, y) { return y.score - x.score || String(x.title).localeCompare(String(y.title)); }); return { results: results, selectedIndex: results.length > 0 ? 0 : -1, providerIntent: intent.intent }; } if (typeof module !== 'undefined' && module.exports) { module.exports = { normalize: normalize, prefixIntent: prefixIntent, subsequenceMatch: subsequenceMatch, fuzzyScore: fuzzyScore, buildDesktopActions: buildDesktopActions, buildApps: buildApps, shapeRow: shapeRow, rankResults: rankResults }; }