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.
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.
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
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.
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);
}
}
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.
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}");
}
);
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.