Gentoo Forums
Gentoo Forums
Gentoo Forums
Quick Search: in
[HOWTO] Extend Thunar with DIY plugins
View unanswered posts
View posts from last 24 hours

Reply to topic    Gentoo Forums Forum Index Documentation, Tips & Tricks
View previous topic :: View next topic  
Author Message

Joined: 09 Mar 2016
Posts: 26

PostPosted: Fri Jul 08, 2016 7:29 pm    Post subject: [HOWTO] Extend Thunar with DIY plugins Reply with quote

Pick the language you are most familiar with and you can extend Thunar the way you want to via plugins.

In this post I'll show how to create compression and extraction functions that use nothing, but the shell itself which Thunar will utilize to manipulate files. For the fun I'll use python to create and extract zip archives instead installing moar dependencies such as zip, unzip, thunar-archive-plugin, xarchiver/file-roller/ark.

Let's start with the compression functions.

Filename compress.zsh

compresstar() { tar --verbose --dereference --create --file\
                 `basename $1`.tar "$@" ;}
compressxz()  { compresstar "$@"
                 xz --verbose --force -9 --extreme `basename $1.tar` ;}
compressgz()  { compresstar "$@"
                 __run_parallel pigz gzip $1 ;}
compressbz()  { compresstar "$@"
                 __run_parallel lbzip2 bzip2 $1 ;}
compresslz()  { compresstar "$@"
                 xz --verbose --force -9 --extreme \
                    --format=lzma `basename $1.tar` ;}
compresslz4() { compresstar "$@"
                 lz4 -vf9 `basename $1.tar`
                 rm `basename $1.tar` ;}
compresslzo() { compresstar "$@"
                 lzop --verbose --force -9 `basename $1.tar`
                 rm `basename $1.tar` ;}
compresslrz() { compresstar "$@"
                 lrzip --verbose --force -f -L 9 `basename $1.tar`
                 rm `basename $1.tar` }
compress7z()  { 7za a -mx=9 `basename $1`.7z "$@" ;}
compresszip() {
    python2 -c"import os;from zipfile import ZipFile,ZIP_DEFLATED;
ufo_obj='"$1"'.split(' ');the_list=list();path_join=os.path.join;
def zip_filez():
  with ZipFile(os.path.basename(ufo_obj[0])+'.zip','a',ZIP_DEFLATED) as archive:
    [archive.write(x) for x in the_list];
    for x in the_list:
      x=(x if not x.startswith(os.sep) else x.replace(os.sep,str(),1));
      print(' {0}adding{1}: {2}{3}{1}'.format(magenta,norm,blue,x));
for x in ufo_obj:
  if os.path.isdir(x):
    for root,_,files in os.walk(x):
      for z in files:
  else:  the_list.append(x);

The extraction function, filename extract.zsh

extract() {
  for xXx in "$@"
    if [[ -f $xXx ]]
      # --bzip2, --gzip, --xz, --lzma, --lzop
      # are 'long' `tar' options standing for:
      # extract the archive and filter it
      # through program --xxx
      case $xXx in
         *.tar.bz2) __fucktard $xXx  --bzip2          ;;
         *.bz2)     __fucktard $xXx  bz2-orig         ;;
         *.t[zb]?*) __fucktard $xXx  --bzip2          ;;
         *.tar.gz)  __fucktard $xXx  --gzip           ;;
         *.gz)      __fucktard $xXx  gz-orig          ;;
         *.t[ag]z)  __fucktard $xXx  --gzip           ;;
         *.tz)      __fucktard $xXx  --gzip           ;;
         *.tar.xz)  __fucktard $xXx  --xz             ;;
         *.xz)      __fucktard $xXx  xz-orig          ;;
         *.txz)     __fucktard $xXx  --xz             ;;
         *.tpxz)    __fucktard $xXx  --xz             ;;
         *.lzma)    __fucktard $xXx  --lzma           ;;
         *.tlz)     __fucktard $xXx  --lzma           ;;
         *.tar)     __fucktard $xXx  tar-orig         ;;
         *.rar)     __fucktard $xXx  rar-orig         ;;
         *.zip)     __fucktard $xXx  zip-orig         ;;
         *.xpi)     __fucktard $xXx  zip-orig         ;;
         *.lz4)     __fucktard $xXx  lz4-orig         ;;
         *.lrz)     __fucktard $xXx  lrz-orig         ;;
         *.tar.lzo) __fucktard $xXx  --lzop           ;;
         *.lzo)     __fucktard $xXx  lzo-orig         ;;
         *.Z)       uncompress $xXx                   ;;
         *.7z)      __fucktard $xXx  seven_zip        ;;
         *.exe)     cabextract $xXx                   ;;
         *)                                           ;;
  unset xXx

