Skip to content

Latest commit

 

History

History
250 lines (211 loc) · 8.59 KB

EXTENSIONS.md

File metadata and controls

250 lines (211 loc) · 8.59 KB

Extensions

Extensions allow developers to add new functionality to Dlauncher, for example an extension that lets users search for symbols and copy them to their clipboard.

Getting Started

Extensions in Dlauncher are made possible through FFI and shared object libraries (.so files) requiring us to write a lot of unsafe code.

First setup a new cargo project:

cargo new dlauncher_extension
cd dlauncher_extension

Then add the following to the Cargo.toml file:

[dependencies]
dlauncher = "0.1.0"
gtk = { version =  "0.15.5", features = ["v3_22"] }
lazy_static = "1.4.0"
log = "0.4.17"

Then add the following to the src/lib.rs file:

use dlauncher::{
  extension::{
    ExtensionContext,
    response::{ExtensionResponse, ExtensionResponseIcon},
  },
};
use dlauncher::util::init_logger;
use lazy_static::lazy_static;
use log::debug;

#[no_mangle]
pub unsafe extern "C" fn on_init(ctx: ExtensionContext) {
  init_logger();
  
  info!("Hello from extension!")
}

The #[no_mangle] attribute is required to prevent the compiler from mangling the function name so that it can be called via on_init in dlauncher. The on_init function is called when the extension is loaded, this is not required but is useful when the extension needs to read data before it is used. We are using the log crate to add support for logging, and the init_logger() function is used to initialize the logger for use. The ctx variable is an ExtensionContext struct containing things that let you interface with the main dlauncher process and window.

Enabling your extension

To test out and debug your extension the easiest way as of now is copy the built .so file to the extensions folder. Before doing this you need to add the file name to the extensions array in the dlauncher.toml file.

extensions = ["dlauncher_extension.so"]

Then we can build the extension, copy it over, then run dlauncher.

cargo build --release
cp target/release/libdlauncher_extension.so ~/.config/dlauncher/extensions

# kill an already running dlauncher
pkill dlauncher

# this is assuming that the dlauncher bin is inside your PATH or usually /usr/bin
dlauncher

This can be shortenned into a one liner:

cargo build --release && cp target/release/libdlauncher_extension.so ~/.config/dlauncher/extensions && pkill dlauncher && dlauncher

Listening to input events

To listen to when a user types in the input field we have to add the on_input function to the src/lib.rs.

use dlauncher::{
  extension::{
    ExtensionContext,
    response::{ExtensionResponse, ExtensionResponseIcon},
  },
};
use dlauncher::util::init_logger;
use lazy_static::lazy_static;
use log::debug;

#[no_mangle]
pub unsafe extern "C" fn on_init(ctx: ExtensionContext) {
  init_logger();
  
  info!("Hello from extension!")
}

#[no_mangle]
pub unsafe extern "C" fn on_input(ctx: ExtensionContext) {
  gtk::set_initialized();
  
  info!("input: {:#?}", ctx.input);
}

gtk::set_initialized() is used to make sure that the gtk library is sure that we initialized it. This is not needed unless you are interfacing with GTK.

Checking for a match

If we want to see if the users input matches something we can use the matches function. Heres a function that checks if the users input matches "zero width space". We also make sure that the input is not None before we check it. The third argument passed in matches is the least score required for a match, a safe value for this is usually 60-80, if you want more precision for your match you can use a higher value like 100-150, just remember that the input has to be very specific and may not yield the best results. Now if we type in the input field something like "ze" we should see in our logs "matched: true".

#[no_mangle]
pub unsafe extern "C" fn on_input(ctx: ExtensionContext) {
  gtk::set_initialized();
  
  if let Some(input) = ctx.input {
    let matched = matches(input, "zero width space", 60);
    info!("matched: {}", matched);
  }
}

Adding a result entry

