Writing a git-annex plugin for ranger
I use git-annex to distribute and synchronize fairly large and mostly static files across different machines. However, being based on Git makes it pretty uncomfortable to use from the command line. So, why not integrating it into our favorite command line file manager, ranger? Because I struggled a bit with ranger’s internals, I will outline how I wrote the plugin.
Initialization
First of all, we need a place for our plugin. By default ranger imports every
Python module from $XDG_CONFIG_HOME/ranger/plugins
in lexicographic order. If
$XDG_CONFIG_HOME
is not set, ~/.config
is used as the default alternative.
To use the plugin from within ranger, you need to provide code that hooks into
one of the two methods provided by the ranger API. hook_init
is called before
the UI is ready, which means you can dump output on stdout
, whereas the UI can
be used in hook_ready
. The suggested way is not to replace the original
function but chaining up like this:
import ranger.api
old_hook_init = ranger.api.hook_init
def hook_init(fm):
# setup
return old_hook_init(fm)
ranger.api.hook_init = hook_init
Adding new commands
In the introductory post, I briefly explained how to write custom commands which
you add to your commands.py
file: You simply subclass from
ranger.api.commands.Command
and write code in the execute
method. However,
commands defined in a plugin are not automatically added to the global command
list. For this you need to extend the commands
dictionary of the file manager
instance, i.e.
def hook_init(fm):
fm.commands.commands['annex_copy'] = copy
When using the annex_copy
command, tab-completion should cycle through all available
remotes. This is done by returning an iterable in the tab
method. In the
execute
method you can access arguments, by calls to the arg
method:
class copy(ranger.api.commands.Command):
def tab(self):
return ('annex_copy {}'.format(r) for r in remotes)
def execute(self):
remote = self.arg(1)
Asynchronous calls
The git-annex plugin works on the current or currently selected list of files,
which you can get via fm.env.get_selection()
. To avoid blocking the UI while
fetching large files, I use the CommandLoader
to run the git annex
commands
in the background (thanks @hut). The loader emits a
signal when the action is finished to which we subscribe in order to refresh the
directory content:
class copy(ranger.api.commands.Command):
def execute(self):
remote = self.arg(1)
def reload_dir():
self.fm.thisdir.unload()
self.fm.thisdir.load_content()
for path in (str(p) for p in self.fm.env.get_selection()):
fname = os.path.basename(path)
cmd = ['git', 'annex', 'copy', '-t', remote, fname]
loader = CommandLoader(cmd, "annex_copy to remote")
loader.signal_bind('after', reload_dir)
fm.loader.add(loader)
Long running actions can be cancelled by the user, so if you need to clean up
you should add that extra code to the cancel
method.
Wrap up
That’s it, a plugin that registers new commands for interacting with git-annex an asynchronous way. Bug reports and pull requests are welcome.