Non-interactive functions that will do most of the heavy work.

Filename diehard.zsh:

# compress.zsh
# pixz does not show any benefits
# pbzip2 is twice slower than lbzip2
__run_parallel() {
    [[ -x "$(command -v $1)" ]] && local comp_prog=$1 \
        || local comp_prog=$2

    ${comp_prog} --verbose --force -9 `basename $3.tar`

# extract.zsh
# The extraction function that does
# all the heavy lifting by using steroids
# and several other prohibited substances
__fucktard() {
    [[ "$(dirname $1)" == "." ]] && local dir_name="${PWD}" \
        || local dir_name="$(dirname $1)"

    local f_name="$(basename $1)"
    local temp_one="$(mktemp --directory --tmpdir XXXXXXX)"
    local temp_two="$(mktemp --directory --tmpdir=${temp_one} XXXXXXX)"

    mv $1 "${temp_two}" && cd "${temp_two}"

    case $2 in
        bz2-orig)  bunzip2 --verbose "${f_name}"              ;;
        gz-orig)   gunzip  --verbose "${f_name}"              ;;
        xz-orig)   unxz    --verbose "${f_name}"              ;;
        zip-orig)  python2 -c"from zipfile import ZipFile;
with ZipFile('"${f_name}"', 'r') as archive:
    print('\n'.join(' \033[1;95mextracted\033[0m: \033[1;94m{0}\033[0m'.format(x.filename)\
    for x in archive.infolist()));archive.extractall()"       ;;
        seven_zip) 7z x "${f_name}"                           ;;
        rar-orig)  unrar x "${f_name}"                        ;;
        lzop-orig) lzop --verbose --extract "${f_name}"       ;;
        lrz-orig)  lrzuntar -v "${f_name}"                    ;;
                if [[ "$2" == "lz4-orig"  ]]
                    lz4 --verbose --decompress "${f_name}"
                if [[ "${f_name}" == *.tar ]]
                    tar --skip-old-files --no-overwrite-dir \
                        --verbose --extract --file "${f_name}"
                fi                                         ;;
           # filter the archive through program $2(--xxx)
           *) tar --skip-old-files --no-overwrite-dir\
                  --verbose --extract $2 --file "${f_name}"  ;;
    # The more scenarios I can think of, `mv'
    # becomes true trouble maker. Let's leave
    # python to deal most common issues (if any)
    python2 -c"import os;from shutil import move,rmtree;
for x in whos_here:
    if is_grenate(say_what):  defuse(say_what);
    if is_dynamite(say_what): rmtree(say_what);
[move(x,'"$dir_name"') for x in whos_here];"
    cd "${dir_name}" && rm -rf "${temp_one}"

The plugin interface that invokes the archive creation and extraction.

Filename my_thunar_plugin.zsh:

#!/usr/bin/env zsh
    'compress.zsh' 'extract.zsh'

for x in "${files_to_source[@]}"
    source "$HOME/.config/zsh/functions/$x"
unset x files_to_source

case $1 in
   gzf) shift; compressgz "$@" ;;
  extr) shift; extract "$@"    ;;
     *)                        ;;

Open up Thunar, select Edit and then click Configure custom actions located at the very bottom.

A new window will be opened, click the + symbol to the right and add a new custom action. The first custom action that we will create is archive extraction.

In the Name field type Extract here, and in the Command field type:

zsh --exec $HOME/.config/misc/my_thunar_plugin.zsh extr %N

Switch to Appearance tab and in the File Pattern field type:


Make sure to toggle the Other Files switch.

Now the second action - archive creation and compression. Pretty much the same steps with some exceptions.

Again click the + symbol, in the Name field type Compress to gzip and in the Command field type:

zsh --exec $HOME/.config/misc/my_thunar_plugin.zsh gzf %N

Switch to Appearance tab and in the File Pattern field type *
and toggle all the switches.

That was it, re-open Thunar and you good to go.

Once you select single/multiple file(s) in Thunar and right click, these custom actions should appear.

This approach of writing Thunar plugins by using the shell itself allows you to invoke the functions on a daily basis as well use them with Thunar when you need to.

The use of /tmp also helps to speed up the extraction since it reads and writes only in the RAM without touching your drive during that time.

Since there is no GUI involved in here, the extraction is lightning fast, as the GUI programs have to first of all read the archive content before even the extraction to begin (the tiny window that moves the progress bar), which in most cases will cause twice slower extraction than using the shell alternatives presented here.
Back to top
View user's profile Send private message

Joined: 09 Mar 2016
Posts: 26

PostPosted: Sat Jul 09, 2016 8:09 pm    Post subject: Reply with quote

I've had enough crashed sessions caused by just trying to rename some file/directory in Thunar.

It's not like this problem started since 1.6.10, it's been for awhile.

