Introduction
Module fman
- show_alert(...)
- show_prompt(...)
- show_status_message(...)
- clear_status_message(...)
- show_file_open_dialog(...)
- show_quicksearch(...)
- QuicksearchItem
- load_json(...)
- save_json(...)
- get_application_commands(...)
- run_application_command(...)
- get_application_command_aliases(...)
- load_plugin(...)
- unload_plugin(...)
- DirectoryPaneCommand
- DirectoryPane
- ApplicationCommand
- Window
- DirectoryPaneListener
- Task
- submit_task(...)
Module fman.url
- splitscheme(...)
- as_url(...)
- as_human_readable(...)
- dirname(...)
- basename(...)
- join(...)
- relpath(...)
- normalize(...)
Module fman.fs
- Caching (Introduction)
- exists(...)
- is_dir(...)
- iterdir(...)
- touch(...)
- mkdir(...)
- makedirs(...)
- move(...)
- prepare_move(...)
- copy(...)
- prepare_copy(...)
- move_to_trash(...)
- prepare_trash(...)
- delete(...)
- prepare_delete(...)
- samefile(...)
- query(...)
- resolve(...)
- FileSystem
- notify_file_added(...)
- notify_file_changed(...)
- notify_file_removed(...)
- Caching (continued)
- Cache
- cached
- Column
- Built-in columns
Module fman.clipboard
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
identifies one of its members.
The module fman.url exposes several functions that make working with URLs easier:
-
as_human_readable(...)
converts
file://C:/test.txt
intoC:\test.txt
. -
as_url(...)
does the opposite: it turns
C:\test.txt
intofile://C:/test.txt
. -
splitscheme(...) splits
scheme://a/b
intoscheme://
anda/b
.
If your plugin only supports "normal" files, then you should
check that the URLs you are working with are actually
file://
URLs. The idiomatic way of doing this
is as follows:
from fman import DirectoryPaneCommand from fman.url import splitscheme, as_human_readable class MyCommand(DirectoryPaneCommand): def __call__(self): url = self.pane.get_path() # Eg. 'file://C:/Windows' scheme, path = splitscheme(url) # Eg. 'file://', 'C:/Windows' if scheme != 'file://': show_alert('Not supported.') return local_path = as_human_readable(url) # Eg. 'C:\Windows' ...
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.
Example:
from fman import ApplicationCommand, show_alert, YES, NO, YES_TO_ALL, \ NO_TO_ALL, ABORT class AlertExample(ApplicationCommand): def __call__(self): choice = show_alert( 'Do you want to continue?', buttons=YES | NO | YES_TO_ALL | NO_TO_ALL | ABORT, default_button=YES ) if choice == YES: ... elif choice == NO: ... ...
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.
The usual way of handling the result is as follows:
from fman import ApplicationCommand, show_prompt class ShowPrompt(ApplicationCommand): def __call__(self): text, ok = show_prompt('Please enter a value') if text and ok: show_alert('You entered ' + text) else: show_alert("You cancelled or didn't enter any text")
show_status_message(text, timeout_secs=None)
Shows the given text in fman's status bar:
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)
Shows a Quicksearch dialog. The classic example of this is GoTo:
- ~/.config/fman
- ~/dev/fman
- ~/Dropbox/fman
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:
from fman import ApplicationCommand, show_quicksearch, QuicksearchItem class Quicksearch(ApplicationCommand): def __call__(self): result = show_quicksearch(self._get_items) if result: query, value = result show_alert('You typed %r and selected %r.' % (query, value)) else: show_alert('You cancelled the dialog.') def _get_items(self, query): for item in ['Some item', 'Another item', 'And another']: try: index = item.lower().index(query) except ValueError as not_found: continue else: # The characters that should be highlighted: highlight = range(index, index + len(query)) yield QuicksearchItem(item, highlight=highlight)
QuicksearchItem(value, title=value, highlight=None, hint='', description='')
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:
from fman import ApplicationCommand, show_prompt, load_json class Prompt(ApplicationCommand): def __call__(self): settings = load_json('My Settings.json', default={}, save_on_quit=True) value = settings.get('value', '') new_value, do_save = show_prompt('Please enter a value:', value) if do_save: settings['value'] = new_value
(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_commands()
Returns a set of names of available ApplicationCommands. You can pass these names to run_application_command(...) to actually execute the respective command. The canonical use case for this function is the Command Palette, which displays a list of commands and lets you execute them.
Note that there are also other kinds of commands, namely DirectoryPaneCommands. If you want to get a list of all commands available in fman, you have to combine the results of this function with those of DirectoryPane.get_commands(...).
run_application_command(name, args=None)
Runs the
ApplicationCommand with
the given name. The optional parameter args
,
if given, must be a dictionary of arguments to pass to the
command's __call__
method. For instance:
run_application_command('install_plugin', args={'github_repo': 'mherrmann/ArrowNavigation'})
Note that there are not just ApplicationCommands but also DirectoryPaneCommands. To execute a DirectoryPaneCommand, use DirectoryPane.run_command(...) instead.
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.
Note that this function is not meant for DirectoryPaneCommands. To get the aliases of a DirectoryPaneCommand, use DirectoryPane.get_command_aliases(...) instead.
load_plugin(plugin_path)
Loads the plugin in the given directory. This is currently used by fman's command for installing plugins.
unload_plugin(plugin_path)
Unloads the plugin with the given directory path. If the
plugin was not loaded, a ValueError
is raised.
This function is currently used by fman's
RemovePlugin
and ReloadPlugins
commands.
DirectoryPaneCommand
Lets you execute arbitrary actions in the context of the current directory pane. For example, the following command shows an alert with the URL of the current folder:
from fman import DirectoryPaneCommand, show_alert class ShowLocation(DirectoryPaneCommand): def __call__(self): show_alert(self.pane.get_path())
DirectoryPaneCommands can be bound to keyboard shortcuts. Additionally, every DirectoryPaneCommand you define automatically appears in (and can be executed from) 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:
class MoveCursorDown(DirectoryPaneCommand): def __call__(self, toggle_selection=False): ...
Its entries in Key Bindings.json look as follows:
{ "keys": ["Down"], "command": "move_cursor_down" }, { "keys": ["Shift+Down"], "command": "move_cursor_down", "args": {"toggle_selection": true} }
You can also fill in the parameters when you call DirectoryPane.run_command(...).
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:
class MyCommand(DirectoryPaneCommand): def __call__(self): chosen_files = self.get_chosen_files() if not chosen_files: show_alert('No file is selected!') return # Perform your action here...
DirectoryPaneCommand.is_visible()
You can override this method to prevent your command from appearing in the Command Palette and in context menus. For example, to only show your command when the user is viewing the contents of a Zip archive:
from fman.url import splitscheme class MyCommand(DirectoryPaneCommand): def is_visible(self): return splitscheme(self.pane.get_path())[0] == 'zip://' ...
The default implementation simply returns True
(meaning that commands are always visible by default).
DirectoryPaneCommand.aliases
You can set this property to a list of names under which your command should appear in the Command Palette. For example, the built-in Reload command specifies the following aliases:
class Reload(DirectoryPaneCommand): aliases = ('Reload', 'Refresh') ...
If you don't set this property, a single alias is
automatically generated for your command
(MyCommand
-> My command
).
For more information, see
DirectoryPane.get_command_aliases(...)
DirectoryPaneCommand.pane
The DirectoryPane in which this
command is executed. You usually access it via
self.pane
.
DirectoryPane
Represents a
directory pane. You are not meant to use this class
directly. Instead, you usually use it through the
.pane
attribute of
DirectoryPaneCommand.
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, onerror=go to existing parent directory)
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
:
from fman import DirectoryPaneCommand from fman.url import as_url class MyCommand(DirectoryPaneCommand): def __call__(self): def callback(): self.pane.place_cursor_at(as_url(r'C:\Windows\System32'))) self.pane.set_path(as_url(r'C:\Windows'), callback=callback)
The optional parameter onerror
lets you specify
what happens when an error occurs while trying to navigate
to the given directory. The default implementation looks as
follows:
fman fman.url import dirname def onerror(e, url): if isinstance(e, FileNotFoundError): return dirname(url) raise
In words: When a FileNotFoundError
occurs, fman
attempts to open the parent directory instead. This for
instance happens when the original directory was deleted.
All other errors are re-raised by the raise
statement in the above code.
If onerror
is given, fman keeps calling it
until it raises an error or until it returns the same URL
twice. For example: Suppose we call
set_path(C:/X/Y)
when C:/X
does
not exist. Then:
-
onerror
is called with aFileNotFoundError
and pathC:/X/Y
. It returnsC:/X
. -
fman gets another
FileNotFoundError
while navigating toC:/X
. So it callsonerror
withC:/X
. This returnsC:
. -
fman finally succeeds and the path is changed to
C:
.
To disable the default implementation above, pass
onerror=None
. This re-raises all errors and
does not go to the first existing parent directory.
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.select(file_urls)
Select the given files. Note that file_urls
must be a list, even if you want to select a single file.
That is, you need to write
self.pane.select([file])
, not
...select(file)
.
If any of the given files do not exist in the current pane, that is ignored.
DirectoryPane.deselect(file_urls)
Deselect the given files. This works analogously to DirectoryPane.select(...). Please see its documentation.
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.
Note that, in addition to DirectoryPaneCommands, there are also ApplicationCommands. To get a list of all commands available in fman, you will likely have to combine the results of this function with those of get_application_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:
self.pane.run_command('move_cursor_down', args={'toggle_selection': True})
Note that this function does not work for ApplicationCommands. To run an ApplicationCommand, use run_application_command(...) instead.
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.
Note that this method only works for DirectoryPaneCommands. To get the aliases of an ApplicationCommand, use get_application_command_aliases(...) instead.
DirectoryPane.is_command_visible(command_name)
Returns whether the given command's
is_visible(...)
returns True for this pane. This is used by the
Command Palette
to filter out commands that may not be applicable in the
current context.
DirectoryPane.focus()
Give the keyboard focus to this pane.
DirectoryPane.edit_name(file_url, selection_start=0, selection_end=None)
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).
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, ascending=True)
Sets the sort order of files and directories in this pane.
The column
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
to 'core.Size'
,
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, 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:
from fman import DirectoryPaneCommand class MirrorInOtherPane(DirectoryPaneCommand): def __call__(self): panes = self.pane.window.get_panes() this_pane = panes.index(self.pane) other_pane = panes[(this_pane + 1) % len(panes)] other_pane.set_path(self.pane.get_path())
ApplicationCommand
You can extend this class to create "global" commands that do not operate in a specific pane / directory. For example:
from fman import ApplicationCommand, show_alert, FMAN_VERSION class ShowVersion(ApplicationCommand): def __call__(self): show_alert('fman version is ' + FMAN_VERSION)
ApplicationCommands are automatically displayed in the Command Palette. You can also bind them to keyboard shortcuts.
To implement a command that executes in the context of a directory pane, use DirectoryPaneCommand instead.
ApplicationCommand.__call__(**kwargs)
Implement this method to execute the command. Similarly to DirectoryPaneCommand.__call__, you are free to define extra parameters.
ApplicationCommand.aliases
You can set this property to a list of names under which your command should appear in the Command Palette. For example, the built-in Quit command specifies the following aliases:
class Quit(ApplicationCommand): aliases = ('Quit', 'Exit') ...
If you don't set this property, a single alias is
automatically generated for your command
(MyCommand
-> My command
).
For more information, see
get_application_command_aliases(...)
ApplicationCommand.window
The fman Window in which this command lives.
Window
This class is exposed as the .window
property
on the DirectoryPane and
ApplicationCommand
classes. It is only meant to be accessed through that
property.
Window.get_panes()
Returns a list of the directory panes in this window. Please See DirectoryPane.window for an example where it is useful.
Window.minimize()
Minimize the window.
DirectoryPaneListener
Extend this class to be notified of various events in a directory pane. For example, the following listener shows an alert when a file is double-clicked:
from fman import DirectoryPaneListener, show_alert class DoubleclickListener(DirectoryPaneListener): def on_doubleclicked(self, file_url): show_alert('You doubleclicked ' + file_url)
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:
class ArchiveOpenListener(DirectoryPaneListener): def on_command(self, command_name, args): if command_name in ('open_file', 'open_directory'): url = args['url'] scheme, path = splitscheme(url) if scheme == 'file://' and path.endswith('.zip'): new_args = dict(args) new_args['url'] = 'zip://' + path return 'open_directory', new_args
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.before_location_change(url, sort_column='', ascending=True)
This method is called before a pane's location changes.
You can override it to redirect the pane to a different
location, or change the sort order. To do this, return a
three-tuple url, sort_column, ascending
. For
instance, here is how the Core plugin uses this mechanism
to remember the sort settings for your various folders:
from fman import DirectoryPaneListener, load_json class RememberSortSettings(DirectoryPaneListener): def before_location_change(self, url, sort_column='', ascending=True): settings = load_json('Sort Settings.json', default={}) try: data = settings[url] except KeyError: return remembered_col, remembered_asc = data['column'], data['is_ascending'] return url, remembered_col, remembered_asc
To leave the URL and sort order unchanged, simply
return
nothing. In the example above, this is
done when a KeyError
signifies that there is no
saved sort order for the given URL.
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(...).
DirectoryPaneListener.on_files_dropped(file_urls, dest_dir, is_copy_not_move)
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).
DirectoryPaneListener.on_location_bar_clicked()
Called when the user clicks the location bar (= the address bar) in this directory pane.
Task(title, size=0, fn=None, args=(), kwargs=None)
Tasks let you display progress feedback for long running operations. A common example is copying files:
Here is a "dummy" Task that emulates this:
from fman import Task from time import sleep class CopyFile(Task): def __init__(self): super().__init__('Copying some file...') def __call__(self): self.set_text('Calculating size...') # Let's say calculating the file size takes 3 seconds: sleep(3) # Our fake file's size is 10 MB: self.set_size(10) self.set_text('Copying...') num_mb_written = 0 while num_mb_written < self.get_size(): self.check_canceled() # Pretend writing 1 MB takes 1 second: sleep(1) num_mb_written += 1 self.set_progress(num_mb_written)
When you execute this Task, you get the following dialog:
Then, after three seconds, the dialog changes to display the running progress:
After another 10 seconds, the dialog closes. Or you can press the Cancel button to close it earlier.
The above example extends Task
and overrides
__call__(...)
to implement the operation.
Alternatively, you can also use the
fn
, args
and kwargs
parameters. For example:
from fman import Task from fman.fs import FileSystem class MyFileSystem(FileSystem): ... def delete(self, path): ... def prepare_delete(self, path): return [Task( 'Deleting ' + path, fn=self.delete, args=(path,) )]
Task.__call__()
Override this method to perform the steps required for executing this Task. For an example, see above. If you find yourself wanting to use fman.show_alert(...), please use Task.show_alert(...) instead.
Task.set_size(size)
Set the size of this Task to the given integer. It can be
understood as the total number of steps required to complete
this task. When fman computes the completion percentage and
draws the progress bar, the size serves as the upper bound
for the progress. The default value 0
means
that this Task does not have a definite size. In this case,
the progress bar shows a general "loading" animation but no
specific percentage:
Task.get_size()
Return this Task's size; See Task.set_size(...).
Task.set_progress(progress)
Set the progress made so far in the execution of this Task to the given integer. Unlike the Task's size, which gives the total number of steps, the progress gives the number of completed steps.
Task.get_progress()
Return the progress that has been made towards completing this Task; See Task.set_progress(...).
Task.set_text(text)
Set the text displayed above the progress bar to the given string. Note that this is different from the title, which is displayed in the progress dialog's window title bar.
Task.check_canceled()
Call this function in your implementation of
__call__(...) to give fman a
chance to abort your Task when the user presses the Cancel
button in the progress dialog. You should especially do this
in potentially time intensive for
and
while
loops. For example:
from fman import Task class MyTask(Task): def __call__(self): for i in range(999999999): self.check_canceled() # do some processing... while True: self.check_canceled() # do some more processing...
Technically, check_canceled()
raises
Task.Canceled
if
the Task was aborted. Leveraging Python's exception
mechanism in this way has the advantage of cleanly
suspending the Task's execution. Further, it leads to easy
propagation of Task cancellation across methods and
subtasks.
Task.run(subtask)
Run the given Task as a subtask of this Task. Changes to the subtask's text and progress are reflected in this Task: Changing the subtask's text sets this Task's text as well. And any increase in the subtask's progress is also applied to this Task. (Note how this assumes that the subtask's size is already included in this Task's.)
Task.show_alert(*args, **kwargs)
Show an alert pertaining to the execution of this Task. The parameters and return value are the same as for fman.show_alert(...).
The main motivation for having a separate
Task.show_alert(...)
is to let fman coordinate
progress dialogs and alert windows. But there is also a more
semantic reason: Future versions of fman might display tasks
in a separate GUI. This GUI may have special rules for
handling alerts; In particular, it may want to display them
in the background. Using a dedicated method for showing
alerts for Tasks leaves this road open.
Task.get_title()
Return the title of this Task, as passed to the constructor and displayed in the progress dialog's window title bar.
Task.Canceled
This exception is raised by check_canceled() when the user cancels the Task in the progress dialog. You may catch it in your implementation of __call__() to perform necessary cleanup steps. Typically however, you will not need to handle it.
The exception extends Python's
KeyboardInterrupt
, because it is similar in
spirit. Note that this implies that you can't handle it with
except Exception:
. For details why, see
this post.
submit_task(task)
Use this function to submit a Task for execution by fman. If the task takes more than one second to complete, this function opens a progress dialog for it. The function also handles the cancellation of tasks by catching Task.Canceled. Upon completion of the task (successful or otherwise), this function closes the progress dialog again.
For example: To run the CopyFile
Task from
above, add the following code and
save it as a plugin.
Then, use the
Command Palette
to "Reload plugins", then "Run example".
from fman import ApplicationCommand, submit_task class RunExample(ApplicationCommand): def __call__(self): submit_task(CopyFile())
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:
from fman import DirectoryPaneCommand, show_prompt from fman.fs import mkdir from fman.url import join class CreateDirectory(DirectoryPaneCommand): def __call__(self): name, ok = show_prompt("New folder (directory)") if ok and name: dir_url = join(self.pane.get_path(), name) mkdir(dir_url)
relpath(dst, src)
Computes the relative path from the given src
URL to the given dst
URL. For example:
relpath('file:///a/b', 'file:///a') # gives 'b' relpath('file:///dst', 'file:///dst/sub') # gives '..'
This only works if dst
and src
have the same scheme. Otherwise,
a ValueError
is raised.
normalize(url)
Resolve the given URL syntactically. For example,
scheme://a/./b
becomes
scheme://a/b
, scheme://a//b
becomes scheme://a/b
and
scheme://a/b/..
becomes
scheme://a
.
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,
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. Note
that 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:
from fman import DirectoryPaneCommand, show_alert from fman.fs import iterdir class ListContents(DirectoryPaneCommand): def __call__(self): for file_name in iterdir(self.pane.get_path()): show_alert(file_name)
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.
May raise NotImplementedError
if the target
file system does not support this operation.
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 the target file
system does not support this operation, a
NotImplementedError
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. If the target file system does not support
this operation, a NotImplementedError
is
raised.
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')
.
The destination's parent directory
(file://C:/dir
) must exist. If neither the
source nor the destination file system supports the
operation,
io.UnsupportedOperation
or NotImplementedError
is raised.
prepare_move(src_url, dst_url)
This function is analogous to prepare_copy(...). Please consult its documentation instead.
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')
.
The destination's parent directory
(file://C:/dir
) must exist. If neither the
source nor the destination file system supports the
operation,
io.UnsupportedOperation
or NotImplementedError
is raised.
prepare_copy(src_url, dst_url)
copy(...)
immediately
copies the given file or directory without a progress
dialog. It completes when the copy has been performed. If
you do want to show progress feedback as the file is being
copied, use prepare_copy(...)
instead. Like
FileSystem.prepare_copy(...),
it returns an iterable of Task
objects. Here is how you would use it:
from fman import DirectoryPaneCommand, Task, submit_task from fman.fs import prepare_copy from fman.url import basename, join class MyCopy(DirectoryPaneCommand): def __call__(self): src_url = self.pane.get_file_under_cursor() dst_url = join(self._get_opposite_pane().get_path(), basename(src_url)) submit_task(_Copy(src_url, dst_url)) def _get_opposite_pane(self): panes = self.pane.window.get_panes() this_pane = panes.index(self.pane) return panes[(this_pane + 1) % len(panes)] class _Copy(Task): def __init__(self, src_url, dst_url): super().__init__('Copying ' + basename(src_url)) self._src_url = src_url self._dst_url = dst_url def __call__(self): tasks = list(prepare_copy(self._src_url, self._dst_url)) self.set_size(sum(task.get_size() for task in tasks)) for task in tasks: self.run(task)
When you run the above command MyCopy
via the
Command Palette, fman will copy the file under the cursor to
the opposite pane, showing a progress dialog as it proceeds:
(Note that the file has to be pretty large, or else the operation completes so quickly that the progress dialog is never shown.)
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, NotImplementedError
is raised.
To permanently delete a file or directory, use delete(...) instead.
prepare_trash(url)
This function is the analogue of prepare_copy(...) for move_to_trash(...). Please consult its documentation instead.
delete(url)
Permanently delete the given file or directory. If the file
system identified by url
does not support this,
NotImplementedError
is raised.
prepare_delete(url)
This function is the analogue of prepare_copy(...) for delete(...). Please consult its documentation instead.
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)
:
class ZipFileSystem(FileSystem): ... def packed_size(self, path): return 1234 # The packed size of the given file ...
Then, to display this value, you could create a new
Column that uses
query(...)
to obtain it:
class PackedSize(Column): def get_str(self, url): return str(query(url, 'packed_size'))
query(...)
internally uses
Cache.query(...). That is,
query('myfs://directory/file.txt', 'myprop')
is
equivalent to:
compute_value = lambda: file_system.myprop('directory/file.txt') file_system.cache.query('directory/file.txt', 'myprop', compute_value)
where file_system
is the
FileSystem for the
myfs://
scheme.
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.
resolve(url)
Return the "true" location of the given URL. If it does not
exist, raise a FileNotFoundError
For example:
The drives://
file system on windows shows your
local disks. To achieve this, its
iterdir(...) method
returns 'C:'
, 'D:'
etc. But this
means that their URLs are drives://C:
,
drives://D:
etc. When you open one of them,
what we want is to navigate to file://C:
instead. That's what resolve(...)
is for: In
the above example, it turns drives://C:
into
file://C:
.
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. If the folder with the given
path
does not exist, raise
FileNotFoundError
.
Here is a complete example implementation:
from fman.fs import FileSystem class Example(FileSystem): scheme = 'example://' def iterdir(self, path): return ['File.txt', 'Image.jpg']
When you
navigate to example://
,
you see:
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:
class Example(FileSystem): ... def get_default_columns(self, path): return 'core.Name', 'core.Size', 'core.Modified' ...
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 first calls
.exists(path)
.
If this returns False
, a
FileNotFoundError
is raised. Otherwise,
normalize(...)
is used
to normalize the given path.
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.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.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. If the file was successfully created, you
should call
notify_file_added(path)
.
This updates fman's caches and makes the file appear
immediately (instead of after the next reload).
FileSystem.mkdir(path)
Implement this method to support
mkdir(...) and
makedirs(...) for your file system.
If the given path already exists, raise a
FileExistsError
. If the parent directory of the
given path does not exist, raise a
FileNotFoundError
. If the directory was
successfully created, you should call
notify_file_added(path)
.
This updates fman's caches and makes the directory appear
immediately (instead of after the next reload).
This method is for instance used when you execute the
CreateDirectory
command (F7
) from
the Core plugin.
FileSystem.makedirs(path, exist_ok=True)
Standard implementation of makedirs(...) using FileSystem.mkdir(...). You can override this method if you want to use a more specialized implementation.
FileSystem.move_to_trash(path)
Implement this method to support move_to_trash(...) for your file system. To permanently delete files, implement FileSystem.delete(...) instead.
If the operation fails, raise OSError
. You can
set a description what went wrong via the second parameter
to OSError(...)
. For example:
raise OSError(errno.EACCES, "File is in use")
This is then displayed to the user:
If the operation was successful, call
notify_file_removed(path)
.
This updates fman's caches and makes the file disappear
immediately (instead of after the next reload).
Some of fman's commands show a progress dialog for file
operations. If you want to give progress feedback as the
file is being deleted, implement
FileSystem.prepare_trash(...)
.
FileSystem.delete(path)
Implement this method to support delete(...) for your file system.
If the operation fails, raise OSError
. You can
set a description what went wrong via the second parameter
to OSError(...)
. For example:
raise OSError(errno.EACCES, "File is in use")
This is then displayed to the user:
If the operation was successful, call
notify_file_removed(path)
.
This updates fman's caches and makes the file disappear
immediately (instead of after the next reload).
Some of fman's commands show a progress dialog for file
operations. If you want to give progress feedback as the
file is being deleted, implement
FileSystem.prepare_delete(...)
.
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 instead of paths (that is,
scheme://a/b
instead of 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
io.UnsupportedOperation
.
A typical implementation of this method looks as follows:
from fman.url import splitscheme class ZipFileSystem(FileSystem): scheme = 'zip://' def copy(self, src_url, dst_url): src_scheme, src_path = splitscheme(src_url) dst_scheme, dst_path = splitscheme(dst_url) if src_scheme == 'zip://' and dst_scheme == 'file://': # extract the file... elif src_scheme == 'file://' and dst_scheme == 'zip://': # pack the file to the archive... elif src_scheme == dst_scheme: # copy from one archive to another... else: raise UnsupportedOperation() ...
When you invoke copy(...), fman first
forwards this request to the source file system. If this
raises
io.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.
fman only expects your implementation of
copy(...)
to support the following cases:
-
src_url
is a file.dst_url
may exist and be a file. In that case, it is overwritten. -
src_url
is a directory.dst_url
does not exist.
In particular, fman does not require you to handle the case where both the source and the destination are (existing) directories. In this case, the built-in Copy command for example "merges" the two folders by asking the user "do you want to overwrite X?" as it goes along.
Your implementation should call
notify_file_added(...)
for every file/directory it created and
notify_file_changed(path)
for every file it changed (/overwrote). This updates fman's
caches and ensures that your changes are visible immediately
(instead of only after the next reload).
Some of fman's commands show a progress dialog for file
operations. If you want to give detailed progress feedback
for your FileSystem's copy(...)
, you
need to implement
FileSystem.prepare_copy(...)
.
FileSystem.move(src_url, dst_url)
Implement this method to support
move(...) for this file system. This is
analogous to
FileSystem.copy(...). Please
refer to its documentation for further details. The only
difference is that you should also call
notify_file_removed(...)
for any files or directories removed during the move.
FileSystem.prepare_delete(path),
FileSystem.prepare_trash(path),
FileSystem.prepare_copy(src_url, dst_url),
FileSystem.prepare_move(src_url, dst_url)
Consider what's required to display progress feedback when copying a directory:
To display meaningful percentages, we need to calculate the size of all files in the directory. Then, we need to update the progress as each file is being copied.
This is achieved via the prepare_*
functions
listed above. They return iterables of
Task
objects, which encode
the steps required for executing the respective operation.
In the copy example, prepare_copy(...)
returns
one Task
object per file in the directory. Each
task has its size
set to the size of the file.
Further, it contains the logic for copying the respective
file, and for updating the progress indicator as it goes
along.
The default implementations check if your FileSystem
implements the operation to be prepared. (So eg. the default
prepare_copy
checks if you implemented
copy
.) If not, they raise
NotImplementedError
. Otherwise, they return a
single Task with size=0
that calls the
respective operation. In the case of copy:
def prepare_copy(self, src_url, dst_url): if ...: # Check if this FS doesn't implement copy(...) raise NotImplementedError() else: return [Task( 'Copying ' + basename(src_url), size=0, target=self.copy, args=(src_url, dst_url) )]
Because the Task's size is 0
, this gives a
progress dialog without a percentage indicator:
The main reason why you would want to implement
prepare_*
is to give more detailed progress
information than the above.
FileSystem.notify_file_added(path)
Notify fman that the file or directory with the given path now exists in this file system. This lets fman update its caches (and display the new file) immediately instead of having to wait for the next reload. You for instance call this method from your implementation of mkdir(...). To notify fman that a file was added to a different file system, use fman.fs.notify_file_added(...) instead.
FileSystem.notify_file_changed(path)
Notify fman that the file or directory with the given path in this file system was changed. In the case of a directory, this includes that its list of files changed. This method lets fman update its caches immediately instead of having to wait for the next reload. You may for instance call this from your implementation of touch(...) when the file's modification date was changed. To notify fman that a file from a different file system was changed, use fman.fs.notify_file_changed(...) instead.
FileSystem.notify_file_removed(path)
Notify fman that the file with the given path no longer exists in this file system. This lets fman update its caches (and remove the file) immediately instead of having to wait for the next reload. You for instance call this method from your implementation of delete(...). To notify fman that a file from a different file system was removed, use fman.fs.notify_file_removed(...).
FileSystem.cache
The Cache in which you can store file attributes for faster retrieval. For an introduction, please see the discussion below.
notify_file_added(url)
Notify fman that the file or directory with the given URL now exists. Unlike FileSystem.notify_file_added(...), this function takes a URL instead of a path as parameter. Otherwise, the semantics are the same.
notify_file_changed(url)
Notify fman that the file or directory with the given URL was changed. Unlike FileSystem.notify_file_changed(...), this function takes a URL instead of a path as parameter. Otherwise, the semantics are the same.
notify_file_removed(url)
Notify fman that the file or directory with the given URL was removed. Unlike FileSystem.notify_file_removed(...), this function takes a URL instead of a path as parameter. Otherwise, the semantics are the same.
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.
Here's what an implementation could look like:
from fman.fs import FileSystem, cached from os.path import getsize class LocalFileSystem(FileSystem): scheme = 'file://' ... @cached def size_bytes(self, path): return getsize(path) ...
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:
class FTP(FileSystem): ... def iterdir(self, path): for file_info in server.list_contents(path): file_path = path + '/' file_info.name self.cache.put(file_path, 'is_dir', file_info.is_dir) yield file_info.name def is_dir(self, path): return self.cache.query(path, 'is_dir', server.is_dir) ...
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 at once 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:
from fman.fs import FileSystem class Example(FileSystem): scheme = 'example://' def iterdir(self, path): yield 'File.txt' yield 'Image.jpg' def delete(self, path): self.notify_file_removed(path)
As in previous examples, this gives you:
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 delete(...)
above calls
notify_file_removed(...)
, then fman's caches
are updated and the file is removed. 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 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:
from fman.fs import FileSystem, cached from os.path import getsize class LocalFileSystem(FileSystem): @cached def size_bytes(self, path): return getsize(path) def size_bytes_2(self, path): compute_size = lambda: getsize(path) return self.cache.query( path, 'size_bytes_2', compute_size ) ...
Column
Extend this class to define a new column. (If you only want to enable one of the built-in columns Size, Modified or Name for your custom FileSystem, please see here.)
Say you have a pizza://
file system and want
to define a new column Yumminess for it:
To do this, you can create a new subclass of
Column
as follows:
from fman.fs import Column from fman.url import splitscheme class Yumminess(Column): def get_str(self, url): if url == 'pizza://Margherita': return '2' if url == 'pizza://Quattro Stagioni': return '9'
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(...)
:
from fman.fs import FileSystem class PizzaFS(FileSystem): scheme = 'pizza://' def get_default_columns(self, path): return 'core.Name', 'pizza.Yumminess' def iterdir(self, path): return ['Margherita', 'Quattro Stagioni']
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:
from fman.fs import Column class Name2(Column): def get_str(self, url): # Return everything after the last slash: return url[url.rindex('/')+1 : ] def get_sort_value(self, url, is_ascending): # Sort by the displayed name, case insensitively: return self.get_str(url).lower()
This works well, except that files are interspersed with directories:
To fix this, you decide to take is_dir(...)
into account as well:
from fman.fs import Column, is_dir class Name2(Column): ... as before def get_sort_value(self, url, is_ascending): return not is_dir(url), self.get_str(url).lower()
This looks good at first:
However, when you reverse the sort order, the directories appear at the bottom:
The solution to this is to take is_ascending
into account as well:
def get_sort_value(self, url, is_ascending): return is_ascending ^ is_dir(url), self.get_str(url).lower()
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:
from datetime import datetime from fman.fs import FileSystem class Example(FileSystem): scheme = 'example://' def iterdir(self, path): return ['File.txt', 'Image.jpg'] def get_default_columns(self, path): return 'core.Name', 'core.Size', 'core.Modified' def size_bytes(self, path): if path == 'File.txt': return 4 * 1024 elif path == 'Image.jpg': return 1300 * 1024 def modified_datetime(self, path): if path == 'File.txt': return datetime(2018, 2, 1, 12, 7) elif path == 'Image.jpg': return datetime(2017, 4, 15, 9, 36)
When you navigate to this file system, you get:
You see in the code that the file system overrides get_default_columns(...). This tells fman which columns should be shown.
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:
- Return the column's qualified name from get_default_columns(...).
-
Implement
size_bytes(...)
and / ormodified_datetime(...)
.
core.Size
This is the standard Size column. It displays the size
of the given file (eg. 388 MB
). To support it,
you need to implement the
size_bytes(...)
method in your
FileSystem
subclass:
class MyFileSystem(FileSystem): ... def size_bytes(self, path): # 388 MB: return 388 * 1024 * 1024 ...
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:
def size_bytes(self, path): if path == 'File.txt': return 1234 elif path == 'Directory': return None else: raise FileNotFoundError(path)
As a final note, you need to return
'core.Size'
from
get_default_columns(...)
to actually enable the Size column.
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:
from datetime import datetime class MyFileSystem(FileSystem): ... def modified_datetime(self, path): # January 7, 2018 12:41 return datetime(2018, 1, 7, 12, 41) ...
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:
def modified_datetime(self, path): if path == 'File.txt': return datetime(2018, 2, 1, 12, 49) elif path == 'Directory': return None else: raise FileNotFoundError(path)
As a final note, you need to return
'core.Modified'
from
get_default_columns(...)
to actually enable the Modified column.
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:
class MyFileSystem(FileSystem): ... def name(self, path): # Return the name you want displayed: return 'Some file.txt' ...
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.
clear()
Clears all contents of the clipboard.