rofi/mkdocs/docs/guides/Plugins/2017-04-19-rofi-140-sneak-p...

696 lines
22 KiB
Markdown

> This guide is taken from the 1.4.0 release preview posts. The information might
> be outdated, but in general should still be correct and a good starting point
> for writing a plugin. Links have been updated.
> A recent plugin that can be used as example can be found
> [here](https://git.sr.ht/~qball/rofi-ntfy).
## Build system
**Rofi** uses [autotools](https://en.wikipedia.org/wiki/GNU_build_system) as
build system. While there are many opinions about the pre/cons off all the
different options out there (to many to go into in this blog post), we will
stick to [autotools](https://en.wikipedia.org/wiki/GNU_build_system) for now.
To make life easier I create a template project
[here](https://github.com/davatorium/rofi-plugin-template)
This includes the 2 files for the build system and the C template.
### Configure.ac
First we are going to update the `configure.ac` file:
```
AC_INIT([rofi-plugin-template], [0.0.1], [https://my-neat-plugin.org//],[],[https://support.my-neat-plugin.org/])
AC_CONFIG_HEADER([config.h])
AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects dist-xz])
AM_SILENT_RULES([yes])
AC_PROG_CC([clang gcc cc])
AC_PROG_CC_C99
AM_PROG_CC_C_O
AC_USE_SYSTEM_EXTENSIONS
AM_PROG_AR
AM_CFLAGS="-Wall -Wextra -Wparentheses -Winline -pedantic -Wunreachable-code"
PKG_PROG_PKG_CONFIG
PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.40 gio-unix-2.0 gmodule-2.0 ])
PKG_CHECK_MODULES([rofi], [rofi])
[rofi_PLUGIN_INSTALL_DIR]="`$PKG_CONFIG --variable=pluginsdir rofi`"
AC_SUBST([rofi_PLUGIN_INSTALL_DIR])
LT_INIT([disable-static])
AC_SUBST([AM_CFLAGS])
AC_CONFIG_FILES([Makefile ])
AC_OUTPUT
```
Basically the only thing here we need to change is the name of the plugin, the
website and the support site.
```diff
AC_INIT([rofi-file-browser], [0.0.1], [https://davedavenport.github.io/rofi/],[],[https://reddit.org/r/qtools/])
```
### Makefile.am
We need to make a similar change in the `Makefile.am` file, this is important so
each plugin has a unique name. (if they are all called myplugin, it would be
hard to install more then one plugin.)
```
ACLOCAL_AMFLAGS=-I m4
plugindir=@rofi_PLUGIN_INSTALL_DIR@
plugin_LTLIBRARIES = myplugin.la
myplugin_la_SOURCES=\
src/myplugin.c
myplugin_la_CFLAGS= @glib_CFLAGS@ @rofi_CFLAGS@
myplugin_la_LIBADD= @glib_LIBS@ @rofi_LIBS@
myplugin_la_LDFLAGS= -module -avoid-version
```
So we do a search and replace from `myplugin` to `file_browser`:
```
ACLOCAL_AMFLAGS=-I m4
plugindir=${libdir}/rofi/
plugin_LTLIBRARIES = file_browser.la
file_browser_la_SOURCES=\
src/file_browser.c
file_browser_la_CFLAGS= @glib_CFLAGS@ @rofi_CFLAGS@
file_browser_la_LIBADD= @glib_LIBS@ @rofi_LIBS@
file_browser_la_LDFLAGS= -module -avoid-version
```
As you noticed I also changed the name of the c template file. This is not
needed.
## Building the system
Now that we have this setup, it is easy to build:
* Generate the build system:
```bash
autoreconf -i
```
* Create a `build` directory.
```bash
mkdir build
cd build
```
* Run `configure`
```bash
../configure
```
* build
```bash
make
```
* install
```bash
make install
```
You can now test the plugin by calling:
```bash
rofi -show myplugin -modi myplugin
```
If we start changing the template, the name to use will change.
## Edit the C template
The first thing todo is personalize the template. Below I have modified it so it
is called file-browser:
```c
/**
* rofi-file-browser
*
* MIT/X11 License
* Copyright (c) 2017 Qball Cow <qball@gmpclient.org>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <gmodule.h>
#include <rofi/mode.h>
#include <rofi/helper.h>
#include <rofi/mode-private.h>
#include <stdint.h>
G_MODULE_EXPORT Mode mode;
/**
* The internal data structure holding the private data of the TEST Mode.
*/
typedef struct
{
char **array;
unsigned int array_length;
} FileBrowserModePrivateData;
static void get_file_browser ( Mode *sw )
{
/**
* Get the entries to display.
* this gets called on plugin initialization.
*/
}
static int file_browser_mode_init ( Mode *sw )
{
/**
* Called on startup when enabled (in modi list)
*/
if ( mode_get_private_data ( sw ) == NULL ) {
FileBrowserModePrivateData *pd = g_malloc0 ( sizeof ( *pd ) );
mode_set_private_data ( sw, (void *) pd );
// Load content.
get_file_browser ( sw );
}
return TRUE;
}
static unsigned int file_browser_mode_get_num_entries ( const Mode *sw )
{
const FileBrowserModePrivateData *pd = (const FileBrowserModePrivateData *) mode_get_private_data ( sw );
return pd->array_length;
}
static ModeMode file_browser_mode_result ( Mode *sw, int mretv, char **input, unsigned int selected_line )
{
ModeMode retv = MODE_EXIT;
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
if ( mretv & MENU_NEXT ) {
retv = NEXT_DIALOG;
} else if ( mretv & MENU_PREVIOUS ) {
retv = PREVIOUS_DIALOG;
} else if ( mretv & MENU_QUICK_SWITCH ) {
retv = ( mretv & MENU_LOWER_MASK );
} else if ( ( mretv & MENU_OK ) ) {
retv = RELOAD_DIALOG;
} else if ( ( mretv & MENU_ENTRY_DELETE ) == MENU_ENTRY_DELETE ) {
retv = RELOAD_DIALOG;
}
return retv;
}
static void file_browser_mode_destroy ( Mode *sw )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
if ( pd != NULL ) {
g_free ( pd );
mode_set_private_data ( sw, NULL );
}
}
static char *_get_display_value ( const Mode *sw, unsigned int selected_line, G_GNUC_UNUSED int *state, G_GNUC_UNUSED GList **attr_list, int get_entry )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
// Only return the string if requested, otherwise only set state.
return get_entry ? g_strdup("n/a"): NULL;
}
static int file_browser_token_match ( const Mode *sw, GRegex **tokens, unsigned int index )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
// Call default matching function.
return helper_token_match ( tokens, pd->array[index]);
}
Mode mode =
{
.abi_version = ABI_VERSION,
.name = "file_browser",
.cfg_name_key = "display-file_browser",
._init = file_browser_mode_init,
._get_num_entries = file_browser_mode_get_num_entries,
._result = file_browser_mode_result,
._destroy = file_browser_mode_destroy,
._token_match = file_browser_token_match,
._get_display_value = _get_display_value,
._get_message = NULL,
._get_completion = NULL,
._preprocess_input = NULL,
.private_data = NULL,
.free = NULL,
};
```
If we now rebuild the plugin, we need to run the following command:
```bash
rofi -show file_browser -modi file_browser
```
### The mode description
The mode is defined by the `Mode` structure, every mode in rofi has one of the
plugins.
```c
Mode mode =
{
.abi_version = ABI_VERSION,
.name = "file_browser",
.cfg_name_key = "display-file_browser",
._init = file_browser_mode_init,
._get_num_entries = file_browser_mode_get_num_entries,
._result = file_browser_mode_result,
._destroy = file_browser_mode_destroy,
._token_match = file_browser_token_match,
._get_display_value = _get_display_value,
._get_message = NULL,
._get_completion = NULL,
._preprocess_input = NULL,
.private_data = NULL,
.free = NULL,
};
```
The ABI_VERSION is defined in **rofi** header file, so that **rofi** can detect what
ABI the plugin was compiled against.
Not every function needs to be implemented, in the plugin we show the minimum
set.
Lets modify each of the above functions to implement something useful.
### FileBrowserModePrivateData
This is a structure that holds all the private data of this mode.
We are going to extend this so it can hold the state of information we want to
view.
We want to differentiate between 3 different rows:
* Go one level up
* Directory
* Regular file
So we add an enum:
```c
enum FBFileType {
UP,
DIRECTORY,
RFILE,
};
```
We need a structure that hold each entry.
* It should have a **name** we are going to show the user. This will hold an
`utf-8` string. (rofi will only display utf-8).
* It should hold the **path** to the entry. This will be in the file-systems
encoding.
* The type it holds.
```c
typedef struct {
char *name;
char *path;
enum FBFileType type;
} FBFile;
```
Then in the *private* data we hold all the relevant information.
* The current directory to show.
* Array of all the *FBFile* we want to show.
* The length of the array.
```c
typedef struct
{
GFile *current_dir;
FBFile *array;
unsigned int array_length;
} FileBrowserModePrivateData;
```
### Initialization
Now that we have the data structure to hold our information, we need to
initialize it and fill it.
```c
static int file_browser_mode_init ( Mode *sw )
{
if ( mode_get_private_data ( sw ) == NULL ) {
FileBrowserModePrivateData *pd = g_malloc0 ( sizeof ( *pd ) );
mode_set_private_data ( sw, (void *) pd );
pd->current_dir = g_file_new_for_path(g_get_home_dir () );
get_file_browser ( sw );
}
return TRUE;
}
```
The function first checked if we already initialized the private data. You can
include a mode multiple times, and we normally don't want it initialized
multiple times.
We then create a, zero initialized, `FileBrowserModePrivateData` structure and
set this on the mode. Set the current directory to the users home directory and
call `get_file_browser` that will load in the entries. We will discuss this one
later.
### Destroying
On shutdown we want to cleanup, so there is also a destroy function.
```c
static void file_browser_mode_destroy ( Mode *sw )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
if ( pd != NULL ) {
g_object_unref ( pd->current_dir );
free_list ( pd );
g_free ( pd );
mode_set_private_data ( sw, NULL );
}
}
```
This does the exact opposite.
For completeness:
```c
static void free_list ( FileBrowserModePrivateData *pd )
{
for ( unsigned int i = 0; i < pd->array_length; i++ ) {
FBFile *fb = & ( pd->array[i] );
g_free ( fb->name );
g_free ( fb->path );
}
g_free (pd->array);
pd->array = NULL;
pd->array_length = 0;
}
```
### Loading the entries
Lets dive deeper into the `get_file_browser` function.
```c
static void get_file_browser ( Mode *sw )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
```
We want to get access to the private data structure.
```c
char *cdir = g_file_get_path ( pd->current_dir );
DIR *dir = opendir ( cdir );
if ( dir ) {
struct dirent *rd = NULL;
while ((rd = readdir (dir)) != NULL )
{
```
We open the directory and we iterate over each entry. We then want to skip over
hidden files (starting with a .) and insert a special up node for going up one
directory. For this we do not need a path, and we show ".." to the user.
```c
if ( g_strcmp0 ( rd->d_name, ".." ) == 0 ){
pd->array = g_realloc ( pd->array, (pd->array_length+1)*sizeof(FBFile));
pd->array[pd->array_length].name = g_strdup ( ".." );
pd->array[pd->array_length].path = NULL;
pd->array[pd->array_length].type = UP;
pd->array_length++;
continue;
} else if ( rd->d_name[0] == '.' ) {
continue;
}
```
We do a similar filtering act for the rest of the files, we skip `fifo`, `blk`,`character` and `socket` files.
```c
switch ( rd->d_type )
{
case DT_BLK:
case DT_CHR:
case DT_FIFO:
case DT_UNKNOWN:
case DT_SOCK:
break;
case DT_REG:
case DT_DIR:
pd->array = g_realloc ( pd->array, (pd->array_length+1)*sizeof(FBFile));
pd->array[pd->array_length].name = g_strdup ( rd->d_name );
pd->array[pd->array_length].path = g_build_filename ( pd->current_dir, rd->d_name, NULL );
pd->array[pd->array_length].type = (rd->d_type == DT_DIR)? DIRECTORY: RFILE;
pd->array_length++;
}
}
closedir ( dir );
}
```
We then sort the list in a way the user expects this.
```c
g_qsort_with_data ( pd->array, pd->array_length, sizeof (FBFile ), compare, NULL );
}
```
Qsort here uses the following sort function:
```c
static gint compare ( gconstpointer a, gconstpointer b, gpointer data )
{
FBFile *fa = (FBFile*)a;
FBFile *fb = (FBFile*)b;
if ( fa->type != fb->type ){
return (fa->type - fb->type);
}
return g_strcmp0 ( fa->name, fb->name );
}
```
## Showing the entries
When showing each entry, rofi calls the `_get_display_value` function. It calls
them in two situations, to get the state and the display string. Or just to get
the new state. If you need to return a string (should always be malloced), the
`get_entry` parameter is set to 1.
We currently show the name, and prepend an icon using the Awesome Font.
```c
static char *_get_display_value ( const Mode *sw, unsigned int selected_line, G_GNUC_UNUSED int *state, G_GNUC_UNUSED GList **attr_list, int get_entry )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
// Only return the string if requested, otherwise only set state.
if ( !get_entry ) return NULL;
if ( pd->array[selected_line].type == DIRECTORY ){
return g_strdup_printf ( " %s", pd->array[selected_line].name);
} else if ( pd->array[selected_line].type == UP ){
return g_strdup( " ..");
} else {
return g_strdup_printf ( " %s", pd->array[selected_line].name);
}
return g_strdup("n/a");
}
```
The `selected_line` setting will always be within range 0 and the result of
`.get_num_entries`.
```c
static unsigned int file_browser_mode_get_num_entries ( const Mode *sw )
{
const FileBrowserModePrivateData *pd = (const FileBrowserModePrivateData *) mode_get_private_data ( sw );
return pd->array_length;
}
```
> The `attr_list` argument is there for more advanced markup of the string.
## Filtering the entries
When filtering we want to filter on the file name, we luckily store this entry
in `FBFile::name`.
To use **rofi**'s matching algorithm we can use the `helper_token_match` function.
```c
static int file_browser_token_match ( const Mode *sw, GRegex **tokens, unsigned int index )
{
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
// Call default matching function.
return helper_token_match ( tokens, pd->array[index].name);
}
```
The `index` setting will always be within range 0 and the result of
`.get_num_entries`.
## Running it
Now we should be able to build it, install it and run it and see the result.
```bash
rofi -show file_browser -modi file_browser
```
![rofi file browser](rofi-file-browser.png)
## Handling selected entries
This is just an example and can probably be implemented nicer.
Here it also shows some rudimentary parts in **rofi**, that show some of the
ugly details, that will be cleaned up in the future.
```c
static ModeMode file_browser_mode_result ( Mode *sw, int mretv, char **input, unsigned int selected_line )
{
ModeMode retv = MODE_EXIT;
FileBrowserModePrivateData *pd = (FileBrowserModePrivateData *) mode_get_private_data ( sw );
if ( mretv & MENU_NEXT ) {
retv = NEXT_DIALOG;
} else if ( mretv & MENU_PREVIOUS ) {
retv = PREVIOUS_DIALOG;
} else if ( mretv & MENU_QUICK_SWITCH ) {
retv = ( mretv & MENU_LOWER_MASK );
```
This is the user pressing `enter` on the entry. We handle it differently for
each type.
```c
} else if ( ( mretv & MENU_OK ) ) {
if ( selected_line < pd->array_length )
{
if ( pd->array[selected_line].type == UP ) {
GFile *new = g_file_get_parent ( pd->current_dir );
if ( new ){
g_object_unref ( pd->current_dir );
pd->current_dir = new;
free_list (pd);
get_file_browser ( sw );
return RESET_DIALOG;
}
} else if ( pd->array[selected_line].type == DIRECTORY ) {
GFile *new = g_file_new_for_path ( pd->array[selected_line].path );
g_object_unref ( pd->current_dir );
pd->current_dir = new;
free_list (pd);
get_file_browser ( sw );
return RESET_DIALOG;
} else if ( pd->array[selected_line].type == RFILE ) {
char *d = g_strescape ( pd->array[selected_line].path,NULL );
char *cmd = g_strdup_printf("xdg-open '%s'", d );
g_free(d);
char *cdir = g_file_get_path ( pd->current_dir );
helper_execute_command ( cdir,cmd, FALSE );
g_free ( cdir );
g_free ( cmd );
return MODE_EXIT;
}
}
retv = RELOAD_DIALOG;
```
Handle custom entry that does not match an entry:
```c
} else if ( (mretv&MENU_CUSTOM_INPUT) && *input ) {
char *p = rofi_expand_path ( *input );
char *dir = g_filename_from_utf8 ( p, -1, NULL, NULL, NULL );
g_free (p);
if ( g_file_test ( dir, G_FILE_TEST_EXISTS ) )
{
if ( g_file_test ( dir, G_FILE_TEST_IS_DIR ) ){
g_object_unref ( pd->current_dir );
pd->current_dir = g_file_new_for_path ( dir );
g_free ( dir );
free_list (pd);
get_file_browser ( sw );
return RESET_DIALOG;
}
}
g_free ( dir );
retv = RELOAD_DIALOG;
```
We do not support `delete`, just reload.
```c
} else if ( ( mretv & MENU_ENTRY_DELETE ) == MENU_ENTRY_DELETE ) {
retv = RELOAD_DIALOG;
}
return retv;
}
```
The `RESET_DIALOG` will clear the input bar and reload the view, `RELOAD_DIALOG`
will reload the view and re-filter based on the current text.
> Note: `rofi_expand_path` will expand `~` and `~me/` into it full absolute path.
> Note: `helper_execute_command` will spawn command.