This is a projection, not a development branch. The tree above was constructed from the internal source named below under a manifest that decides which paths may leave, then scanned as a whole tree rather than as a series of patches, and only then published. Public history starts here because the history before it was not admissible, and neither was the tree. What used to stand in this repository included a rescue copy of another machine, a directory of phone handoffs, deployment wired to one house, and a submodule pointing at a forge no stranger can reach. None of that was ever the product. It stays in the private forge, which is allowed to hold the whole working organism, and this is what was deliberately sent out instead. Three mechanisms produced this tree, in decreasing order of trust. A top-level path the manifest does not name never arrives at all, which is the one that catches directories nobody has thought of yet. Named internal files inside admitted roots are dropped. A short, reviewed table replaces deployment defaults that a public build must not carry -- an endpoint aimed at one LAN, a VPN profile belonging to one phone, packaging built from one checkout path. Everything after this commit is an ordinary publication with the same three trailers, so a force push stops being routine and starts meaning that something deliberate happened. The trailers bind the projection to its source without pretending the public SHA is the private one: same lineage, different tree, and the record says so. Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047 Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27 Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
205 lines
6.9 KiB
QML
205 lines
6.9 KiB
QML
pragma Singleton
|
|
|
|
import qs.modules.common
|
|
import qs.modules.common.functions
|
|
import Quickshell
|
|
|
|
/**
|
|
* - Eases fuzzy searching for applications by name
|
|
* - Guesses icon name for window class name
|
|
*/
|
|
Singleton {
|
|
id: root
|
|
property bool sloppySearch: Config.options?.search.sloppy ?? false
|
|
property real scoreThreshold: 0.2
|
|
property var substitutions: ({
|
|
"code-url-handler": "visual-studio-code",
|
|
"Code": "visual-studio-code",
|
|
"gnome-tweaks": "org.gnome.tweaks",
|
|
"pavucontrol-qt": "pavucontrol",
|
|
"wps": "wps-office2019-kprometheus",
|
|
"wpsoffice": "wps-office2019-kprometheus",
|
|
"footclient": "foot",
|
|
})
|
|
property var regexSubstitutions: [
|
|
{
|
|
"regex": /^steam_app_(\d+)$/,
|
|
"replace": "steam_icon_$1"
|
|
},
|
|
{
|
|
"regex": /Minecraft.*/,
|
|
"replace": "minecraft"
|
|
},
|
|
{
|
|
"regex": /.*polkit.*/,
|
|
"replace": "system-lock-screen"
|
|
},
|
|
{
|
|
"regex": /gcr.prompter/,
|
|
"replace": "system-lock-screen"
|
|
}
|
|
]
|
|
|
|
// Deduped list to fix double icons
|
|
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
|
|
.filter((app, index, self) =>
|
|
index === self.findIndex((t) => (
|
|
t.id === app.id
|
|
))
|
|
)
|
|
|
|
readonly property var preppedNames: list.map(a => ({
|
|
name: Fuzzy.prepare(`${a.name} `),
|
|
entry: a
|
|
}))
|
|
|
|
readonly property var preppedIcons: list.map(a => ({
|
|
name: Fuzzy.prepare(`${a.icon} `),
|
|
entry: a
|
|
}))
|
|
|
|
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
|
|
if (root.sloppySearch) {
|
|
return root.levenshteinQuery(search);
|
|
}
|
|
|
|
return Fuzzy.go(search, preppedNames, {
|
|
all: true,
|
|
key: "name"
|
|
}).map(r => {
|
|
return r.obj.entry
|
|
});
|
|
}
|
|
|
|
function levenshteinQuery(search: string): var {
|
|
const prepared = BitwiseFuzzy.prepare(search);
|
|
return BitwiseFuzzy.search(prepared, list, { key: "name", threshold: root.scoreThreshold });
|
|
}
|
|
|
|
function iconExists(iconName) {
|
|
if (!iconName || iconName.length == 0) return false;
|
|
return (Quickshell.iconPath(iconName, true).length > 0)
|
|
&& !iconName.includes("image-missing");
|
|
}
|
|
|
|
function getReverseDomainNameAppName(str) {
|
|
return str.split('.').slice(-1)[0]
|
|
}
|
|
|
|
function getKebabNormalizedAppName(str) {
|
|
return str.toLowerCase().replace(/\s+/g, "-");
|
|
}
|
|
|
|
function getUndescoreToKebabAppName(str) {
|
|
return str.toLowerCase().replace(/_/g, "-");
|
|
}
|
|
|
|
/**
|
|
* Resolve a window app_id / pinned appId to its DesktopEntry.
|
|
*
|
|
* DesktopEntries.heuristicLookup() matches the entry id and
|
|
* StartupWMClass, which covers most apps but NOT the ones that append an
|
|
* instance suffix to their Wayland app_id. Firefox is the reference case:
|
|
* it derives its remoting name from the profile, so the window reports
|
|
* "firefox-default" while the entry is "firefox" with
|
|
* StartupWMClass=firefox. heuristicLookup returns null, and a dock tap on
|
|
* a pinned-but-not-running app then calls execute() on null — a silent
|
|
* dead tap that reports no error anywhere.
|
|
*
|
|
* So: fall back to trimming trailing "-segment" pieces, longest match
|
|
* first, then to a StartupWMClass prefix scan. Returns null only when
|
|
* nothing in the entry set plausibly owns the id.
|
|
*/
|
|
function resolveEntry(appId) {
|
|
if (!appId || appId.length == 0) return null;
|
|
|
|
const direct = DesktopEntries.heuristicLookup(appId);
|
|
if (direct) return direct;
|
|
|
|
// "firefox-default" -> "firefox"; "signal-desktop-beta" -> "signal-desktop"
|
|
let candidate = appId;
|
|
while (candidate.includes("-")) {
|
|
candidate = candidate.slice(0, candidate.lastIndexOf("-"));
|
|
const trimmed = DesktopEntries.heuristicLookup(candidate);
|
|
if (trimmed) return trimmed;
|
|
}
|
|
|
|
// Last resort: an entry whose StartupWMClass prefixes the app_id.
|
|
const lowered = appId.toLowerCase();
|
|
for (const entry of root.list) {
|
|
const wmClass = entry.startupClass;
|
|
if (!wmClass || wmClass.length == 0) continue;
|
|
if (lowered.startsWith(wmClass.toLowerCase())) return entry;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function guessIcon(str) {
|
|
if (!str || str.length == 0) return "image-missing";
|
|
|
|
// Quickshell's desktop entry lookup
|
|
const entry = DesktopEntries.byId(str);
|
|
if (entry) return entry.icon;
|
|
|
|
// Normal substitutions
|
|
if (substitutions[str]) return substitutions[str];
|
|
if (substitutions[str.toLowerCase()]) return substitutions[str.toLowerCase()];
|
|
|
|
// Regex substitutions
|
|
for (let i = 0; i < regexSubstitutions.length; i++) {
|
|
const substitution = regexSubstitutions[i];
|
|
const replacedName = str.replace(
|
|
substitution.regex,
|
|
substitution.replace,
|
|
);
|
|
if (replacedName != str) return replacedName;
|
|
}
|
|
|
|
// Icon exists -> return as is
|
|
if (iconExists(str)) return str;
|
|
|
|
|
|
// Simple guesses
|
|
const lowercased = str.toLowerCase();
|
|
if (iconExists(lowercased)) return lowercased;
|
|
|
|
const reverseDomainNameAppName = getReverseDomainNameAppName(str);
|
|
if (iconExists(reverseDomainNameAppName)) return reverseDomainNameAppName;
|
|
|
|
const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase();
|
|
if (iconExists(lowercasedDomainNameAppName)) return lowercasedDomainNameAppName;
|
|
|
|
const kebabNormalizedGuess = getKebabNormalizedAppName(str);
|
|
if (iconExists(kebabNormalizedGuess)) return kebabNormalizedGuess;
|
|
|
|
const undescoreToKebabGuess = getUndescoreToKebabAppName(str);
|
|
if (iconExists(undescoreToKebabGuess)) return undescoreToKebabGuess;
|
|
|
|
// Search in desktop entries
|
|
const iconSearchResults = Fuzzy.go(str, preppedIcons, {
|
|
all: true,
|
|
key: "name"
|
|
}).map(r => {
|
|
return r.obj.entry
|
|
});
|
|
if (iconSearchResults.length > 0) {
|
|
const guess = iconSearchResults[0].icon
|
|
if (iconExists(guess)) return guess;
|
|
}
|
|
|
|
const nameSearchResults = root.fuzzyQuery(str);
|
|
if (nameSearchResults.length > 0) {
|
|
const guess = nameSearchResults[0].icon
|
|
if (iconExists(guess)) return guess;
|
|
}
|
|
|
|
// Desktop entry lookup, suffix-tolerant so "firefox-default" and
|
|
// friends land on their real entry's icon instead of falling through.
|
|
const heuristicEntry = root.resolveEntry(str);
|
|
if (heuristicEntry) return heuristicEntry.icon;
|
|
|
|
// Give up
|
|
return "application-x-executable";
|
|
}
|
|
}
|