diff --git a/Cargo.lock b/Cargo.lock
index a364d18..de9c77e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -978,6 +978,26 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kidex"
+version = "0.1.0"
+dependencies = [
+ "abi_stable",
+ "anyrun-plugin",
+ "fuzzy-matcher",
+ "kidex-common",
+ "open",
+]
+
+[[package]]
+name = "kidex-common"
+version = "0.1.0"
+source = "git+https://github.com/Kirottu/kidex#afcbf54b37dc47b914e0c8f873cb1f11c69674a7"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -1214,6 +1234,16 @@ version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
 
+[[package]]
+name = "open"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
+dependencies = [
+ "pathdiff",
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "openssl"
 version = "0.10.45"
@@ -1333,6 +1363,12 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
 
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
 [[package]]
 name = "percent-encoding"
 version = "2.2.0"
@@ -1660,9 +1696,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.91"
+version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
+checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
 dependencies = [
  "itoa",
  "ryu",
diff --git a/Cargo.toml b/Cargo.toml
index dc61ef8..8b2edad 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,5 +7,6 @@ members = [
   "plugins/symbols",
   "plugins/rink",
   "plugins/shell",
+  "plugins/kidex",
   "plugins/translate",
 ]
\ No newline at end of file
diff --git a/README.md b/README.md
index 408c387..906605d 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,8 @@ Anyrun requires plugins to function, as they provide the results for input. The
   - Calculator & unit conversion
 - [Shell](plugins/shell)
   - Run shell commands
+- [Kidex](plugins/kidex)
+  - File search provided by [Kidex](https://github.com/Kirottu/kidex)
 
 ## Configuration
 
diff --git a/anyrun-interface/src/lib.rs b/anyrun-interface/src/lib.rs
index e287d9f..99c4e11 100644
--- a/anyrun-interface/src/lib.rs
+++ b/anyrun-interface/src/lib.rs
@@ -50,7 +50,8 @@ pub enum HandleResult {
     /// Shut down the program
     Close,
     /// Refresh the items. Useful if the runner wants to alter results in place.
-    Refresh,
+    /// The inner value can set an exclusive mode for the plugin.
+    Refresh(bool),
     /// Copy the content, due to how copying works it must be done like this.
     Copy(RVec<u8>),
 }
diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs
index f5bea99..cb759a9 100644
--- a/anyrun/src/main.rs
+++ b/anyrun/src/main.rs
@@ -1,4 +1,4 @@
-use std::{cell::RefCell, env, fs, path::PathBuf, rc::Rc, time::Duration};
+use std::{cell::RefCell, env, fs, mem, path::PathBuf, rc::Rc, time::Duration};
 
 use abi_stable::std_types::{ROption, RVec};
 use anyrun_interface::{HandleResult, Match, PluginInfo, PluginRef, PollResult};
@@ -42,6 +42,9 @@ enum PostRunAction {
 /// Some data that needs to be shared between various parts
 struct RuntimeData {
     args: Args,
+    /// A plugin may request exclusivity which is set with this
+    exclusive: Option<PluginView>,
+    plugins: Vec<PluginView>,
     post_run_action: PostRunAction,
 }
 
@@ -100,6 +103,8 @@ fn main() {
                 override_plugins,
                 config_dir,
             },
+            exclusive: None,
+            plugins: Vec::new(),
             post_run_action: PostRunAction::None,
         });
         -1 // Magic GTK number to continue running
@@ -216,61 +221,60 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
         .build();
 
     // Load plugins from the paths specified in the config file
-    let plugins = Rc::new(
-        plugins
-            .iter()
-            .map(|plugin_path| {
-                let mut user_path = PathBuf::from(&format!("{}/plugins", config_dir));
-                let mut global_path = PathBuf::from("/etc/anyrun/plugins");
-                user_path.extend(plugin_path.iter());
-                global_path.extend(plugin_path.iter());
+    runtime_data.borrow_mut().as_mut().unwrap().plugins = plugins
+        .iter()
+        .map(|plugin_path| {
+            // Load the plugin's dynamic library.
+            let mut user_path = PathBuf::from(&format!("{}/plugins", config_dir));
+            let mut global_path = PathBuf::from("/etc/anyrun/plugins");
+            user_path.extend(plugin_path.iter());
+            global_path.extend(plugin_path.iter());
 
-                // Load the plugin's dynamic library.
-                let plugin = if plugin_path.is_absolute() {
-                    abi_stable::library::lib_header_from_path(plugin_path)
-                } else if user_path.exists() {
-                    abi_stable::library::lib_header_from_path(&user_path)
-                } else {
-                    abi_stable::library::lib_header_from_path(&global_path)
-                }
-                .and_then(|plugin| plugin.init_root_module::<PluginRef>())
-                .expect("Failed to load plugin");
+            // Load the plugin's dynamic library.
+            let plugin = if plugin_path.is_absolute() {
+                abi_stable::library::lib_header_from_path(plugin_path)
+            } else if user_path.exists() {
+                abi_stable::library::lib_header_from_path(&user_path)
+            } else {
+                abi_stable::library::lib_header_from_path(&global_path)
+            }
+            .and_then(|plugin| plugin.init_root_module::<PluginRef>())
+            .expect("Failed to load plugin");
 
-                // Run the plugin's init code to init static resources etc.
-                plugin.init()(config_dir.clone().into());
+            // Run the plugin's init code to init static resources etc.
+            plugin.init()(config_dir.clone().into());
 
-                let plugin_box = gtk::Box::builder()
+            let plugin_box = gtk::Box::builder()
+                .orientation(gtk::Orientation::Horizontal)
+                .spacing(10)
+                .name(style_names::PLUGIN)
+                .build();
+            plugin_box.add(&create_info_box(&plugin.info()()));
+            plugin_box.add(
+                &gtk::Separator::builder()
                     .orientation(gtk::Orientation::Horizontal)
-                    .spacing(10)
                     .name(style_names::PLUGIN)
-                    .build();
-                plugin_box.add(&create_info_box(&plugin.info()()));
-                plugin_box.add(
-                    &gtk::Separator::builder()
-                        .orientation(gtk::Orientation::Horizontal)
-                        .name(style_names::PLUGIN)
-                        .build(),
-                );
-                let list = gtk::ListBox::builder()
-                    .name(style_names::PLUGIN)
-                    .hexpand(true)
-                    .build();
+                    .build(),
+            );
+            let list = gtk::ListBox::builder()
+                .name(style_names::PLUGIN)
+                .hexpand(true)
+                .build();
 
-                plugin_box.add(&list);
+            plugin_box.add(&list);
 
-                let row = gtk::ListBoxRow::builder().name(style_names::PLUGIN).build();
-                row.add(&plugin_box);
+            let row = gtk::ListBoxRow::builder().name(style_names::PLUGIN).build();
+            row.add(&plugin_box);
 
-                main_list.add(&row);
+            main_list.add(&row);
 
-                PluginView { plugin, row, list }
-            })
-            .collect::<Vec<PluginView>>(),
-    );
+            PluginView { plugin, row, list }
+        })
+        .collect::<Vec<PluginView>>();
 
     // Connect selection events to avoid completely messing up selection logic
-    for plugin_view in plugins.iter() {
-        let plugins_clone = plugins.clone();
+    for plugin_view in runtime_data.borrow().as_ref().unwrap().plugins.iter() {
+        let plugins_clone = runtime_data.borrow().as_ref().unwrap().plugins.clone();
         plugin_view.list.connect_row_selected(move |list, row| {
             if row.is_some() {
                 let combined_matches = plugins_clone
@@ -303,9 +307,9 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
         .build();
 
     // Refresh the matches when text input changes
-    let plugins_clone = plugins.clone();
+    let runtime_data_clone = runtime_data.clone();
     entry.connect_changed(move |entry| {
-        refresh_matches(entry.text().to_string(), plugins_clone.clone())
+        refresh_matches(entry.text().to_string(), runtime_data_clone.clone())
     });
 
     // Handle other key presses for selection control and all other things that may be needed
@@ -321,7 +325,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
             // Handle selections
             constants::Down | constants::Tab | constants::Up => {
                 // Combine all of the matches into a `Vec` to allow for easier handling of the selection
-                let combined_matches = plugins
+                let combined_matches = runtime_data
+                    .borrow()
+                    .as_ref()
+                    .unwrap()
+                    .plugins
                     .iter()
                     .flat_map(|view| {
                         view.list.children().into_iter().map(|child| {
@@ -335,7 +343,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
                     .collect::<Vec<(gtk::ListBoxRow, gtk::ListBox)>>();
 
                 // Get the selected match
-                let (selected_match, selected_list) = match plugins
+                let (selected_match, selected_list) = match runtime_data
+                    .borrow()
+                    .as_ref()
+                    .unwrap()
+                    .plugins
                     .iter()
                     .find_map(|view| view.list.selected_row().map(|row| (row, view.list.clone())))
                 {
@@ -395,9 +407,14 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
             }
             // Handle when the selected match is "activated"
             constants::Return => {
-                let (selected_match, plugin) = match plugins
+                let mut _runtime_data = runtime_data.borrow_mut();
+
+                let (selected_match, plugin_view) = match _runtime_data
+                    .as_ref()
+                    .unwrap()
+                    .plugins
                     .iter()
-                    .find_map(|view| view.list.selected_row().map(|row| (row, view.plugin)))
+                    .find_map(|view| view.list.selected_row().map(|row| (row, view)))
                 {
                     Some(selected) => selected,
                     None => {
@@ -406,19 +423,25 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
                 };
 
                 // Perform actions based on the result of handling the selection
-                match plugin.handle_selection()(unsafe {
+                match plugin_view.plugin.handle_selection()(unsafe {
                     (*selected_match.data::<Match>("match").unwrap().as_ptr()).clone()
                 }) {
                     HandleResult::Close => {
                         window.close();
                         Inhibit(true)
                     }
-                    HandleResult::Refresh => {
-                        refresh_matches(entry_clone.text().to_string(), plugins.clone());
+                    HandleResult::Refresh(exclusive) => {
+                        if exclusive {
+                            _runtime_data.as_mut().unwrap().exclusive = Some(plugin_view.clone());
+                        } else {
+                            _runtime_data.as_mut().unwrap().exclusive = None;
+                        }
+                        mem::drop(_runtime_data); // Drop the mutable borrow
+                        refresh_matches(entry_clone.text().into(), runtime_data.clone());
                         Inhibit(false)
                     }
                     HandleResult::Copy(bytes) => {
-                        runtime_data.borrow_mut().as_mut().unwrap().post_run_action =
+                        _runtime_data.as_mut().unwrap().post_run_action =
                             PostRunAction::Copy(bytes.into());
                         window.close();
                         Inhibit(true)
@@ -441,7 +464,11 @@ fn activate(app: &gtk::Application, runtime_data: Rc<RefCell<Option<RuntimeData>
     main_list.show();
 }
 
-fn handle_matches(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, matches: RVec<Match>) {
+fn handle_matches(
+    plugin_view: PluginView,
+    runtime_data: Rc<RefCell<Option<RuntimeData>>>,
+    matches: RVec<Match>,
+) {
     // Clear out the old matches from the list
     for widget in plugin_view.list.children() {
         plugin_view.list.remove(&widget);
@@ -534,7 +561,11 @@ fn handle_matches(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, matches
     // Refresh the items in the view
     plugin_view.row.show_all();
 
-    let combined_matches = plugins
+    let combined_matches = runtime_data
+        .borrow()
+        .as_ref()
+        .unwrap()
+        .plugins
         .iter()
         .flat_map(|view| {
             view.list.children().into_iter().map(|child| {
@@ -595,27 +626,40 @@ fn create_info_box(info: &PluginInfo) -> gtk::Box {
 }
 
 /// Refresh the matches from the plugins
-fn refresh_matches(input: String, plugins: Rc<Vec<PluginView>>) {
-    for plugin_view in plugins.iter() {
+fn refresh_matches(input: String, runtime_data: Rc<RefCell<Option<RuntimeData>>>) {
+    for plugin_view in runtime_data.borrow().as_ref().unwrap().plugins.iter() {
         let id = plugin_view.plugin.get_matches()(input.clone().into());
         let plugin_view = plugin_view.clone();
-        let plugins = plugins.clone();
+        let runtime_data_clone = runtime_data.clone();
         // If the input is empty, skip getting matches and just clear everything out.
         if input.is_empty() {
-            handle_matches(plugin_view, plugins, RVec::new());
+            handle_matches(plugin_view, runtime_data_clone, RVec::new());
+        // If a plugin has requested exclusivity, respect it
+        } else if let Some(exclusive) = &runtime_data.borrow().as_ref().unwrap().exclusive {
+            if plugin_view.plugin.info() == exclusive.plugin.info() {
+                glib::timeout_add_local(Duration::from_micros(1000), move || {
+                    async_match(plugin_view.clone(), runtime_data_clone.clone(), id)
+                });
+            } else {
+                handle_matches(plugin_view.clone(), runtime_data_clone, RVec::new());
+            }
         } else {
             glib::timeout_add_local(Duration::from_micros(1000), move || {
-                async_match(plugin_view.clone(), plugins.clone(), id)
+                async_match(plugin_view.clone(), runtime_data_clone.clone(), id)
             });
         }
     }
 }
 
 /// Handle the asynchronously running match task
-fn async_match(plugin_view: PluginView, plugins: Rc<Vec<PluginView>>, id: u64) -> glib::Continue {
+fn async_match(
+    plugin_view: PluginView,
+    runtime_data: Rc<RefCell<Option<RuntimeData>>>,
+    id: u64,
+) -> glib::Continue {
     match plugin_view.plugin.poll_matches()(id) {
         PollResult::Ready(matches) => {
-            handle_matches(plugin_view, plugins, matches);
+            handle_matches(plugin_view, runtime_data, matches);
             glib::Continue(false)
         }
         PollResult::Pending => glib::Continue(true),
diff --git a/plugins/kidex/Cargo.toml b/plugins/kidex/Cargo.toml
new file mode 100644
index 0000000..66623f0
--- /dev/null
+++ b/plugins/kidex/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "kidex"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyrun-plugin = { path = "../../anyrun-plugin" }
+kidex-common = { git = "https://github.com/Kirottu/kidex", features = ["util"] }
+abi_stable = "0.11.1"
+fuzzy-matcher = "0.3.7"
+open = "3.2.0"
diff --git a/plugins/kidex/src/lib.rs b/plugins/kidex/src/lib.rs
new file mode 100644
index 0000000..7b2c6d4
--- /dev/null
+++ b/plugins/kidex/src/lib.rs
@@ -0,0 +1,146 @@
+use abi_stable::std_types::{ROption, RString, RVec};
+use anyrun_plugin::{anyrun_interface::HandleResult, *};
+use fuzzy_matcher::FuzzyMatcher;
+use kidex_common::IndexEntry;
+use std::{os::unix::prelude::OsStrExt, process::Command};
+
+pub struct State {
+    index: Vec<(usize, IndexEntry)>,
+    selection: Option<IndexEntry>,
+}
+
+enum IndexAction {
+    Open,
+    CopyPath,
+    Back,
+}
+
+impl From<u64> for IndexAction {
+    fn from(value: u64) -> Self {
+        match value {
+            0 => Self::Open,
+            1 => Self::CopyPath,
+            2 => Self::Back,
+            _ => unreachable!(),
+        }
+    }
+}
+
+pub fn handler(selection: Match, state: &mut State) -> HandleResult {
+    match &state.selection {
+        Some(index_entry) => match selection.id.unwrap().into() {
+            IndexAction::Open => {
+                if let Err(why) = Command::new("xdg-open").arg(&index_entry.path).spawn() {
+                    println!("Error running xdg-open: {}", why);
+                }
+                HandleResult::Close
+            }
+            IndexAction::CopyPath => {
+                HandleResult::Copy(index_entry.path.clone().into_os_string().as_bytes().into())
+            }
+            IndexAction::Back => {
+                state.selection = None;
+                HandleResult::Refresh(false)
+            }
+        },
+        None => {
+            let (_, index_entry) = state
+                .index
+                .iter()
+                .find(|(id, _)| selection.id == ROption::RSome(*id as u64))
+                .unwrap();
+
+            state.selection = Some(index_entry.clone());
+            HandleResult::Refresh(true)
+        }
+    }
+}
+
+pub fn init(_config_dir: RString) -> State {
+    State {
+        index: match kidex_common::util::get_index(None) {
+            Ok(index) => index.into_iter().enumerate().collect(),
+            Err(why) => {
+                println!("Failed to get kidex index: {}", why);
+                Vec::new()
+            }
+        },
+        selection: None,
+    }
+}
+
+pub fn get_matches(input: RString, state: &mut State) -> RVec<Match> {
+    match &state.selection {
+        Some(index_entry) => {
+            let path = index_entry.path.to_string_lossy();
+            vec![
+                Match {
+                    title: "Open File".into(),
+                    description: ROption::RSome(path.clone().into()),
+                    id: ROption::RSome(IndexAction::Open as u64),
+                    icon: ROption::RSome("document-open".into()),
+                },
+                Match {
+                    title: "Copy Path".into(),
+                    description: ROption::RSome(path.into()),
+                    id: ROption::RSome(IndexAction::CopyPath as u64),
+                    icon: ROption::RSome("edit-copy".into()),
+                },
+                Match {
+                    title: "Back".into(),
+                    description: ROption::RNone,
+                    id: ROption::RSome(IndexAction::Back as u64),
+                    icon: ROption::RSome("edit-undo".into()),
+                },
+            ]
+            .into()
+        }
+        None => {
+            let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().smart_case();
+            let mut index = state
+                .index
+                .clone()
+                .into_iter()
+                .filter_map(|(id, index_entry)| {
+                    matcher
+                        .fuzzy_match(&index_entry.path.as_os_str().to_string_lossy(), &input)
+                        .map(|val| (index_entry, id, val))
+                })
+                .collect::<Vec<_>>();
+
+            index.sort_by(|a, b| b.2.cmp(&a.2));
+
+            index.truncate(3);
+            index
+                .into_iter()
+                .map(|(entry_index, id, _)| Match {
+                    title: entry_index
+                        .path
+                        .file_name()
+                        .map(|name| name.to_string_lossy().into())
+                        .unwrap_or("N/A".into()),
+                    icon: ROption::RSome(if entry_index.directory {
+                        "folder".into()
+                    } else {
+                        "text-x-generic".into()
+                    }),
+                    description: entry_index
+                        .path
+                        .parent()
+                        .map(|path| path.display().to_string().into())
+                        .into(),
+                    id: ROption::RSome(id as u64),
+                })
+                .collect()
+        }
+    }
+}
+
+pub fn info() -> PluginInfo {
+    PluginInfo {
+        name: "Kidex".into(),
+        icon: "folder".into(),
+    }
+}
+
+plugin!(init, info, get_matches, handler, State);