Qball's Weblog

Rofi 1.4.0: Sneak Preview Plugins

Tags Rofi  plugins 

On request a blog post of a feature in 1.4.0 I wanted to keep kind-of quiet: Plugins. Early on rofi was already setup to make it trivial to add plugin support for modes. For the upcoming 1.4.0 release a 20 lines (it has grown since that) patch is included adding supports for plugins.

This blog post will go through the steps of writing a small custom mode for rofi.

Build system

Rofi uses autotools 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 for now.

To make life easier I create a template project here.

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])

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.

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=${libdir}/rofi/

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:

You can now test the plugin by calling:

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:

/**
 * 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:

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.

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:

So we add an enum:

enum FBFileType {                                                         
    UP,                                                                   
    DIRECTORY,                                                            
    RFILE,                                                                
};                                                                        

We need a structure that hold each entry.

typedef struct {                                                          
    char *name;                                                           
    char *path;                                                           
    enum FBFileType type;                                                 
} FBFile;                                                                 

Then in the private data we hold all the relevant information.

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.

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.

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:

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.

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.

    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.

            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.

            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.

    g_qsort_with_data ( pd->array, pd->array_length, sizeof (FBFile ), compare, NULL );
}

Qsort here uses the following sort function:

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.

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.

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.

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.

rofi -show file_browser -modi file_browser

rofi file browser

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.


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.

    } 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:

    } 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.

    } 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.

The Full Code

The full code will become available here.

comments powered by Disqus