From a7fedd25c86aa83c291677e577089d500aac3316 Mon Sep 17 00:00:00 2001 From: yihao Date: Thu, 16 Jul 2026 23:07:36 +0800 Subject: [PATCH 1/3] feat: add Locate Files plugin --- locate-files/README.md | 125 +++++++++++ locate-files/indexer.luau | 285 ++++++++++++++++++++++++ locate-files/launcher.luau | 356 ++++++++++++++++++++++++++++++ locate-files/plugin.toml | 110 +++++++++ locate-files/thumbnail.webp | Bin 0 -> 14842 bytes locate-files/translations/en.json | 44 ++++ locate-files/update-index.sh | 51 +++++ 7 files changed, 971 insertions(+) create mode 100644 locate-files/README.md create mode 100644 locate-files/indexer.luau create mode 100644 locate-files/launcher.luau create mode 100644 locate-files/plugin.toml create mode 100644 locate-files/thumbnail.webp create mode 100644 locate-files/translations/en.json create mode 100644 locate-files/update-index.sh diff --git a/locate-files/README.md b/locate-files/README.md new file mode 100644 index 0000000..d783113 --- /dev/null +++ b/locate-files/README.md @@ -0,0 +1,125 @@ +# Locate Files + +A Noctalia v5 launcher provider that maintains a private index of the user's +home directory, searches it with `plocate`, falls back to `fzf` for typos, and +opens results with `xdg-open`. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `yihao/locate-files` | +| Entries | Launcher provider: `files`; service: `indexer` | +| Launcher prefix | `/f` | + +## Requirements + +- Noctalia 5.0.0 or newer +- `locate` and `updatedb` from `plocate` +- `fzf` +- `flock` from `util-linux` +- `xdg-open` + +The plugin creates and refreshes its own database; it does not use the system +database under `/var/lib/plocate/`. + +## Usage + +1. Enable `yihao/locate-files` in **Settings > Plugins**. +2. Open the launcher and type `/f` followed by part of a path or file name. +3. Select a result to open it in its default application. + +Dot-paths are hidden by default. Add search options after a `--` separator: + +```text +/f invoice -- --hidden +/f project -- --type file +/f cache -- --type directory +/f config -- --hidden --type=directory +/f invioce -- --fuzzy +``` + +Supported options are `-h`/`--hidden`, `--fuzzy`, and +`-t`/`--type file|directory`. Type aliases `f`, `d`, and `dir` are accepted. + +Use the launcher action below to rebuild the index immediately: + +```text +/f --reindex +``` + +Searches run asynchronously with a 250 ms debounce and return at most 50 paths +by default. Stale responses are discarded by Noctalia when the query changes. +Change the result limit from the plugin settings in **Settings > Plugins**. + +Substring search through `plocate` remains the default. Fuzzy search runs after +an exact query has no usable results, can be selected as the default backend, or +can be disabled except for `--fuzzy`. Fuzzy search needs at least three +characters and uses a bounded 150-candidate `fzf` result set by default. The +launcher scores those candidates with filename-first Noctalia fuzzy scores, a +small `fzf` rank bonus, optional file/directory weights, and a global-search +bias. It does not attempt to read an internal `fzf` score. + +The provider is disabled from unprefixed global search by default. Enable it in +Noctalia's launcher-provider configuration if desired; the **Global search score +bias** setting keeps weak file matches below applications. Advanced plugin +settings also expose the fuzzy candidate limit and file/directory score weights. + +The same settings page provides **Pruned paths**, **Pruned directory names**, and +an **Index refresh interval**. Pruned paths are omitted when the database is +built, so changing them triggers a rebuild. Relative prune paths are resolved +from the home directory; home-relative paths such as `~/Downloads` are also +supported. + +Pruned directory names apply at every depth and are literal, not regex or glob +patterns. Add `node_modules`, `.git`, or `__pycache__` to omit every matching +directory's contents. Names containing slashes or spaces are ignored; use +**Pruned paths** for a specific directory such as `Projects/example/bin`. + +## Settings + +| Setting | Default | Description | +| --- | --- | --- | +| Maximum results | `50` | Maximum launcher rows returned per query. | +| Pruned paths | `[]` | Home-relative or absolute directories excluded during indexing. | +| Pruned directory names | `[]` | Literal directory names excluded at every depth. | +| Index refresh interval | `60` minutes | Time between automatic index refreshes, from 1 minute to 7 days. | +| Fuzzy search | `fallback` | Fallback after no exact result, always fuzzy, or only `--fuzzy`. | +| Fuzzy candidate limit | `150` | Advanced: fzf candidates scored by the launcher, from 100 to 200. | +| Global search score bias | `-100` | Advanced: score adjustment when global launcher search is enabled. | +| File and directory score weights | `0` | Advanced: type-specific ranking adjustments. | + +## Private Index + +The `indexer` service builds the database automatically when it is missing and +refreshes it every 60 minutes by default. The frequency is configurable from the +plugin settings between 1 and 10080 minutes (7 days). + +## IPC + +Rebuild the private index immediately: + +```sh +noctalia msg plugin yihao/locate-files:indexer all reindex +``` + +The database, NUL-delimited `paths.cache`, lock, log, status, and metadata files +live under the directory returned by `noctalia.pluginDataDir()`. Index builds use +`umask 077`, enforce mode `0600` on both search artifacts, run detached, and are +serialized with `flock`. The cache is exported from the staged private database +and atomically replaced after every successful build. A previous generation +remains usable during routine refreshes. Searches pause during a +prune-configuration rebuild so removed paths are not served from an old index. + +## Implementation + +- `plugin.toml` declares the launcher, index service, dependencies, and settings. +- `indexer.luau` schedules builds and shares index status with the launcher. +- `update-index.sh` stages `updatedb` output and exports `paths.cache` outside + Noctalia's callback timeout. +- `launcher.luau` runs the private `locate` database by default, then uses + `fzf --read0 --print0 --scheme=path --filter` on `paths.cache` when selected. + It applies query options, ranks bounded candidates, and publishes launcher rows. +- Selecting a row opens its safely shell-quoted absolute path with `xdg-open`. +- `translations/en.json` supplies the settings and launcher text required by + the v5 manifest API. diff --git a/locate-files/indexer.luau b/locate-files/indexer.luau new file mode 100644 index 0000000..b6fda9e --- /dev/null +++ b/locate-files/indexer.luau @@ -0,0 +1,285 @@ +--!nonstrict + +local ACTIVE_POLL_MS = 5000 + +local dataDir = noctalia.pluginDataDir() +local pluginDir = noctalia.pluginDir() +local home = noctalia.getenv("HOME") + +local database = dataDir ~= nil and dataDir .. "/home.db" or nil +local pathsCache = dataDir ~= nil and dataDir .. "/paths.cache" or nil +local statusFile = dataDir ~= nil and dataDir .. "/index.status" or nil +local metadataFile = dataDir ~= nil and dataDir .. "/index-metadata.json" or nil +local helper = pluginDir ~= nil and pluginDir .. "/update-index.sh" or nil + +local indexing = false +local pending = false +local sequence = 0 +local activeToken = nil +local activeSignature = nil + +local function shellQuote(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function normalizePath(value) + local path = noctalia.string.trim(value) + if path == "" then + return nil + end + if path:sub(1, 1) ~= "/" and path:sub(1, 1) ~= "~" then + path = home .. "/" .. path + end + path = noctalia.expandPath(path) + if path ~= "/" then + path = path:gsub("/+$", "") + end + return path +end + +local function isWithin(path, root) + return root == "/" or path == root or path:sub(1, #root + 1) == root .. "/" +end + +local function configuredPrunes() + local paths = {} + for _, value in ipairs(noctalia.getConfig("blacklist_paths") or {}) do + local path = normalizePath(value) + if path ~= nil and isWithin(path, home) then + local redundant = false + for i = #paths, 1, -1 do + if isWithin(path, paths[i]) then + redundant = true + break + end + if isWithin(paths[i], path) then + table.remove(paths, i) + end + end + if not redundant then + table.insert(paths, path) + end + end + end + + if dataDir ~= nil and isWithin(dataDir, home) then + table.insert(paths, dataDir) + end + table.sort(paths) + return paths +end + +local function configuredPruneNames() + local names = {} + local seen = {} + for _, value in ipairs(noctalia.getConfig("blacklist_names") or {}) do + local name = noctalia.string.trim(value) + if name ~= "" and not name:find("/") and not name:find("%s") and not seen[name] then + seen[name] = true + table.insert(names, name) + end + end + table.sort(names) + return names +end + +local function configSignature(prunes, pruneNames) + return noctalia.json.encode({ root = home, prunes = prunes, pruneNames = pruneNames, formatVersion = 3 }) or "" +end + +local function readStatus() + local raw = statusFile ~= nil and noctalia.readFile(statusFile) or nil + if raw == nil then + return nil, nil, nil + end + local status, token, detail = noctalia.string.trim(raw):match("^([^|]+)|([^|]+)|?(.*)$") + return status, token, detail +end + +local function publish(status, detail) + noctalia.state.set("index_status", status) + noctalia.state.set("index_detail", detail or "") +end + +local function readMetadata() + local raw = metadataFile ~= nil and noctalia.readFile(metadataFile) or nil + if raw == nil then + return nil + end + local value = noctalia.json.decode(raw) + return type(value) == "table" and value or nil +end + +local function saveMetadata(signature) + local metadata = { + signature = signature, + builtAt = tonumber(noctalia.formatTime("%s")) or 0, + } + local encoded = noctalia.json.encode(metadata, true) + if encoded ~= nil then + noctalia.writeFile(metadataFile, encoded) + end +end + +local function buildCommand(token, prunes, pruneNames) + local parts = { + "sh", + shellQuote(helper), + shellQuote(database), + shellQuote(pathsCache), + shellQuote(statusFile), + shellQuote(token), + "updatedb", + "--require-visibility", "no", + "--database-root", shellQuote(home), + "--output", shellQuote(database .. ".tmp." .. token), + "--prunenames", "''", + "--prunepaths", "''", + } + for _, path in ipairs(prunes) do + table.insert(parts, "--add-single-prunepath") + table.insert(parts, shellQuote(path)) + end + for _, name in ipairs(pruneNames) do + table.insert(parts, "--add-prunenames") + table.insert(parts, shellQuote(name)) + end + return table.concat(parts, " ") +end + +local function startIndex() + if indexing then + pending = true + return + end + if database == nil or pathsCache == nil or statusFile == nil or metadataFile == nil or helper == nil or home == nil then + publish("error", "plugin directories are unavailable") + return + end + if not noctalia.commandExists("updatedb") or not noctalia.commandExists("locate") or not noctalia.commandExists("flock") then + publish("error", "updatedb, locate, or flock is not installed") + return + end + + local prunes = configuredPrunes() + local pruneNames = configuredPruneNames() + sequence += 1 + activeToken = noctalia.formatTime("%s") .. "-" .. tostring(sequence) + activeSignature = configSignature(prunes, pruneNames) + indexing = true + publish("building", "") + noctalia.setUpdateInterval(ACTIVE_POLL_MS) + + if not noctalia.runAsync(buildCommand(activeToken, prunes, pruneNames)) then + indexing = false + publish("error", "could not start updatedb") + end +end + +local function indexHasCurrentConfig() + if database == nil or pathsCache == nil or not noctalia.fileExists(database) or not noctalia.fileExists(pathsCache) then + return false + end + local metadata = readMetadata() + if metadata == nil then + return false + end + local signature = configSignature(configuredPrunes(), configuredPruneNames()) + if metadata.signature ~= signature then + return false + end + return true +end + +local function indexIsCurrent() + if not indexHasCurrentConfig() then + return false + end + local metadata = readMetadata() + local refreshMinutes = noctalia.getConfig("index_refresh_minutes") or 60 + local now = tonumber(noctalia.formatTime("%s")) or 0 + return now - (metadata.builtAt or 0) < refreshMinutes * 60 +end + +local function scheduleIdlePoll() + local metadata = readMetadata() + if metadata == nil or metadata.builtAt == nil then + noctalia.setUpdateInterval(60000) + return + end + local refreshMinutes = noctalia.getConfig("index_refresh_minutes") or 60 + local refreshMs = refreshMinutes * 60 * 1000 + local now = tonumber(noctalia.formatTime("%s")) or 0 + local elapsedMs = (now - metadata.builtAt) * 1000 + local remainingMs = refreshMs - elapsedMs + if remainingMs <= 0 then + noctalia.setUpdateInterval(1000) + return + end + noctalia.setUpdateInterval(remainingMs) +end + +local function ensureIndex() + if not indexing and not indexIsCurrent() then + if not indexHasCurrentConfig() then + noctalia.state.set("index_requires_rebuild", true) + end + startIndex() + end +end + +function update() + local status, token, detail = readStatus() + if indexing and status == "ready" then + indexing = false + if token == activeToken and activeSignature ~= nil then + saveMetadata(activeSignature) + end + local currentSignature = configSignature(configuredPrunes(), configuredPruneNames()) + local matchesCurrentConfig = token == activeToken and activeSignature == currentSignature + if pending or not matchesCurrentConfig then + noctalia.state.set("index_requires_rebuild", true) + else + noctalia.state.set("index_requires_rebuild", false) + publish("ready", "") + end + elseif indexing and status == "error" then + indexing = false + publish("error", detail) + elseif status == "running" then + publish("building", "") + end + + if not indexing and pending then + pending = false + startIndex() + return + end + + ensureIndex() + if indexing then + noctalia.setUpdateInterval(ACTIVE_POLL_MS) + else + scheduleIdlePoll() + end +end + +function onConfigChanged() + if not indexIsCurrent() then + noctalia.state.set("index_requires_rebuild", true) + startIndex() + end +end + +function onIpc(event, _payload) + if event == "reindex" then + startIndex() + end +end + +noctalia.state.watch("reindex_request", function(_value) + startIndex() +end) + +publish("checking", "") +update() diff --git a/locate-files/launcher.luau b/locate-files/launcher.luau new file mode 100644 index 0000000..2a44ca7 --- /dev/null +++ b/locate-files/launcher.luau @@ -0,0 +1,356 @@ +--!nonstrict + +local DEFAULT_MAX_RESULTS = 50 +local DEFAULT_FUZZY_CANDIDATE_LIMIT = 150 +local MAX_CANDIDATES = 500 +local CANDIDATE_MULTIPLIER = 4 +local MIN_FUZZY_QUERY_LENGTH = 3 + +local TYPE_ALIASES = { + any = "any", + file = "file", + f = "file", + directory = "directory", + dir = "directory", + d = "directory", +} + +local dataDir = noctalia.pluginDataDir() +local database = dataDir ~= nil and dataDir .. "/home.db" or nil +local pathsCache = dataDir ~= nil and dataDir .. "/paths.cache" or nil +local queryGeneration = 0 + +local function shellQuote(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function buildExactCommand(request, limit) + return "locate -d " .. shellQuote(database) + .. " -0 -e -i -l " .. tostring(limit) .. " -- " .. shellQuote(request.text) +end + +local function buildFuzzyCommand(request, limit) + return "fzf --read0 --print0 --scheme=path --filter=" .. shellQuote(request.text) + .. " < " .. shellQuote(pathsCache) .. " | head -z -n " .. tostring(limit) +end + +local function showStatus(query, titleKey, glyph, subtitle) + launcher.setResults(query, { + { + id = "", + title = noctalia.tr(titleKey), + subtitle = subtitle, + glyph = glyph, + }, + }) +end + +local function parseFlags(rawFlags) + local flags = {} + for flag in rawFlags:gmatch("%S+") do + table.insert(flags, flag) + end + + local options = { hidden = false, kind = "any", forceFuzzy = false } + local i = 1 + while i <= #flags do + local flag = flags[i] + if flag == "--hidden" or flag == "-h" then + options.hidden = true + elseif flag == "--fuzzy" then + options.forceFuzzy = true + elseif flag == "--type" or flag == "-t" then + i += 1 + if flags[i] == nil then + return nil, noctalia.tr("launcher.flags.type_missing") + end + options.kind = flags[i] + else + local kind = flag:match("^%-%-type=(.+)$") + if kind == nil then + return nil, noctalia.tr("launcher.flags.unknown", { flag = flag }) + end + options.kind = kind + end + i += 1 + end + + local normalizedKind = TYPE_ALIASES[options.kind] + if normalizedKind == nil then + return nil, noctalia.tr("launcher.flags.type_invalid", { value = options.kind }) + end + options.kind = normalizedKind + return options, nil +end + +local function parseRequest(query, generation) + local input = noctalia.string.trim(query) + local text, rawFlags = input:match("^(.-)%s+%-%-%s*(.*)$") + if text == nil then + text = input + rawFlags = "" + end + + local options, err = parseFlags(rawFlags) + if err ~= nil then + return nil, err + end + + local fuzzyMode = noctalia.getConfig("fuzzy_mode") or "fallback" + if fuzzyMode ~= "fallback" and fuzzyMode ~= "always" and fuzzyMode ~= "off" then + fuzzyMode = "fallback" + end + + return { + query = query, + text = noctalia.string.trim(text), + hidden = options.hidden, + kind = options.kind, + forceFuzzy = options.forceFuzzy, + fuzzyMode = fuzzyMode, + maxResults = noctalia.getConfig("max_results") or DEFAULT_MAX_RESULTS, + generation = generation, + }, nil +end + +local function isCurrent(request) + return request.generation == queryGeneration +end + +local function fuzzyEligible(request) + return #request.text >= MIN_FUZZY_QUERY_LENGTH +end + +local function fuzzyCandidateLimit() + local configured = tonumber(noctalia.getConfig("fuzzy_candidate_limit")) or DEFAULT_FUZZY_CANDIDATE_LIMIT + return math.max(100, math.min(math.floor(configured), 200)) +end + +local function exactCandidateLimit(request) + local needsFiltering = not request.hidden or request.kind ~= "any" + local multiplier = needsFiltering and CANDIDATE_MULTIPLIER or 1 + return math.min(request.maxResults * multiplier, MAX_CANDIDATES) +end + +local function kindMatches(kind, info) + if kind == "any" then + return true + end + return info ~= nil and (kind == "directory") == info.isDir +end + +local function needsFileInfo(request) + if request.kind ~= "any" then + return true + end + return (tonumber(noctalia.getConfig("file_weight")) or 0) ~= 0 + or (tonumber(noctalia.getConfig("directory_weight")) or 0) ~= 0 +end + +local function pathTitle(path) + local cleanPath = path == "/" and path or path:gsub("/+$", "") + return cleanPath:match("([^/]+)$") or cleanPath +end + +local function scoreCandidate(path, request, source, rank, candidateLimit, info) + local filenameScore = noctalia.fuzzyScore(request.text, pathTitle(path)) + local pathScore = noctalia.fuzzyScore(request.text, path) + local lexicalScore + if filenameScore ~= nil then + lexicalScore = filenameScore * 5 + (pathScore or 0) * 0.25 + elseif pathScore ~= nil then + lexicalScore = pathScore + else + return nil + end + + local rankBonus = 0 + if source == "fuzzy" then + rankBonus = (candidateLimit - rank + 1) / candidateLimit * 2 + end + + local typeWeight = 0 + if info ~= nil then + typeWeight = tonumber(noctalia.getConfig(info.isDir and "directory_weight" or "file_weight")) or 0 + end + + return lexicalScore + rankBonus + typeWeight + (tonumber(noctalia.getConfig("global_score_bias")) or -100) +end + +local function collectResults(stdout, request, source, candidateLimit) + local rows = {} + local needsInfo = needsFileInfo(request) + local rank = 0 + for path in stdout:gmatch("([^%z]+)") do + rank += 1 + local hidden = path:find("/%.") ~= nil + if request.hidden or not hidden then + local info = needsInfo and noctalia.fileInfo(path) or nil + if kindMatches(request.kind, info) then + local score = scoreCandidate(path, request, source, rank, candidateLimit, info) + if score ~= nil then + table.insert(rows, { + id = path, + title = pathTitle(path), + subtitle = path, + glyph = info ~= nil and info.isDir and "folder" or "file", + score = score, + rank = rank, + }) + end + end + end + end + table.sort(rows, function(a, b) + if a.score == b.score then + return a.rank < b.rank + end + return a.score > b.score + end) + while #rows > request.maxResults do + table.remove(rows) + end + for _, row in ipairs(rows) do + row.rank = nil + end + return rows +end + +local function searchFailed(result) + return result.timedOut or (result.exitCode ~= 0 and noctalia.string.trim(result.stderr) ~= "") +end + +local function publishRows(rows, request) + if not isCurrent(request) then + return + end + if #rows > 0 then + launcher.setResults(request.query, rows) + else + showStatus(request.query, "launcher.no_results", "file-off", request.text) + end +end + +local function startFuzzySearch(request) + if not fuzzyEligible(request) then + if isCurrent(request) then + showStatus(request.query, "launcher.fuzzy_too_short", "search", noctalia.tr("launcher.fuzzy_too_short_subtitle")) + end + return + end + if not noctalia.commandExists("fzf") then + if isCurrent(request) then + showStatus(request.query, "launcher.error", "alert-triangle", "fzf is not installed") + end + return + end + + local candidateLimit = fuzzyCandidateLimit() + local started = noctalia.runAsync(buildFuzzyCommand(request, candidateLimit), function(result) + if not isCurrent(request) then + return + end + if searchFailed(result) then + showStatus(request.query, "launcher.error", "alert-triangle", noctalia.string.trim(result.stderr)) + return + end + publishRows(collectResults(result.stdout, request, "fuzzy", candidateLimit), request) + end) + if not started and isCurrent(request) then + showStatus(request.query, "launcher.error", "alert-triangle", nil) + end +end + +local function startExactSearch(request) + local candidateLimit = exactCandidateLimit(request) + local started = noctalia.runAsync(buildExactCommand(request, candidateLimit), function(result) + if not isCurrent(request) then + return + end + if searchFailed(result) then + showStatus(request.query, "launcher.error", "alert-triangle", noctalia.string.trim(result.stderr)) + return + end + + local rows = collectResults(result.stdout, request, "exact", candidateLimit) + if #rows == 0 and request.fuzzyMode == "fallback" and fuzzyEligible(request) then + startFuzzySearch(request) + return + end + publishRows(rows, request) + end) + if not started and isCurrent(request) then + showStatus(request.query, "launcher.error", "alert-triangle", nil) + end +end + +local function startSearch(request) + if request.forceFuzzy or request.fuzzyMode == "always" then + startFuzzySearch(request) + else + startExactSearch(request) + end +end + +function onQuery(query) + queryGeneration += 1 + local generation = queryGeneration + if noctalia.string.trim(query) == "--reindex" then + launcher.setResults(query, { + { + id = "action:reindex", + title = noctalia.tr("launcher.reindex"), + subtitle = noctalia.tr("launcher.reindex_subtitle"), + glyph = "database-cog", + }, + }) + return + end + + local request, parseError = parseRequest(query, generation) + if parseError ~= nil then + showStatus(query, "launcher.flags.error", "alert-triangle", parseError) + return + end + + if request.text == "" then + showStatus(query, "launcher.hint", "search", noctalia.tr("launcher.hint_subtitle")) + return + end + + local requiresFuzzy = request.forceFuzzy or request.fuzzyMode == "always" + if requiresFuzzy and not fuzzyEligible(request) then + showStatus(query, "launcher.fuzzy_too_short", "search", noctalia.tr("launcher.fuzzy_too_short_subtitle")) + return + end + + if not noctalia.commandExists("locate") then + showStatus(query, "launcher.missing", "alert-triangle", noctalia.tr("launcher.missing_subtitle")) + return + end + + local indexStatus = noctalia.state.get("index_status") + local requiresRebuild = noctalia.state.get("index_requires_rebuild") == true + if database == nil or pathsCache == nil or not noctalia.fileExists(database) or not noctalia.fileExists(pathsCache) or requiresRebuild then + local failed = indexStatus == "error" + local titleKey = failed and "launcher.index_error" or "launcher.index_building" + local subtitle = failed and noctalia.state.get("index_detail") or noctalia.tr("launcher.index_building_subtitle") + showStatus(query, titleKey, failed and "alert-triangle" or "database-cog", subtitle) + return + end + + showStatus(query, "launcher.searching", "loader", nil) + startSearch(request) +end + +function onActivate(path) + if path == "action:reindex" then + local request = noctalia.state.get("reindex_request") or 0 + noctalia.state.set("reindex_request", request + 1) + noctalia.notify(noctalia.tr("title"), noctalia.tr("launcher.reindex_started")) + return + end + if path ~= "" and not noctalia.runAsync("xdg-open " .. shellQuote(path)) then + noctalia.notifyError(noctalia.tr("title"), noctalia.tr("launcher.open_error")) + end +end diff --git a/locate-files/plugin.toml b/locate-files/plugin.toml new file mode 100644 index 0000000..1f5adfe --- /dev/null +++ b/locate-files/plugin.toml @@ -0,0 +1,110 @@ +id = "yihao/locate-files" +name = "Locate Files" +version = "1.5.0" +plugin_api = 3 +author = "yihao" +license = "MIT" +dependencies = ["locate", "updatedb", "fzf", "flock", "xdg-open"] +tags = ["launcher", "productivity", "utility"] +icon = "file-search" +description = "Find and open files from Noctalia's launcher using locate." + +[[setting]] +key = "max_results" +type = "int" +label_key = "settings.max_results.label" +description_key = "settings.max_results.description" +default = 50 +min = 1 +max = 200 + +[[setting]] +key = "blacklist_paths" +type = "string_list" +label_key = "settings.blacklist_paths.label" +description_key = "settings.blacklist_paths.description" +default = [] + +[[setting]] +key = "blacklist_names" +type = "string_list" +label_key = "settings.blacklist_names.label" +description_key = "settings.blacklist_names.description" +default = [] + +[[setting]] +key = "index_refresh_minutes" +type = "int" +label_key = "settings.index_refresh_minutes.label" +description_key = "settings.index_refresh_minutes.description" +default = 60 +min = 1 +max = 10080 + +[[setting]] +key = "fuzzy_mode" +type = "select" +label_key = "settings.fuzzy_mode.label" +description_key = "settings.fuzzy_mode.description" +default = "fallback" +options = [ + { value = "fallback", label_key = "settings.fuzzy_mode.fallback" }, + { value = "always", label_key = "settings.fuzzy_mode.always" }, + { value = "off", label_key = "settings.fuzzy_mode.off" }, +] + +[[setting]] +key = "fuzzy_candidate_limit" +type = "int" +label_key = "settings.fuzzy_candidate_limit.label" +description_key = "settings.fuzzy_candidate_limit.description" +default = 150 +min = 100 +max = 200 +step = 10 +advanced = true + +[[setting]] +key = "global_score_bias" +type = "double" +label_key = "settings.global_score_bias.label" +description_key = "settings.global_score_bias.description" +default = -100 +min = -1000 +max = 1000 +step = 10 +advanced = true + +[[setting]] +key = "file_weight" +type = "double" +label_key = "settings.file_weight.label" +description_key = "settings.file_weight.description" +default = 0 +min = -1000 +max = 1000 +step = 10 +advanced = true + +[[setting]] +key = "directory_weight" +type = "double" +label_key = "settings.directory_weight.label" +description_key = "settings.directory_weight.description" +default = 0 +min = -1000 +max = 1000 +step = 10 +advanced = true + +[[launcher_provider]] +id = "files" +entry = "launcher.luau" +prefix = "f" +glyph = "file-search" +include_in_global_search = true +debounce_ms = 250 + +[[service]] +id = "indexer" +entry = "indexer.luau" diff --git a/locate-files/thumbnail.webp b/locate-files/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..2542fec4e3ca8143b7ef2734f0cdd17ee165f518 GIT binary patch literal 14842 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 literal 0 HcmV?d00001 diff --git a/locate-files/translations/en.json b/locate-files/translations/en.json new file mode 100644 index 0000000..42ecda4 --- /dev/null +++ b/locate-files/translations/en.json @@ -0,0 +1,44 @@ +{ + "title": "Locate Files", + "settings.max_results.label": "Maximum results", + "settings.max_results.description": "Maximum number of paths returned for each search.", + "settings.blacklist_paths.label": "Pruned paths", + "settings.blacklist_paths.description": "Directories excluded when building the private home index. Relative paths are resolved from your home directory.", + "settings.blacklist_names.label": "Pruned directory names", + "settings.blacklist_names.description": "Literal directory names excluded everywhere in the private index, such as node_modules or .git. Names cannot contain slashes or spaces.", + "settings.index_refresh_minutes.label": "Index refresh interval", + "settings.index_refresh_minutes.description": "Minutes between automatic updates of the private home index.", + "settings.fuzzy_mode.label": "Fuzzy search", + "settings.fuzzy_mode.description": "Use fuzzy matching after no exact result, always, or only when forced with --fuzzy.", + "settings.fuzzy_mode.fallback": "After no exact result", + "settings.fuzzy_mode.always": "Always", + "settings.fuzzy_mode.off": "Only when forced", + "settings.fuzzy_candidate_limit.label": "Fuzzy candidate limit", + "settings.fuzzy_candidate_limit.description": "Maximum fzf matches ranked by the launcher before results are shown.", + "settings.global_score_bias.label": "Global search score bias", + "settings.global_score_bias.description": "Score added to file results when this provider is enabled in global search.", + "settings.file_weight.label": "File score weight", + "settings.file_weight.description": "Score added to regular file results.", + "settings.directory_weight.label": "Directory score weight", + "settings.directory_weight.description": "Score added to directory results.", + "launcher.hint": "Type a file name to search", + "launcher.hint_subtitle": "Results come from the locate database", + "launcher.missing": "locate is not installed", + "launcher.missing_subtitle": "Install plocate or another locate implementation", + "launcher.searching": "Searching files...", + "launcher.no_results": "No files found", + "launcher.fuzzy_too_short": "Fuzzy search needs at least 3 characters", + "launcher.fuzzy_too_short_subtitle": "Keep typing to search the full private path index", + "launcher.error": "File search failed", + "launcher.flags.error": "Invalid search options", + "launcher.flags.unknown": "Unknown option: {flag}", + "launcher.flags.type_missing": "--type requires file or directory", + "launcher.flags.type_invalid": "Invalid type: {value}. Use file or directory.", + "launcher.index_building": "Building the home file index", + "launcher.index_building_subtitle": "Search will be available when indexing finishes", + "launcher.index_error": "Home file indexing failed", + "launcher.reindex": "Rebuild the home file index", + "launcher.reindex_subtitle": "Refresh the database now", + "launcher.reindex_started": "Home file indexing started", + "launcher.open_error": "Could not start xdg-open" +} diff --git a/locate-files/update-index.sh b/locate-files/update-index.sh new file mode 100644 index 0000000..865bd9c --- /dev/null +++ b/locate-files/update-index.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +set -u +umask 077 + +if [ "$#" -lt 5 ]; then + exit 2 +fi + +database=$1 +paths_cache=$2 +status_file=$3 +token=$4 +shift 4 + +staged_database="$database.tmp.$token" +staged_cache="$paths_cache.tmp.$token" + +cleanup() { + rm -f "$staged_database" "$staged_cache" +} + +trap cleanup EXIT HUP INT TERM + +exec 9>"$database.lock" +if ! flock -n 9; then + exit 0 +fi + +write_status() { + status=$1 + printf '%s|%s\n' "$status" "$token" >"$status_file.tmp" + mv -f "$status_file.tmp" "$status_file" +} + +write_status running + +if "$@" >"$database.log" 2>&1 \ + && locate -d "$staged_database" -0 / >"$staged_cache" 2>>"$database.log" \ + && [ -s "$staged_database" ] \ + && [ -s "$staged_cache" ] \ + && chmod 600 "$staged_database" "$staged_cache" \ + && mv -f "$staged_cache" "$paths_cache" \ + && mv -f "$staged_database" "$database"; then + trap - EXIT HUP INT TERM + write_status ready +else + code=$? + printf 'error|%s|%s\n' "$token" "$code" >"$status_file.tmp" + mv -f "$status_file.tmp" "$status_file" +fi From 77a592254fa3c90ee119247773236dd7a19f0956 Mon Sep 17 00:00:00 2001 From: yihao Date: Thu, 16 Jul 2026 23:09:50 +0800 Subject: [PATCH 2/3] fix: use integer score settings --- locate-files/plugin.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locate-files/plugin.toml b/locate-files/plugin.toml index 1f5adfe..0c5273d 100644 --- a/locate-files/plugin.toml +++ b/locate-files/plugin.toml @@ -1,6 +1,6 @@ id = "yihao/locate-files" name = "Locate Files" -version = "1.5.0" +version = "1.5.1" plugin_api = 3 author = "yihao" license = "MIT" @@ -66,7 +66,7 @@ advanced = true [[setting]] key = "global_score_bias" -type = "double" +type = "int" label_key = "settings.global_score_bias.label" description_key = "settings.global_score_bias.description" default = -100 @@ -77,7 +77,7 @@ advanced = true [[setting]] key = "file_weight" -type = "double" +type = "int" label_key = "settings.file_weight.label" description_key = "settings.file_weight.description" default = 0 @@ -88,7 +88,7 @@ advanced = true [[setting]] key = "directory_weight" -type = "double" +type = "int" label_key = "settings.directory_weight.label" description_key = "settings.directory_weight.description" default = 0 From 92acfe391ec18ccf151be3d0e07a7d2892ce5a67 Mon Sep 17 00:00:00 2001 From: yihao Date: Thu, 16 Jul 2026 23:11:04 +0800 Subject: [PATCH 3/3] chore: set initial release version --- locate-files/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locate-files/plugin.toml b/locate-files/plugin.toml index 0c5273d..a4edfe9 100644 --- a/locate-files/plugin.toml +++ b/locate-files/plugin.toml @@ -1,6 +1,6 @@ id = "yihao/locate-files" name = "Locate Files" -version = "1.5.1" +version = "0.1.0" plugin_api = 3 author = "yihao" license = "MIT"