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();