Decided that it's time to take action and replace the built-in renaming function. The new renaming function will sanitize the input. Only valid alphabets, numbers and some punctuation marks are allowed, everything else will be discarded.

Upstream repo

   Copyright Aaron Caffrey
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   GNU General Public License for more details.
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA.

   Compile with:
    gcc -Wall -Wextra -O2 main.c -o rename `pkg-config --cflags --libs gtk+-3.0`

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <gtk/gtk.h>

#define POS_LEFT 1
#define POS_RIGHT 2
#define MAX_NAME_LEN 300

void try_to_rename(void);
void sanitize_name(char *);
void display_warning(const char *, const char *);

GtkWidget *window, *entry;
char orig[MAX_NAME_LEN];

int main(int argc, char *argv[]) {
  if (argc == 1 || argc > 2) {
    exit (EXIT_FAILURE);

  snprintf (orig, MAX_NAME_LEN, "%s", argv[1]);
  if (0 != access (orig, F_OK) || 0 != access (orig, W_OK)) {
    exit (EXIT_FAILURE);

  GtkWidget *grid, *entry_label, *ok_button;

  gtk_init(&argc, &argv);

  window       = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW(window), "Rename");
  gtk_container_set_border_width (GTK_CONTAINER(window), 6);
  gtk_window_set_default_size (GTK_WINDOW(window), 250, 20);

  grid         = gtk_grid_new();
  gtk_grid_set_row_spacing (GTK_GRID(grid), 7);
  gtk_grid_set_column_spacing (GTK_GRID(grid), 5);
  gtk_container_set_border_width (GTK_CONTAINER(grid), 2);
  gtk_container_add (GTK_CONTAINER(window), grid);

  entry_label  = gtk_label_new ("New name:");
  gtk_grid_attach (GTK_GRID(grid), entry_label, POS_LEFT, 1, 1, 1);

  entry        = gtk_entry_new();
  gtk_entry_set_width_chars (GTK_ENTRY(entry), 1);
  gtk_entry_set_text (GTK_ENTRY(entry), orig);
  gtk_entry_set_max_length (GTK_ENTRY(entry), 200);
  gtk_grid_attach (GTK_GRID(grid), entry, POS_RIGHT, 1, 1, 1);

  ok_button    = gtk_button_new_with_label ("OK");
  g_signal_connect (G_OBJECT(ok_button), "clicked", G_CALLBACK(try_to_rename), NULL);
  gtk_grid_attach (GTK_GRID(grid), ok_button, POS_RIGHT, 2, 1, 1);

  g_object_set (G_OBJECT(entry), "activates-default", TRUE, NULL);
  g_object_set (G_OBJECT(ok_button), "can-default", TRUE, "has-default", TRUE, NULL);

  g_signal_connect (G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
  gtk_widget_show_all (window);

  return EXIT_SUCCESS;

void try_to_rename(void) {
  char sanitized[MAX_NAME_LEN];
  const gchar *new_name = gtk_entry_get_text (GTK_ENTRY(entry));

  snprintf (sanitized, MAX_NAME_LEN, "%s", new_name);
  sanitize_name (sanitized);
  gtk_entry_set_text (GTK_ENTRY(entry), sanitized);

  if (0 == strcmp (orig, sanitized)) {
    display_warning ("Not funny", "Same name ?!");

  } else if ('\0' == sanitized[0] ||
      '\0' == orig[0]) {
    display_warning ("U mad br0", "Empty huh ?!");

  } else {

    if (0 != access (orig, F_OK)) {
      display_warning ("Snafu ?!", "The file doesnt exist anymore.");
    } else {
      rename (orig, sanitized);

void sanitize_name (char *str) {
  char *ptr = str;
  char allowed[] = "()_.-";

  /* Discard all the leading '    space' */
  while (*ptr) {
    if (isspace((unsigned char) *ptr)) {
    } else {

  for (; *ptr; ptr++) {
    if (isspace((unsigned char) *ptr) &&
        (isalnum((unsigned char) *(ptr + 1)) ||
        NULL != strchr(allowed, *(ptr + 1)))) {
      *str++ = '_';
    } else if (isalnum((unsigned char) *ptr) || (NULL != strchr(allowed, *ptr)
        && (*ptr != *(ptr + 1)))) {
      *str++ = *ptr;
  *str = '\0';

void display_warning(const char *str1, const char *str2) {
  GtkWidget *warning = gtk_message_dialog_new(
  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG(warning), "%s", str2);
  gtk_dialog_run (GTK_DIALOG(warning));
  gtk_widget_destroy (warning);
Back to top
View user's profile Send private message
Display posts from previous:   
Reply to topic    Gentoo Forums Forum Index Documentation, Tips & Tricks All times are GMT
Page 1 of 1

Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum