diff --git a/resources.gresource.xml b/resources.gresource.xml index dec2c2bc..325a770d 100644 --- a/resources.gresource.xml +++ b/resources.gresource.xml @@ -21,6 +21,7 @@ icons/arrow-circular-top-right-symbolic.svg icons/circle-filled-symbolic.svg icons/hourglass-symbolic.svg + icons/paper-symbolic.svg icons/loop-arrow-symbolic.svg icons/minus-circle-filled-symbolic.svg icons/shield-danger-symbolic.svg diff --git a/resources/icons/paper-symbolic.svg b/resources/icons/paper-symbolic.svg new file mode 100644 index 00000000..32a6518b --- /dev/null +++ b/resources/icons/paper-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/runner/plugins/files.ts b/src/runner/plugins/files.ts new file mode 100644 index 00000000..c3d201ac --- /dev/null +++ b/src/runner/plugins/files.ts @@ -0,0 +1,116 @@ +import { execAsync } from "ags/process"; +import { escapeSpecialCharacters, isInstalled, translateDirWithEnvironment } from "../../modules/utils"; +import { Runner } from "../Runner"; +import { Notifications } from "../../modules/notifications"; +import GLib from "gi://GLib?version=2.0"; + + +type RipGrepJSON = { + type: "begin"|"match"|"end"|"summary"; + data: { + path: { text: string; }; + lines?: { + text: string; + }; + stats?: { + elapsed: { + secs: number; + nanos: number; + human: string; + }; + searches: number; + searches_with_match: number; + }; + binary_offset?: number|null; + + }; +}; + +type Item = { + isDir: boolean; + isLink: boolean; + isExecutable: boolean; + path: string; + name: string; +}; + +class _PluginFiles implements Runner.Plugin { + #rgAvailable: boolean = false; + #findAvailable: boolean = false; + prefix = "/"; + prioritize = true; + + init() { + // check if ripgrep is installed + this.#rgAvailable = isInstalled("rg"); + this.#findAvailable = isInstalled("find"); + } + + async handle(search: string, limit: number = 30) { + + if(!this.#rgAvailable) + return { + title: "`ripgrep` not found", + description: "Try installing `ripgrep` before using this feature", + actionClick: () => execAsync("xdg-open 'https://github.com/BurntSushi/ripgrep'") + } satisfies Runner.Result; + + if(search.length < 1) + return + + if(/^\//.test(search)) { + search = translateDirWithEnvironment(search); + + if(!this.#findAvailable) + return { + title: "`findutils` not found", + description: "Try installing GNU `findutils` before using this feature" + } satisfies Runner.Result; + + return await this.find(search); + } + + const str = escapeSpecialCharacters(search); + const res = execAsync(["bash", "-c", `'rg --json "${search}" | head -n ${limit}'` ]); + const jsons: Array = (await res)?.split('\n').map(ostr => JSON.parse(ostr)); + } + + private async find(path: string): Promise> { + const items: Array = (await execAsync(["find", path])).split('\n').map(item => ({ + isDir: GLib.file_test(item, GLib.FileTest.IS_DIR), + isLink: GLib.file_test(item, GLib.FileTest.IS_SYMLINK), + isExecutable: GLib.file_test(item, GLib.FileTest.IS_EXECUTABLE), + path: item, + name: item.split('/')[item.split('/').length - 1] + })); + + return items.map(item => { + if(item.isDir) + return { + title: item.name, + icon: "inode-directory-symbolic", + description: `Directory${item.isLink ? " (link)" : ""}`, + actionClick: () => Runner.setEntryText(`${this.prefix}${item.path}`), + closeOnClick: false + } satisfies Runner.Result; + + return { + title: item.name, + icon: "paper-symbolic", + description: `File${item.isExecutable ? " (executable)" : ""}${ + item.isLink ? " (link)" : ""}`, + closeOnClick: true, + actionClick: () => execAsync(`xdg-open ${item.path}`).catch((e: Error) => { + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Error when opening file", + body: `The following error occurred while opening "${item.name + }" with XDG: ${e.message}` + }); + }) + } satisfies Runner.Result; + }); + } +} + +export const PluginFiles = new _PluginFiles();