Plugin API

The plugin API gives you immense power to customize and
extend fman. For a quick introduction to writing your first
plugin, please see
here.
Once you've gotten started, you may also want to take a look
at the
Core plugin. It
gives lots of examples of how the API is meant to be used.

If you have questions about developing plugins for fman, or
want to suggest improvements/extensions to the API, please
get in touch. We
want to make fman's plugin support truly outstanding. Your
feedback can guide us there!

Everything is a URL

In fman's API, files are identified by URLs instead of
traditional file paths. For example:
file://C:/Windows instead of
C:\Windows. This lets fman handle many
different file systems. For instance, when working with a
Zip archive, the URL
zip://C:/archive.zip/directory/file.txt
is used to identify one of its members.

The module fman.url exposes several
functions that make working with URLs easier:

In all cases, it is recommended that you use the
fman.fs module to work with files.
Eg. use fman.fs.mkdir(...) instead of
Python's os.mkdir(...) to create directories.
The reason is that fman
caches files. When you use the
recommended functions, you not only make your plugin
potentially usable on more file systems. You also let fman
update its caches more quickly.

Finally, it must be noted that fman URLs are not URLs in the
strict technical sense. For example,
file://C:/My file.txt is not valid according to
the official URL standard (it would have to be
encoded as file:///C:/My%20file.txt). We ignore
these requirements. For our purposes, a URL is any string
that starts with a scheme:// prefix and is
optionally followed by a path whose components are separated
by forward slashes /.

Module fman

This module contains the main and most basic functions for
interacting with fman. When writing a plugin, you usually
create a subclass of one of the following:

DirectoryPaneCommand
Use this if you want to perform an action in the current
folder. For example, fman's own commands for copying
and moving files are based on this class.

ApplicationCommand
This class is meant for "global" functionality that does
not depend on the current folder. For instance, the
built-in About command, which displays version
and licensing information, is based on this class.

DirectoryPaneListener
Lets you react to events in the current folder such as
when the user doubleclicks a file. You can also be
notified when the user navigates to another folder. The
built-in GoTo feature uses this to count how
often you visit each directory, and then recommends
the most-visited locations first.

show_alert(text, buttons=OK, default_button=OK)

Shows an alert to the user:

The optional arguments
buttons and default_button let you
specify the buttons in the dialog. The available buttons
are OK, CANCEL, YES,
NO, YES_TO_ALL,
NO_TO_ALL and ABORT. You can
combine them with |. For instance,
YES | NO shows two buttons, Yes and No.

The function returns the button clicked by the user. It is
guaranteed that one of the supplied buttons is
always returned: If there is only one button and the user
cancels the dialog, that button is returned. If there are
two buttons or more and none of them is NO,
CANCEL or ABORT, then the dialog
is not cancellable. Otherwise, the dialog is cancellable and
an appropriate button is returned when the user does cancel.

show_prompt(text, default='', selection_start=0, selection_end=None)

Prompts the user to enter a value:

If given, the default parameter lets you
prefill the text in the dialog. When you do this,
selection_start and selection_end
let you specify which part(s) of the text should be
pre-selected. When selection_end is not given,
it defaults to the end of the text. If you only want to
place the cursor without selecting any text, use the same
value for both selection_start and
selection_end.

The function returns a tuple text, ok. When the
user accepts the dialog, ok is
True and text contains the text
he entered (possibly the empty string). If the dialog was
cancelled, '', False is returned.

show_status_message(text, timeout_secs=None)

If optional parameter timeout_secs is given,
the message disappears after the specified number of
seconds.

clear_status_message()

Clears the status bar by writing a default "idle text"
(usually "Ready") into the status bar.

show_file_open_dialog(caption, dir_path, filter_text='')

Shows a native file open dialog in the given directory.
The dialog's caption is set to the given
caption. If dir_path points to a
file, that file is selected. You can use
filter_text to only show some files. Valid
examples are *.jpg,
Images (*.png *.jpg). To use multiple
filters, separate them with ;;. For instance:
Images (*.png);;Text files (*.txt).

The function returns the path of the file chosen by the
user. If the dialog was cancelled, the empty string
'' is returned.

show_quicksearch(get_items, get_tab_completion=None)

The parameter get_items must be a function
taking a single parameter, query. It is
supposed to return an iterable of
QuicksearchItems that
represent the items to be displayed when the user types in
the given query.

The optional parameter get_tab_completion lets
you customize the Tab completion behaviour. It should be a
function taking two parameters: query and
curr_item. The first parameter indicates the
text which the user typed in. The second gives the currently
selected QuicksearchItem. If
no item is currently shown, it is None. The
function should return the string the query
should be completed to.

If the user cancels the dialog, None is
returned. Otherwise, the result is a tuple
query, value. query is the
(possibly unfinished) query which the user typed into the
dialog. In the example above, it would be
'fman'. The second element of the tuple is the
value of the selected
QuicksearchItem. In the
example above, this would be the row
~/dev/fman. If no item was selected (because
no suggestions were displayed), the second tuple element is
None.

The following example shows a Quicksearch dialog with a
filterable list of elements:

A suggestion in a Quicksearch dialog (see
show_quicksearch(...)).
The title, hint and
description fields are best summarized by an
image:

Thus in the image:

title = 'ZipSelected'

hint = '5★'

description = 'Zip the files selected in the current panel.'

In addition to these attributes, every QuicksearchItem has a
value and (optionally) a
highlight.

The motivation for value becomes clear when you
think about fman's built-in GoTo dialog. A suggested path
might be ~ (the user's home directory). But the
tilde ~ is just a nice representation for the
user. What it stands for is the full path to the home
directory (eg. C:\Users\Michael). In this case,
the title would be '~' but
the value is 'C:\Users\Michael'.

If title is not given, the value
is used (/displayed) instead.

Finally, highlight specifies which characters
in the title should be highlighted. In the above example,
you see that the first three characters Zip of the
title are highlighted in white. This is achieved by setting
highlight to the indices of these characters:
[0, 1, 2]. If we wanted to highlight just the
Z and the S instead (as in
ZipSelected), we would have to set this to
[0, 3].

load_json(name, default=None, save_on_quit=False)

Loads the JSON file with the given name from the currently
loaded plugins. If the file does not exist,
default is returned. If you pass
save_on_quit=True, then any changes you make to
the returned object are persisted across fman restarts.
Either way, within one fman session, it is guaranteed that
multiple calls to load_json(...) with the same JSON name
always return the same object.

As an example, here is how you can prompt the user for a
setting that is persisted across fman restarts:

(Note: It would be more elegant to use
save_json(...) instead of
save_on_quit here. But the above lets us
demonstrate all parameters.)

Automatic merging of JSON files

When multiple JSON files with the same name exist,
load_json(...) merges them automatically. This naturally
occurs when a file appears in multiple plugins. On top of
that, load_json(...) also supports platform-specific
JSON files. For example, suppose you are on Windows and
create two files:

Settings.json with contents {"a": 1, "b": 2}

Settings (Windows).json with contents {"b": 99}

When you call load_json('Settings.json'), you
get {"a": 1, "b": 99}. (On other platforms, use
My Settings (Mac).json for Mac and
My Settings (Linux).json for Linux.)

In the above example, we defined a dictionary
{...}. Values in a dictionary override
previous values.
JSON files can also contain lists [...]. In
this case, lists loaded later are prepended to
previous ones.
The order in which the files are loaded (and thus merged)
is given by the
Plugin load order.

save_json(name, value=None)

Saves the JSON file with the given name in the User's
Settings plugin. If value is not given,
then the file must have been loaded with
load_json(...) first.

get_application_command_aliases(command_name)

Returns a list of the
aliases
associated with the given
ApplicationCommand. If the
ApplicationCommand does not define .aliases,
then a single alias is auto-generated from the name of the
command (eg. MyCommand gets alias
My command). The aliases are what's displayed
by the
Command Palette.

An alternative to DirectoryPaneCommands are
ApplicationCommands. Use
the latter if your command is global to the entire
application and does not operate in a specific pane or
directory.

DirectoryPaneCommand.__call__(**kwargs)

When implementing a DirectoryPaneCommand, you must define
__call__ as in the previous example. You are
free to define additional parameters. For instance, here is
the definition of fman's command for moving the cursor down
to the next file:

DirectoryPaneCommand.get_chosen_files()

Returns a list of the currently selected files (=the files
marked "red"). If no files are selected, returns a
one-element list consisting of the file under the cursor.
If there is no file under the cursor (eg. when the current
directory is empty), then an empty list is returned.

Because the "empty directory" case is common, your plugin
should handle it. A common way of doing this is:

DirectoryPane

DirectoryPane.get_path()

Returns the URL of the directory currently displayed by this
directory pane. For instance, if you are viewing the
contents of C:\Windows, then
file://C:/Windows is returned.

DirectoryPane.set_path(dir_url, callback=None)

Navigate to the given directory. The optional
callback parameter gets called when the new
directory has been fully loaded. For example, to navigate to
C:\Windows and place the cursor at
C:\Windows\System32:

DirectoryPane.get_selected_files()

Returns a list of the currently selected (="red") files. If
no files are selected in this way, the empty list
[] is returned.

DirectoryPane.get_file_under_cursor()

Returns the file under the cursor (the "cursor" is what you
move when you press Arrow Up/Down). If there is no file
currently under the cursor, None is returned.
This happens when the pane is currently displaying an empty
directory.

DirectoryPane.place_cursor_at(file_url)

Place the cursor at the given file in the current directory.
If the file does not exist (or hast not yet been loaded!), a
ValueError is raised. See
set_path(...) for an
example.

DirectoryPane.move_cursor_down(toggle_selection=False)

Move the cursor down. If toggle_selection is
given and true, then the current file is also (de-)selected.

DirectoryPane.move_cursor_up(toggle_selection=False)

Move the cursor up. If toggle_selection is
given and true, then the current file is also (de-)selected.

DirectoryPane.move_cursor_home(toggle_selection=False)

Move the cursor to the top. If toggle_selection
is given and true, then the files above the cursor are also
(de-)selected.

DirectoryPane.move_cursor_end(toggle_selection=False)

Move the cursor to the bottom. If toggle_selection
is given and true, then the files below the cursor are also
(de-)selected.

DirectoryPane.move_cursor_page_down(toggle_selection=False)

Move the cursor one page down. If
toggle_selection is given and true, then the
files between the current and the new cursor position are
also (de-)selected.

DirectoryPane.move_cursor_page_up(toggle_selection=False)

Move the cursor one page up. If
toggle_selection is given and true, then the
files between the current and the new cursor position are
also (de-)selected.

DirectoryPane.select_all()

Select all files in the current directory.

DirectoryPane.clear_selection()

Deselect all files in the current directory.

DirectoryPane.toggle_selection(file_url)

If the given file in the current directory is selected,
deselect it. Otherwise, select it.

DirectoryPane.reload()

Reload the current directory. Note: If you
find that you need to call this function because fman is not
noticing that your files or directories have changed, that's
a bug. Please
get in touch.

DirectoryPane.get_commands()

Return the names of all
DirectoryPaneCommands
available for this pane, as a set of strings. You can then
use run_command to
run them. This function is used by the Command Palette to
find all available commands.

DirectoryPane.run_command(name, args=None)

Run the
DirectoryPaneCommand
with the given name in the context of this
pane. The optional parameter args, if given,
must be a dictionary of arguments to pass to the command's
__call__
method. For instance:

DirectoryPane.get_command_aliases(command_name)

Returns a list of the
aliases
associated with the given
DirectoryPaneCommand. If
the DirectoryPaneCommand does not define
.aliases, then a single alias is auto-generated
from the name of the command (eg. MyCommand
gets alias My command). The aliases are what's
displayed by the
Command Palette.

DirectoryPane.focus()

Start editing the name of the given file in the current
directory. To be notified when the editing process is
complete, implement
DirectoryPaneListener.on_name_edited(...).
For an example, please see the implementation of the
Rename command (Shift+F6) in the
Core plugin.

The optional parameters selection_start and
selection_end let you specify which part(s) of
the file name should be pre-selected. The default
implementation uses this to only select the file's base name
without the extension. If you only want to place the cursor
without selecting any text, use the same value for both
selection_start and selection_end.

DirectoryPane.get_columns()

Returns a list of the names of the columns in the current
pane. For instance:
['core.Name', 'core.Size', 'core.Modified'].

DirectoryPane.set_sort_column(column_index, ascending=True)

Sets the sort order of files and directories in this pane.
The column_index parameter identifies the
column by which the files are to be sorted. For instance, if
get_columns()
returns
['core.Name', 'core.Size', 'core.Modified']
and you set column_index to 1,
then the files are sorted by their Size. The
ascending parameter lets you specify whether
you want the files sorted in ascending or descending order.

DirectoryPane.get_sort_column()

Returns a tuple column_index, ascending that
indicates the current sort order. For the meaning of the
two values, see
set_sort_column(...).

DirectoryPane.window

The fman Window this directory pane
belongs to. A common use case for this attribute is to find
out what the "opposite" pane is in copy/move operations:

The most interesting capability of DirectoryPaneListener
right now is probably to rewrite commands via
on_command(...).

DirectoryPaneListener.on_command(command_name, args)

Implement this function to be notified when a command is
executed, or to execute another command instead. The
function receives the name of the command about to be
executed and its args as a (possibly empty)
dictionary.

To rewrite the command that is executed, you can return a
new tuple command_name, args that is then
executed instead. The Core plugin for instance uses this to
display the contents of Zip files in fman: The built-in Zip
file system handles zip:// URLs. But when you
press Enter on a local file, its URL is
file://, not zip://. To make the
zip:// file system handle the file, the Core
plugin rewrites the URL via (roughly) the following logic:

It is currently possible to redefine existing commands by
simply creating a command with the same name in your own
plugin. This is discouraged. Instead, define a command with
a new name and use on_command(...) to rewrite
the old command to your new one.

DirectoryPaneListener.on_path_changed()

This method is called whenever the user navigates to a
different directory. The
GoTo command
uses this to keep track of the directories you visit most
often, so it can suggest them to you first. Use
self.pane.get_path() to find out what the new
path is.

DirectoryPaneListener.on_doubleclicked(file_url)

Implement this method to be notified when a file was
doubleclicked.

DirectoryPaneListener.on_name_edited(file_url, new_name)

This method is called when the user changes the name of a
file. The process of editing the name is normally started
via
DirectoryPane.edit_name(...).

This method is called at the end of a drag and drop
operation, when the user drops the dragged files in fman.
The files are given by the list file_urls.
dest_dir is the URL of the directory in fman on
which the files were dropped.
is_copy_not_move is a boolean flag indicating
whether the files should be copied or moved: The user
indicates this by pressing Ctrl while
performing the drag (Alt on Mac).

Module fman.url

As explained in the
Introduction, many of the
functions in fman's API use URLs. This module makes it
easier to work with them.

The functions in this module simply perform string
manipulations. They do not actually query the file system.
For this, you should use module
fman.fs.

Except where noted otherwise, the functions in this module
are platform-independent. That is, given the same inputs,
they return the same output independently of the operating
system they are executed on.

splitscheme(url)

Splits the given URL into a pair scheme, path
such that scheme ends with '://'
and url == scheme + path. For example:
splitscheme('file://C:/test.txt') gives
'file://', 'C:/test.txt'. A common use case of
this is checking whether the URL of a file is supported by
your command / plugin. Please see the section
Everything is a URL for an example.

If the URL is not valid because it doesn't contain
://, then a ValueError is raised.

as_url(local_file_path, scheme='file://')

Converts the given file path into a URL with the given
scheme. For instance, on Windows,
as_url(r'C:\Directory') returns
'file://C:/Directory'.

If you want the URL to have a scheme other than
file://, supply the optional
scheme parameter.

This function may return platform-dependent results.

as_human_readable(url)

If given a file:// URL, this function serves as
the inverse of as_url(...). That is,
it turns file:// URLs into their corresponding
local file paths. Eg. on Windows,
file://C:/test.txt becomes
C:\test.txt.

If the given URL is not a file:// URL, it is
returned unchanged.

The function gets its name from its use for presenting URLs
and paths to the user. For example: Say you want to prompt
the user before overwriting a file (which is given to you as
a URL by fman's API). If it is a file:// URL,
then you want to show it to the user as a local file path
(ie. you want to ask "Do you want to override C:\test.txt",
not "... file://C:/test.txt"). Otherwise, you want to show
the URL unchanged.

The return value of this function is platform-dependent.

dirname(url)

Returns the parent directory of the given URL. For example:
dirname('file://C:/test.txt') returns
file://C:. Like all functions in this module,
this is a purely lexicographic operation. It does not
resolve ... For instance:
dirname('file:///Users/..') gives
file:///Users. If the URL consists of a scheme
only (eg. 'file://'), it is returned unchanged.
Perhaps also notable is that
dirname('file:///') returns
file://.

basename(url)

Returns the last component of the given URL's path. For
instance: basename('file://C:/test.txt')
returns test.txt. If the last component is
empty (as eg. in file:/// and
file://), then the empty string ''
is returned.

join(url, *paths)

Constructs a new URL relative to the given url.
A very common use case of this function is to construct the
URL of a file in the current directory. For example, here is
the (rough) implementation of fman's built-in command for
creating a new directory:

Module fman.fs

This module lets you interact with the file system. As
explained in section
Everything is a URL, it does not use
paths C:\test.txt to identify files, but URLs
file://C:/test.txt. This makes it possible for
fman to uniformly support many different file systems, such
as zip://, ftp://,
dropbox:// etc.

Caching

To improve performance, fman's file system backend caches
files and their attributes. For example, when you call
iterdir(...) to list the contents of a
directory, then repeat calls to this function with the same
parameter don't query the file system, but return a cached
result instead.

The modifying functions in this module, such as
mkdir(...) for creating a directory,
automatically update the cache. For this reason, it is
recommended that you use the functions in this module
whenever possible. So in the example just given, the
recommendation would be
not to use
Python's os.mkdir(...), even if it is
available.

exists(url)

Returns True or False depending on
whether the file with the given URL exists.

is_dir(existing_url)

Returns True or False depending on
whether the given URL denotes a directory. If the URL does
not exist, FileNotFoundError is raised. This is
different from Python's isdir(...), which
returns False.

iterdir(url)

Returns the names of the files in the given directory. This
is similar to Python's os.listdir(...), but
returns an
iterable.
The difference is that listdir(...) loads and returns all
contents at once. This means that you need to "wait" until
all contents have been loaded before you can access even the
first one. iterdir(...) on the other hand returns the
files one by one, and may thus let you access the first ones
sooner. You can iterate over the results just like you would
iterate over a list:

If you really do need a list, use
list(iterdir(url)) to load all files at once.

touch(url)

Create the file with the given URL. If the file already
exists, update its modification time to the current time.

mkdir(url)

Create the given directory. If it already exists, a
FileExistsError is raised. If the parent
directory of url does not yet exist,
FileNotFoundError is raised. If you want to
create a directory and all its parent directories,
use makedirs(...) instead.

makedirs(url, exist_ok=False)

Create the given directory and all its parent directories.
Unless exist_ok is True, a
FileExistsError is raised if the directory
already exists.

move(src_url, dst_url)

Move the file or directory given by src_url to
dst_url. It is important that
dst_url must be the final destination URL, not
just the parent directory. That is, you can't call
move('file://C:/test.txt', 'file://C:/dir').
Instead, you should call
move('file://C:/test.txt', 'file://C:/dir/test.txt').

copy(src_url, dst_url)

Copy the file or directory given by src_url to
dst_url. It is important that
dst_url must be the final destination URL, not
just the parent directory. That is, you can't call
copy('file://C:/test.txt', 'file://C:/dir').
Instead, you should call
copy('file://C:/test.txt', 'file://C:/dir/test.txt').

move_to_trash(url)

Move the file or directory with the given URL to the
respective file system's trash. For example:
move_to_trash('file://...') moves the file to
your OS's trash but
move_to_trash('dropbox://...') may move the
file to Dropbox's recycle bin. If the operation is not
supported,
io.UnsupportedOperation
is raised.

delete(url)

Permanently delete the given file or directory.

samefile(url1, url2)

Return True if both URLs refer to the same file
or directory.

query(url, fs_method_name)

The Name,
Size and
Modified columns are
well-known. But what if a file system wants to display
another value? For example, the zip:// file
system may want to display the packed size of a file in an
archive.

query(...) lets you call arbitrary methods on
FileSystem classes. In the example
above, the zip:// file system could define a
method packed_size(path):

This has several consequences: First, you can use
query(...) to obtain values which your own
FileSystem placed in the cache
via Cache.put(...). Second, it
means query(...) also puts items in the cache
and may return cached values for consecutive calls.

FileSystem

Extend this class to add support for new file systems. fman
does this itself to implement support for local files (via
the file:// FS), Zip files (via
zip://) or the drives:// file
system on Windows that shows your drives. You can find the
source code of these implementations in the
Core plugin.
For a more introductory example, check out
this blog post.

FileSystem.scheme

As mentioned in the introduction, every
file in fman is identified by a URL. For example, a file on
your local hard drive may have the URL
file://C:/test.txt, whereas a member of a Zip
archive may be identified as
zip://C:/archive.zip/member.txt.

The scheme property lets fman determine which
FileSystem is responsible for handling a given
URL. You must set it to a string ending with
://. For instance:

class Example(FileSystem):
scheme = 'example://'
...

This tells fman that URLs starting with
example:// are to be handled by your
FileSystem.

Every file system's scheme must be unique. In
other words, "overriding" other another file system by
reusing its scheme is not supported.

FileSystem.iterdir(path)

Implement this method to tell fman which files (and
directories) are in the folder with the given
path. It should return an
iterable
of file names. For instance:

Note how the two files from the code snippet are visible and
the location is example://.

Instead of returning a list [...], a common
idiom is to use Python's yield keyword:

def iterdir(self, path):
yield 'File.txt'
yield 'Image.jpg'

This has the following advantage: When you use
return, fman only receives the file names once
the entire iterdir(...) computation has
completed. Say for example that you use a custom file system
to implement file search. Then fman can display the search
results only after the entire disk has been searched. If on
the other hand you use yield, then the files
are displayed one by one as they are found.

FileSystem.is_dir(path)

Implement this method to tell fman whether the given path
is a file or a directory. If the given path does not exist,
you must raise a FileNotFoundError. This is
unlike Python's standard
isdir(...)
function, which returns False when the path
does not exist. For an example of this method, see the
introductory blog post.

FileSystem.get_default_columns(path)

Return the qualified names of the columns that should be
displayed by default for the given path. The standard
implementation returns the one-element tuple
('core.Name',). Override this method to display
other columns. A typical implementation looks as follows:

Only the Name column works "out of the box". To get the
Size or Modified columns to work for your file system,
please see here. If you want
to define an entirely new column (say a Permissions column),
please see the documentation of the
Column class.

The qualified names you need to return from this
method are of the form
'package_name.class_name'. For example, if the
code of your plugin lies in
my_plugin/__init__.py and your column class is
called MyColumn, then you need to return
'my_plugin.MyColumn'.

FileSystem.resolve(path)

Given the path of a file on this file system, return the
canonical URL of the file, possibly on a different file
system. If the given path does not exist, raise a
FileNotFoundError.

Consider the following example: You're inside a Zip file
at zip://C:/test.zip and go up a directory.
fman removes the last part of the URL and navigates to
zip://C:. This is not a Zip archive.
What we want is to open file://C: instead.
Overriding resolve(...) lets us do this: Before
fman opens zip://C:, it calls
ZipFileSystem.resolve('C:'). This returns
file://C: and so fman goes through the usual
procedure of displaying local files.

Another example where resolve(...) is useful is
for the built-in drives:// file system, which
displays your drives on Windows. When
DrivesFileSystem.iterdir(...) returns
['C:', 'D:', ...], their URLs are
drives://C: etc. But when the user opens them,
we want to go to file://C: instead.
DrivesFileSystem overrides
resolve(...) to achieve this.

The default implementation of this method normalizes the
given path syntactically and returns it as a URL. For
example, a/./b becomes
scheme://a/b, a//b becomes
scheme://a/b and a/b/.. becomes
scheme://a.

FileSystem.exists(path)

Returns whether the given path exists. The default
implementation relies on the fact that
is_dir(...)
raises FileNotFoundError if the given path does
not exist to return True or False.
You can override this method to use a more specialized
implementation.

FileSystem.touch(path)

Implement this method to support
touch(...) for your file system. This
is for instance used by the command
CreateAndEditFile (Shift+F4) from
the Core plugin.

FileSystem.mkdir(path)

Implement this method to support
mkdir(...) and
makedirs(...) for your file system.
If the given path already exists, raise a
FileExistsError. This method is for instance
used when you execute the CreateDirectory
command (F7) from the Core plugin.

FileSystem.delete(path)

FileSystem.copy(src_url, dst_url)

Implement this method to support
copy(...) for this file system. As for
that function, it's important that dst_url is
the final destination URL, not that of the parent directory.
Note that unlike most other functions in this class, this
method works with URLs such as scheme://a/b
instead of paths such as a/b. This lets you
support copying across file systems. For example, a "copy"
from zip://C:/archive.zip/member.txt to
file://C:/Temp/member.txt extracts the file. At
least one of src_url and dst_url
is guaranteed to be from the current file system. If your
file system does not support the given source or
destination, raise UnsupportedOperation.

When you invoke copy(...), fman first
forwards this request to the source file system. If this
raises UnsupportedOperation, fman then tries
the destination file system. For example: When you call
copy(file:// -> zip://), fman invokes
LocalFileSystem.copy(...), where
LocalFileSystem handles the
file:// scheme. This cannot copy to
zip://, so raises
UnsupportedOperation. fman then calls
ZipFileSystem.copy(...). This succeeds and the
file is extracted.

FileSystem.move(src_url, dst_url)

Implement this method to support
move(...) for this file system. This is
exactly analogous to
FileSystem.copy(...). Please
refer to its documentation for further details.

FileSystem.samefile(path1, path2)

Return True or False depending on
whether the two paths refer to the same file. The default
implementation simply checks if the two paths
resolve(...) to the same
URL. You can override this method if you want to use a more
specialized implementation.

FileSystem.cache

The Cache in which you can store file
attributes for faster retrieval. For an introduction, please
see the discussion below.

Caching (continued)

Most file systems would be unusably slow without caching.
Consider for example FTP: If fman had to make a round trip
to the server to query the attributes of each file in a
directory, then listing the folder's contents would take
ages. The solution is to ask the server "send me all files
and their attributes", then store and retrieve them later
when needed.

fman does its best to cache the values returned by custom
FileSystem implementations. But because the
flows of information differ so much across file systems, you
will most likely have to think a little about caching to
make your FileSystem usable. Fortunately, the
API makes this pretty easy.

The simplest case is when file attributes can only be
queried individually. A good example of this is the local
file system. To determine the size of C:\a.txt
and C:\b.txt, your computer needs to first look
at a.txt and then at b.txt. It
can't do both at the same time.

It uses @cached (described
below) to cache the result of Python's
getsize(...). This avoids excessive reads from
local disk.

A more complex (but common) situation is that you receive
file information alongside the list of directory contents.
This would be the case in the FTP example above. Under these
circumstances, you want to put the information into the
cache as you receive it. Here is an example of what this
could look like:

The server is assumed to be given in this
example. It returns file_info objects, which
have a name and an is_dir
property. In other words, the server gives us several pieces
of information when we list the contents of a directory.
We store them in
FileSystem.cache
before yielding the respective file names to the caller.
For the workings of .cache, please consult the
documentation of the Cache class.

Subtleties of caching in fman

There are a few subtle points when it comes to caching in
fman. First, fman will often empty "your" FileSystem's
caches, for instance when a directory pane is reloaded.
Don't use the cache to store information that you need
across pane reloads. A server connection for example is
something you most likely don't want to store in the cache.

The second issue pertains to
iterdir(...): fman
doesn't just cache the results of this function. It
also updates them when you call modifying functions such as
delete(...). Consider
this example:

However, things get interesting when you delete one of the
files, say File.txt:

File.txt has disappeared from the list of
files. But why? iterdir(...) above hasn't
changed and should still return it.

The reason is that when you implement
delete(...) (like we did above) and it doesn't
throw an error, then fman assumes that the deletion was
successful and removes the file from its cache without
consulting with your implementation of
iterdir(...) first. Only when the pane is
reloaded does File.txt appear again.

The learning from this is that iterdir(...) is
special when it comes to caching. Unlike other functions
such as is_dir(...), where you almost certainly
want to perform some caching, you most likely won't see much
benefit from caching the results of
iterdir(...).

Cache

This class lets you cache file attributes for improved
performance. You are not meant to use it directly. Instead,
you access it through the
.cache
attribute of the FileSystem class.

Cache.put(path, attr, value)

Place a file attribute in the cache. For example:
cache.put('C:/test.txt', 'is_dir', False). The
attribute can later be retrieved via
Cache.get(...) or
Cache.query(...).

Cache.get(path, attr)

Retrieve a value from the cache. Eg:
cache.get('C:/test.txt', 'is_dir'). If the
value is not stored in the cache, a KeyError is
raised.

Cache.query(path, attr, compute_value)

Retrieve a value from the cache. If it doesn't exist,
execute compute_value(), place its result in
the cache and then return it.

The important property of this method is that it is a
canonical operation. Consider the following example: fman
starts, and both the left and the right pane are at
C:\. It would be unnecessary for both panes to
load the contents of C:\. Instead, only the
first pane loads the contents. The caching mechanism ensures
that the results results are shared with the second pane.

In technical terms, consecutive calls to
Cache.query(...) with the same
path and attr block until the
initial call has completed.

Cache.clear(path)

Deletes the cached attributes for the given path and all
paths below it.

cached

You can use this decorator to annotate any
FileSystem method which takes a
path as its single parameter. The two methods
in the following code snippet are equivalent:

At the moment, the only way to enable a column for a file
system is to override
FileSystem.get_default_columns(...).
A consequence of this is that you can
(currently)
only define new columns for a
FileSystem you created yourself.
Here is the implementation of the
pizza:// file system. Note how it returns
'pizza.Yumminess' from its
get_default_columns(...):

The pizza. prefix assumes that the code for the
Yuminess column lies in
<your plugin>/pizza/__init__.py.

Columns often have to obtain values from the underlying file
system (or from its cache). This can be done with
query(...).

Column.display_name

By default, columns are displayed in fman under their class
name: In the example above, our
Yumminess(Column) was displayed in the
screenshot as "Yumminess". If we wanted it to appear as
"Taste" instead, we could set its display_name
as follows:

class Yumminess(Column):
display_name = 'Taste'
...

Column.get_str(url)

Return the text to be displayed in the column for the file
with the given URL.

Column.get_sort_value(url, is_ascending)

Return the value that should be used to compare files when
sorting by this column. You must always return a value, and
it must always be of the same type. That is, you can't
return 'foo' (a string) in one case and
3 (an integer) in another.

The is_ascending parameter indicates the order
in which the column is being sorted. Its purpose is best
illustrated by an example.

Say you want to define your own Name column,
Name2. A first version may look as follows:

This finally produces the expected result. Note how the
files are sorted in descending order, but the folders still
appear at the top:

(In case you want to look it up, ^ is Python's
exclusive or operator.)

Built-in columns

The Core plugin comes with several built-in columns. When
you implement a new FileSystem,
only the Name column is shown by
default. This section explains the steps required for also
enabling the Size and
Modified columns. It does not
cover implementing entirely new columns. If this is your
goal, please see the documentation of the
Column class instead.

Here is an example of a file system that implements all of
the built-in columns:

The size_bytes(...) method computes the value
for the Size column. In the
example, we for instance see that the "size" of
File.txt is 4 * 1024, which is
displayed in the screenshot as 4 KB.

Finally, the modified_datetime(...) method
gives the value for the
Modified column. It returns a
datetime object. In our example, this
is datetime(2017, 4, 15, 9, 36) which is
displayed as 15.04.2017. (The actual display on
your system depends on your locale. If you are in the US,
the month will likely be displayed first.)

In short, the steps required for supporting a built-in
column in your file system are:

If the given path does not have a size (eg. when it's a
directory), your size_bytes(...) can simply not
return a value (or equivalently
return None). If the given path does not
exist, you should raise a FileNotFoundError.
Here is an example:

core.Modified

This is the standard Modified column. It displays the
modified time of the given file (eg.
07.01.18 12:41). To support it, you need to
implement the modified_datetime(...) method
in your FileSystem subclass:

If the given path does not have a modified date, your
modified_datetime(...) can simply not return a
value (or equivalently return None).
If the given path does not exist, you should raise a
FileNotFoundError. Here is an example:

core.Name

This is the standard Name column. By default, it displays
the base name of the given file (eg. test.txt
for file://C:/test.txt). If you want it to
display a different value, you can override the
name(...) method in your
FileSystem subclass:

As an implementation detail that might be of educational
interest, all three column classes above use
query(...) to call your FileSystem.

Module fman.clipboard

This module contains functions that let you interact with
your Operating System's clipboard. A modern OS's clipboard
doesn't just contain a single value. Instead, it can contain
values for multiple formats. For example, when you copy an
image to the clipboard, then the "image" format may contain
the image's pixels, the "plain text" format may contain the
image's file name and the "path" format may contain the
image file's path. Depending on where you paste, the
appropriate format is used.

This module lets you manipulate the "plain text" and the
"path/URL" formats of the clipboard.

set_text(text)

Copies the given text into the clipboard.

get_text()

Returns the plain text in the clipboard.

copy_files(file_urls)

Copies the given files to the clipboard, so you can paste
them into fman or your OS's native file manager.

cut_files(file_urls)

Cuts the given files to the clipboard, so that when you
paste them into fman or your OS's native file manager, the
files are moved to the new destination. On macOS, this
function raises NotImplementedError. The reason
for this is that it is not technically possible on macOS to
place files on the clipboard in such a way that apps like
Finder understand that the files are meant to be moved and
not copied. Instead the choice of whether to copy or move is
made when you paste, by pressing either Cmd+V
or Cmd+Alt+V.

get_files()

Returns a list of URLs of the files currently on the
clipboard. To determine whether the files were cut or
copied, use files_were_cut().

files_were_cut()

On Windows and Linux, returns whether the files on the
clipboard are meant to be moved or copied when pasted. On
Mac, cutting files is not supported (see
cut_files(...)), so
False is always returned.