Usually once the match is found we can add a result entry that will show up in the UI. This can be made easy by the ExtensionResponse struct. This will allow you to make a "builder" that will allow you to easily create a result entry with lines.

#[no_mangle]
pub unsafe extern "C" fn on_input(ctx: ExtensionContext) {
  gtk::set_initialized();
  
  if let Some(input) = ctx.input {
    let matched = matches(input, "zero width space", 60);
    if !matched {
      return;
    }
    
    let mut response = ExtensionResponse::builder(&ctx.name, None);
    response.line(
      "Zero Width Space",
      "Press enter to copy to your clipboard",
      ExtensionResponseIcon::themed("spacer-symbolic")
    );
  }
}

Here, the line function takes 3 arguments: name, description and icon. The ExtensionResponseIcon enum is used to specify the icon that will be used for the result entry. The themed function is used to specify a themed icon, for example using the Papirus icon theme. The svg function is used to specify a svg string, which is useful when you want to use a custom icons, or dynamic icons that can be made on the fly.

Now when we type in "ze" we should see the result entry showing Zero Width Space with the icon and everything. but when we press enter the character doesn't get copied to the clipboard.

Controlling what happens when a line is clicked/entered

The ExtensionResponse struct has a couple more functions that let you add actions that happen when the user clicks or presses enter on the line. ExtensionResponse::line_on_enter adds a 4th argument that will take a function.

#[no_mangle]
pub unsafe extern "C" fn on_input(ctx: ExtensionContext) {
  gtk::set_initialized();
  
  if let Some(input) = ctx.input {
    let matched = matches(input, "zero width space", 60);
    if !matched {
      return;
    }
    
    let mut response = ExtensionResponse::builder(&ctx.name, None);
    response.line_on_enter(
      "Zero Width Space",
      "Press enter to copy to your clipboard",
      ExtensionResponseIcon::themed("spacer-symbolic"),
      |ctx| {
        info!("hello! i was clicked");
      }
    );
  }
}

Now when we press enter on the line we should see the print in the logs. Inside the closure we can now use the utility function copy_to_clipboard to copy the text to the clipboard.

response.line_on_enter(
  "Zero Width Space",
  "Press enter to copy to your clipboard",
  ExtensionResponseIcon::themed("spacer-symbolic"),
  |ctx| {
    copy_to_clipboard("\u{200B}");
  }
);

States via static variables

Some extensions might require a prefix, like sym equal meaning that sym is the prefix and equal are the arguments (This example is refering to an extension that lets you look up symbols and copy them to your clipboard). An inefficient way of getting the user's prefix would be ctx.config.get("prefix").unwrap_or("sym") every time inside your on_input function. To get around this we can use a static variable, and set the value during the on_init function.

To make sure that the value is thread safe and can be mutated we use a Mutex wrapped in an Arc. We also use lazy_static as normally we can't call functions in static variables.

We add the following at the top of the file:

lazy_static! {
  #[derive(Debug)] static ref PREFIX: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
}

Then inside of our on_init function we set the value of the static variable:

#[no_mangle]
pub unsafe extern "C" fn on_init(ctx: ExtensionContext) {
  init_logger();

  let mut prefix = PREFIX.lock().unwrap();
  *prefix = ctx
    .config
    .get("prefix")
    .unwrap_or_else(|| "sym".to_string())
    + " ";
}

The following gets a lock of the prefix value, then sets the value of it to the prefix key in the extension config, and if it doesn't exist it sets it to sym.

In our on_input function we can now use the static variable to get the prefix:

#[no_mangle]
pub unsafe extern "C" fn on_input(ctx: ExtensionContext) {
  gtk::set_initialized();

  if let Some(input) = ctx.input {
    let prefix = &*PREFIX.lock().unwrap();

    if input.is_empty() || !input.to_lowercase().starts_with(prefix) {
      return;
    }
    
    // do stuff now.
  }
} 

That's it! Feel free to explore the API, as extensions let you have full control over everything that happens.