From 6d6081d53015e7140c7fb203d7583530a9572ba5 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sat, 25 Oct 2025 10:52:44 -0300 Subject: [PATCH] :sparkles: feat(runner/plugins): implement fuzzy search in wallpapers and clipboard plugins now the clipboard and the wallpapers runner plugins support fuzzy searching! it's much better than the previous way, as it can match results with characters in-between words and sorted results based on matching score. thanks to fuse.js! --- package.json | 3 +- src/runner/Runner.tsx | 5 ++- src/runner/plugins/clipboard.ts | 69 +++++++++++++++++++++++--------- src/runner/plugins/wallpapers.ts | 39 +++++++++++++----- 4 files changed, 83 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index a5b3eb6..139e77e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "ags": "link:../../../../usr/share/ags/js", - "gnim-utils": "github:retrozinndev/gnim-utils" + "gnim-utils": "github:retrozinndev/gnim-utils", + "fuse.js": "^7.1.0" } } diff --git a/src/runner/Runner.tsx b/src/runner/Runner.tsx index 3c8c9c9..35c2602 100644 --- a/src/runner/Runner.tsx +++ b/src/runner/Runner.tsx @@ -31,7 +31,7 @@ export interface Plugin { /** runs when runner opens */ readonly init?: () => void; /** handle the user input to return results (does not include plugin's prefix) */ - readonly handle: (inputText: string) => (Result|Array|null|undefined); + readonly handle: (inputText: string, limit?: number) => (Result|Array|null|undefined); /** runs when runner closes */ readonly onClose?: () => void; /** prioritize this plugin's results over other results. @@ -171,7 +171,8 @@ function getPluginResults(input: string, limit?: number): Array { } const results = calledPlugins.map(plugin => plugin.handle( - plugin.prefix ? input.replace(plugin.prefix, "") : input) + plugin.prefix ? input.replace(plugin.prefix, "") : input), + limit ).filter(value => value !== undefined && value !== null).flat(1); return limit != null && limit > 0 ? diff --git a/src/runner/plugins/clipboard.ts b/src/runner/plugins/clipboard.ts index c2fa24e..b6990a1 100644 --- a/src/runner/plugins/clipboard.ts +++ b/src/runner/plugins/clipboard.ts @@ -1,33 +1,62 @@ import { Gtk } from "ags/gtk4"; -import { Clipboard } from "../../modules/clipboard"; +import { Clipboard, ClipboardItem } from "../../modules/clipboard"; import { Runner } from "../Runner"; import { jsx } from "ags/gtk4/jsx-runtime"; +import Fuse from "fuse.js"; -export const PluginClipboard = { - prefix: '>', - prioritize: true, - handle: (search) => { + +class _PluginClipboard implements Runner.Plugin { + #fuse!: Fuse; + prefix = '>'; + prioritize = true; + + init() { + const items: ReadonlyArray = [...Clipboard.getDefault().history]; + this.#fuse = new Fuse( + items, + { + keys: [ "id", "preview" ] satisfies Array, + ignoreDiacritics: false, + isCaseSensitive: false, + shouldSort: true, + useExtendedSearch: false + } + ); + } + + private clipboardResult(item: ClipboardItem): Runner.Result { + return { + icon: jsx(Gtk.Label, { + label: `${item.id}`, + css: "font-size: 16px; margin-right: 8px; font-weight: 600;" + }), + title: item.preview, + actionClick: () => Clipboard.getDefault().selectItem(item).catch((err: Error) => { + console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${ + err.message ? `${err.message}\n` : ""}Stack: ${err.stack}` + ); + }) + }; + } + + handle(search: string, limit?: number) { if(Clipboard.getDefault().history.length < 1) return { icon: "edit-paste-symbolic", title: "Clipboard is empty", description: "Copy something and it will be shown right here!" }; + + if(search.trim().length === 0) + return Clipboard.getDefault().history.map(item => + this.clipboardResult(item) + ); - return Clipboard.getDefault().history.filter(item => - // not the best way to search, but it works - Runner.regExMatch(search, item.id) || Runner.regExMatch(search, item.preview)).map((item) => ({ - icon: jsx(Gtk.Label, { - label: `${item.id}`, - css: "font-size: 16px; margin-right: 8px; font-weight: 600;" - }), - title: item.preview, - actionClick: () => Clipboard.getDefault().selectItem(item).catch((err: Error) => { - console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${ - err.message ? `${err.message}\n` : ""}Stack: ${err.stack}` - ); - }) - })); + return this.#fuse.search(search, { + limit: limit ?? Infinity + }).map(result => this.clipboardResult(result.item as ClipboardItem)) } -} as Runner.Plugin; +} + +export const PluginClipboard = new _PluginClipboard(); diff --git a/src/runner/plugins/wallpapers.ts b/src/runner/plugins/wallpapers.ts index 7e3696c..bd7fa6e 100644 --- a/src/runner/plugins/wallpapers.ts +++ b/src/runner/plugins/wallpapers.ts @@ -1,3 +1,4 @@ +import Fuse from "fuse.js"; import { Wallpaper } from "../../modules/wallpaper"; import { Runner } from "../Runner"; @@ -7,7 +8,8 @@ import Gio from "gi://Gio?version=2.0"; class _PluginWallpapers implements Runner.Plugin { prefix = "#"; prioritize = true; - #files: (Array|undefined); + #fuse!: Fuse; + #files!: Array; init() { this.#files = []; @@ -21,17 +23,34 @@ class _PluginWallpapers implements Runner.Plugin { this.#files.push(`${dir.get_path()}/${file.get_name()}`); } } + + this.#fuse = new Fuse( + this.#files as ReadonlyArray, + { + useExtendedSearch: false, + shouldSort: true, + isCaseSensitive: false + } + ); } - handle(search: string) { - if(this.#files!.length > 0) - return this.#files!.filter(file => - // also not the best way to search, but it works - Runner.regExMatch(search, file.split('/')[file.split('/').length-1]) - ).map(path => ({ - title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""), - actionClick: () => Wallpaper.getDefault().setWallpaper(path) - })); + private wallpaperResult(path: string): Runner.Result { + return { + title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""), + actionClick: () => Wallpaper.getDefault().setWallpaper(path) + }; + } + + handle(search: string, limit?: number) { + if(search.trim().length === 0) + return this.#files.map(path => + this.wallpaperResult(path) + ); + + if(this.#files.length > 0) + return this.#fuse.search(search, { + limit: limit ?? Infinity + }).map(result => this.wallpaperResult(result.item)); return { title: "No wallpapers found!",