diff options
author | hut <hut@lavabit.com> | 2010-05-14 17:27:04 +0200 |
---|---|---|
committer | hut <hut@lavabit.com> | 2010-05-14 17:27:04 +0200 |
commit | 420d3d1af8d2991ae6a8cbe061adaab878c4b3ff (patch) | |
tree | ad1cdc33cc12984c141618439b646424d8a76c25 /ranger | |
parent | 792d6b23711c428a4a2b98537f44e2fbaed97fdd (diff) | |
parent | 2f3326a41430364cb34a227d311691cada605327 (diff) | |
download | ranger-420d3d1af8d2991ae6a8cbe061adaab878c4b3ff.tar.gz |
Merge branch 'master' into cp
Conflicts: ranger/__main__.py
Diffstat (limited to 'ranger')
-rw-r--r-- | ranger/__main__.py | 75 | ||||
-rw-r--r-- | ranger/api/commands.py | 9 | ||||
-rw-r--r-- | ranger/core/environment.py | 8 | ||||
-rw-r--r-- | ranger/core/fm.py | 2 | ||||
-rw-r--r-- | ranger/defaults/commands.py | 26 | ||||
-rw-r--r-- | ranger/defaults/keys.py | 33 | ||||
-rw-r--r-- | ranger/defaults/options.py | 2 | ||||
-rw-r--r-- | ranger/ext/accumulator.py | 1 | ||||
-rw-r--r-- | ranger/ext/human_readable.py | 2 | ||||
-rw-r--r-- | ranger/ext/spawn.py | 27 | ||||
-rw-r--r-- | ranger/fsobject/__init__.py | 11 | ||||
-rw-r--r-- | ranger/fsobject/directory.py | 83 | ||||
-rw-r--r-- | ranger/fsobject/file.py | 8 | ||||
-rw-r--r-- | ranger/fsobject/fsobject.py | 141 | ||||
-rw-r--r-- | ranger/gui/mouse_event.py | 9 | ||||
-rw-r--r-- | ranger/gui/ui.py | 4 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 10 | ||||
-rw-r--r-- | ranger/gui/widgets/browserview.py | 26 | ||||
-rw-r--r-- | ranger/gui/widgets/console.py | 22 | ||||
-rw-r--r-- | ranger/help/__init__.py | 3 | ||||
-rw-r--r-- | ranger/help/console.py | 31 | ||||
-rw-r--r-- | ranger/help/index.py | 1 | ||||
-rw-r--r-- | ranger/help/invocation.py | 106 | ||||
-rw-r--r-- | ranger/help/movement.py | 37 | ||||
-rw-r--r-- | ranger/help/starting.py | 2 | ||||
-rw-r--r-- | ranger/shared/mimetype.py | 8 |
26 files changed, 475 insertions, 212 deletions
diff --git a/ranger/__main__.py b/ranger/__main__.py index d25065a1..6d574693 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -16,29 +16,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# Most import statements in this module are inside the functions. +# This enables more convenient exception handling in ranger.py +# (ImportError will imply that this module can't be found) +# convenient exception handling in ranger.py (ImportError) + import os import sys -import ranger - -from optparse import OptionParser, SUPPRESS_HELP -from ranger.ext.openstruct import OpenStruct -from ranger import __version__, USAGE, DEFAULT_CONFDIR -import ranger.api.commands - -from signal import signal, SIGINT -from locale import getdefaultlocale, setlocale, LC_ALL - -from ranger.ext import curses_interrupt_handler -from ranger.core.runner import Runner -from ranger.core.fm import FM -from ranger.core.environment import Environment -from ranger.shared import (EnvironmentAware, FileManagerAware, - SettingsAware) -from ranger.gui.defaultui import DefaultUI as UI -from ranger.fsobject.file import File def parse_arguments(): """Parse the program arguments""" + from optparse import OptionParser, SUPPRESS_HELP + from ranger import __version__, USAGE, DEFAULT_CONFDIR + from ranger.ext.openstruct import OpenStruct parser = OptionParser(usage=USAGE, version='ranger ' + __version__) parser.add_option('-d', '--debug', action='store_true', @@ -83,6 +73,8 @@ def allow_access_to_confdir(confdir, allow): def load_settings(fm, clean): + import ranger + import ranger.api.commands if not clean: allow_access_to_confdir(ranger.arg.confdir, True) @@ -132,6 +124,7 @@ def load_settings(fm, clean): def load_apps(fm, clean): + import ranger if not clean: allow_access_to_confdir(ranger.arg.confdir, True) try: @@ -152,15 +145,28 @@ def main(): print(errormessage) print('ranger requires the python curses module. Aborting.') sys.exit(1) + from locale import getdefaultlocale, setlocale, LC_ALL + import ranger + from ranger.ext import curses_interrupt_handler + from ranger.core.runner import Runner + from ranger.core.fm import FM + from ranger.core.environment import Environment + from ranger.gui.defaultui import DefaultUI as UI + from ranger.fsobject import File + from ranger.shared import (EnvironmentAware, FileManagerAware, + SettingsAware) # Ensure that a utf8 locale is set. - if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): - for locale in ('en_US.utf8', 'en_US.UTF-8'): - try: setlocale(LC_ALL, locale) - except: pass - else: break + try: + if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): + for locale in ('en_US.utf8', 'en_US.UTF-8'): + try: setlocale(LC_ALL, locale) + except: pass + else: break + else: setlocale(LC_ALL, '') else: setlocale(LC_ALL, '') - else: setlocale(LC_ALL, '') + except: + print("Warning: Unable to set locale. Expect encoding problems.") arg = parse_arguments() ranger.arg = arg @@ -172,6 +178,8 @@ def main(): if arg.targets: target = arg.targets[0] + if target.startswith('file://'): + target = target[7:] if not os.access(target, os.F_OK): print("File or directory doesn't exist: %s" % target) sys.exit(1) @@ -190,6 +198,7 @@ def main(): # Initialize objects EnvironmentAware._assign(Environment(path)) fm = FM() + crash_exception = None try: load_settings(fm, ranger.arg.clean) FileManagerAware._assign(fm) @@ -199,11 +208,25 @@ def main(): fm.initialize() fm.ui.initialize() fm.loop() + except Exception as e: + crash_exception = e + if not (arg.debug or arg.clean): + import traceback + dumpname = ranger.relpath_conf('traceback') + traceback.print_exc(file=open(dumpname, 'w')) finally: fm.destroy() + if crash_exception: + print("Fatal: " + str(crash_exception)) + if arg.debug or arg.clean: + raise crash_exception + else: + print("A traceback has been saved to " + dumpname) + print("Please include it in a bugreport.") if __name__ == '__main__': - top_dir = os.path.dirname(sys.path[0]) - sys.path.insert(0, top_dir) + # The ranger directory can be executed directly, for example by typing + # python /usr/lib/python2.6/site-packages/ranger + sys.path.insert(0, os.path.dirname(sys.path[0])) main() diff --git a/ranger/api/commands.py b/ranger/api/commands.py index 55bb5b54..ca3f730d 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -48,13 +48,14 @@ class CommandContainer(object): def get_command(self, name, abbrev=True): if abbrev: lst = [cls for cmd, cls in self.commands.items() \ - if cmd.startswith(name) \ - and cls.allow_abbrev \ + if cls.allow_abbrev and cmd.startswith(name) \ or cmd == name] if len(lst) == 0: raise KeyError - if len(lst) == 1 or self.commands[name] in lst: + if len(lst) == 1: return lst[0] + if self.commands[name] in lst: + return self.commands[name] raise ValueError("Ambiguous command") else: try: @@ -80,7 +81,7 @@ class Command(FileManagerAware): def tab(self): """Override this""" - def quick_open(self): + def quick(self): """Override this""" def _tab_only_directories(self): diff --git a/ranger/core/environment.py b/ranger/core/environment.py index a485e277..296ba108 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -19,7 +19,7 @@ import pwd import socket from os.path import abspath, normpath, join, expanduser, isdir -from ranger.fsobject.directory import Directory, NoDirectoryGiven +from ranger.fsobject import Directory from ranger.container import KeyBuffer, KeyManager, History from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.shared import SettingsAware @@ -179,12 +179,8 @@ class Environment(SettingsAware, SignalDispatcher): path = normpath(join(self.path, expanduser(path))) if not isdir(path): - return - - try: - new_cwd = self.get_directory(path) - except NoDirectoryGiven: return False + new_cwd = self.get_directory(path) try: os.chdir(path) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 7a55afa7..d2aa00ec 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -30,7 +30,7 @@ from ranger.container import Bookmarks from ranger.core.runner import Runner from ranger import relpath_conf from ranger.ext.get_executables import get_executables -from ranger.fsobject.directory import Directory +from ranger.fsobject import Directory from ranger.ext.signal_dispatcher import SignalDispatcher from ranger import __version__ from ranger.core.loader import Loader diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index bfe038c5..8728f9be 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -58,8 +58,8 @@ from ranger.api.commands import * alias('e', 'edit') alias('q', 'quit') -alias('q!', 'quit!') -alias('qall', 'quit!') +alias('q!', 'quitall') +alias('qall', 'quitall') class cd(Command): """ @@ -213,18 +213,27 @@ class quit(Command): self.fm.tab_close() -class quit_now(Command): +class quitall(Command): """ - :quit! + :quitall Quits the program immediately. """ - name = 'quit!' def execute(self): self.fm.exit() +class quit_bang(quitall): + """ + :quit! + + Quits the program immediately. + """ + name = 'quit!' + allow_abbrev = False + + class terminal(Command): """ :terminal @@ -358,7 +367,10 @@ class edit(Command): def execute(self): line = parse(self.line) - self.fm.edit_file(line.rest(1)) + if not line.chunk(1): + self.fm.edit_file(self.fm.env.cf.path) + else: + self.fm.edit_file(line.rest(1)) def tab(self): return self._tab_directory_content() @@ -403,7 +415,7 @@ class rename(Command): """ def execute(self): - from ranger.fsobject.file import File + from ranger.fsobject import File line = parse(self.line) if not line.rest(1): return self.fm.notify('Syntax: rename <newname>', bad=True) diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index 4dd7f280..b17e5e8f 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -162,26 +162,28 @@ map('T', fm.tag_remove()) map(' ', fm.mark(toggle=True)) map('v', fm.mark(all=True, toggle=True)) -map('V', fm.mark(all=True, val=False)) +map('V', 'uv', fm.mark(all=True, val=False)) # ------------------------------------------ file system operations map('yy', 'y<dir>', fm.copy()) map('dd', 'd<dir>', fm.cut()) -map('ud', fm.uncut()) map('pp', fm.paste()) map('po', fm.paste(overwrite=True)) map('pl', fm.paste_symlink()) map('p<bg>', fm.hint('press *p* once again to confirm pasting' \ ', or *l* to create symlinks')) +map('u<bg>', fm.hint("un*y*ank, unbook*m*ark, unselect:*v*")) +map('ud', 'uy', fm.uncut()) + # ---------------------------------------------------- run programs map('S', fm.execute_command(os.environ['SHELL'])) map('E', fm.edit_file()) map('du', fm.execute_command('du --max-depth=1 -h | less')) # -------------------------------------------------- toggle options -map('z<bg>', fm.hint("show_*h*idden *p*review_files *P*review_dirs " \ - "*d*irs_first flush*i*nput *m*ouse")) +map('z<bg>', fm.hint("[*cdfhimpPs*] show_*h*idden *p*review_files "\ + "*P*review_dirs *f*ilter flush*i*nput *m*ouse")) map('zh', fm.toggle_boolean_option('show_hidden')) map('zp', fm.toggle_boolean_option('preview_files')) map('zP', fm.toggle_boolean_option('preview_directories')) @@ -190,6 +192,7 @@ map('zd', fm.toggle_boolean_option('sort_directories_first')) map('zc', fm.toggle_boolean_option('collapse_preview')) map('zs', fm.toggle_boolean_option('sort_case_insensitive')) map('zm', fm.toggle_boolean_option('mouse_enabled')) +map('zf', fm.open_console(cmode.COMMAND, 'filter ')) # ------------------------------------------------------------ sort map('o<bg>', 'O<bg>', fm.hint("*s*ize *b*ase*n*ame *m*time" \ @@ -217,10 +220,14 @@ def append_to_filename(arg): command = 'rename ' + arg.fm.env.cf.basename arg.fm.open_console(cmode.COMMAND, command) +@map("I") +def insert_before_filename(arg): + append_to_filename(arg) + arg.fm.ui.console.move(right=len('rename '), absolute=True) + map('cw', fm.open_console(cmode.COMMAND, 'rename ')) map('cd', fm.open_console(cmode.COMMAND, 'cd ')) map('f', fm.open_console(cmode.COMMAND_QUICK, 'find ')) -map('bf', fm.open_console(cmode.COMMAND, 'filter ')) map('d<bg>', fm.hint('d*u* (disk usage) d*d* (cut)')) map('@', fm.open_console(cmode.OPEN, '@')) map('#', fm.open_console(cmode.OPEN, 'p!')) @@ -243,7 +250,10 @@ map('gR', fm.cd(RANGERDIR)) map('gc', '<C-W>', fm.tab_close()) map('gt', '<TAB>', fm.tab_move(1)) map('gT', '<S-TAB>', fm.tab_move(-1)) -map('gn', '<C-N>', fm.tab_new()) +@map('gn', '<C-N>') +def newtab_and_gohome(arg): + arg.fm.tab_new() + arg.fm.cd('~') # To return to the original directory, type `` for n in range(1, 10): map('g' + str(n), fm.tab_open(n)) map('<A-' + str(n) + '>', fm.tab_open(n)) @@ -258,7 +268,7 @@ map('ct', fm.search(order='tag')) map('cc', fm.search(order='ctime')) map('cm', fm.search(order='mimetype')) map('cs', fm.search(order='size')) -map('c<bg>', fm.hint('*c*time *m*imetype *s*ize')) +map('c<bg>', fm.hint('*c*time *m*imetype *s*ize *t*ag *w*:rename')) # ------------------------------------------------------- bookmarks for key in ALLOWED_BOOKMARK_KEYS: @@ -266,6 +276,7 @@ for key in ALLOWED_BOOKMARK_KEYS: map("m" + key, fm.set_bookmark(key)) map("um" + key, fm.unset_bookmark(key)) map("`<bg>", "'<bg>", "m<bg>", fm.draw_bookmarks()) +map('um<bg>', fm.hint("delete which bookmark?")) # ---------------------------------------------------- change views map('i', fm.display_file()) @@ -350,18 +361,18 @@ map = keymanager.get_context('console') map.merge(global_keys) map.merge(readline_aliases) -map('<up>', wdg.history_move(-1)) -map('<down>', wdg.history_move(1)) +map('<up>', '<C-P>', wdg.history_move(-1)) +map('<down>', '<C-N>', wdg.history_move(1)) map('<home>', wdg.move(right=0, absolute=True)) map('<end>', wdg.move(right=-1, absolute=True)) map('<tab>', wdg.tab()) map('<s-tab>', wdg.tab(-1)) -map('<c-c>', '<esc>', wdg.close()) +map('<C-C>', '<C-D>', '<ESC>', wdg.close()) map('<CR>', '<c-j>', wdg.execute()) map('<F1>', lambda arg: arg.fm.display_command_help(arg.wdg)) map('<backspace>', wdg.delete(-1)) -map('<delete>', wdg.delete(1)) +map('<delete>', wdg.delete(0)) map('<C-W>', wdg.delete_word()) map('<C-K>', wdg.delete_rest(1)) map('<C-U>', wdg.delete_rest(-1)) diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index ac074e1c..d93d7685 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -88,7 +88,7 @@ max_history_size = 20 max_console_history_size = 20 # Try to keep so much space between the top/bottom border when scrolling: -scroll_offset = 2 +scroll_offset = 8 # Flush the input after each key hit? (Noticable when ranger lags) flushinput = True diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index 2e599c85..75f58ad7 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -68,6 +68,7 @@ class Accumulator(object): return self.move(to=self.pointer) + # XXX Is this still necessary? move() ensures correct pointer position def correct_pointer(self): lst = self.get_list() diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index 1a3d1ec9..beeaf6d3 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -24,7 +24,7 @@ def human_readable(byte, seperator=' '): return '0' exponent = int(math.log(byte, 2) / 10) - flt = float(byte) / (1 << (10 * exponent)) + flt = round(float(byte) / (1 << (10 * exponent)), 2) if exponent > MAX_EXPONENT: return '>9000' # off scale diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py new file mode 100644 index 00000000..9723c1ed --- /dev/null +++ b/ranger/ext/spawn.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from subprocess import Popen, PIPE +ENCODING = 'utf-8' + +def spawn(*args): + """Runs a program, waits for its termination and returns its stdout""" + if len(args) == 1: + popen_arguments = args[0] + else: + popen_arguments = args + process = Popen(popen_arguments, stdout=PIPE) + stdout, stderr = process.communicate() + return stdout.decode(ENCODING) diff --git a/ranger/fsobject/__init__.py b/ranger/fsobject/__init__.py index 10997df6..5fb4b877 100644 --- a/ranger/fsobject/__init__.py +++ b/ranger/fsobject/__init__.py @@ -16,16 +16,9 @@ """FileSystemObjects are representation of files and directories with fast access to their properties through caching""" -T_FILE = 'file' -T_DIRECTORY = 'directory' -T_UNKNOWN = 'unknown' -T_NONEXISTANT = 'nonexistant' - BAD_INFO = '?' -class NotLoadedYet(Exception): - pass - +# So they can be imported from other files more easily: from .fsobject import FileSystemObject from .file import File -from .directory import Directory, NoDirectoryGiven +from .directory import Directory diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index 43af772a..60387e7b 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -13,10 +13,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os +import os.path +import stat +from stat import S_ISLNK, S_ISDIR +from os.path import join, isdir, basename from collections import deque from time import time +from ranger.ext.mount_path import mount_path from ranger.fsobject import BAD_INFO, File, FileSystemObject from ranger.shared import SettingsAware from ranger.ext.accumulator import Accumulator @@ -34,8 +38,17 @@ def sort_by_directory(path): """returns 0 if path is a directory, otherwise 1 (for sorting)""" return 1 - path.is_directory -class NoDirectoryGiven(Exception): - pass +def accept_file(fname, hidden_filter, name_filter): + if hidden_filter: + try: + if hidden_filter.search(fname): + return False + except: + if hidden_filter in fname: + return False + if name_filter and name_filter not in fname: + return False + return True class Directory(FileSystemObject, Accumulator, SettingsAware): is_directory = True @@ -68,14 +81,11 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): 'type': lambda path: path.mimetype, } - def __init__(self, path): - from os.path import isfile - - if isfile(path): - raise NoDirectoryGiven() + def __init__(self, path, **kw): + assert not os.path.isfile(path), "No directory given!" Accumulator.__init__(self) - FileSystemObject.__init__(self, path) + FileSystemObject.__init__(self, path, **kw) self.marked_items = list() @@ -150,33 +160,19 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): in each iteration. """ - # log("generating loader for " + self.path + "(" + str(id(self)) + ")") - from os.path import join, isdir, basename - from os import listdir - import ranger.ext.mount_path - self.loading = True self.load_if_outdated() try: if self.exists and self.runnable: yield - self.mount_path = ranger.ext.mount_path.mount_path(self.path) - - filenames = [] - for fname in listdir(self.path): - if not self.settings.show_hidden: - hfilter = self.settings.hidden_filter - if hfilter: - if isinstance(hfilter, str) and hfilter in fname: - continue - if hasattr(hfilter, 'search') and \ - hfilter.search(fname): - continue - if isinstance(self.filter, str) and self.filter \ - and self.filter not in fname: - continue - filenames.append(join(self.path, fname)) + self.mount_path = mount_path(self.path) + + hidden_filter = not self.settings.show_hidden \ + and self.settings.hidden_filter + filenames = [join(self.path, fname) \ + for fname in os.listdir(self.path) \ + if accept_file(fname, hidden_filter, self.filter)] yield self.load_content_mtime = os.stat(self.path).st_mtime @@ -185,13 +181,25 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): files = [] for name in filenames: - if isdir(name): + try: + file_lstat = os.lstat(name) + if S_ISLNK(file_lstat.st_mode): + file_stat = os.stat(name) + else: + file_stat = file_lstat + stats = (file_stat, file_lstat) + is_a_dir = S_ISDIR(file_stat.st_mode) + except: + stats = None + is_a_dir = False + if is_a_dir: try: item = self.fm.env.get_directory(name) except: - item = Directory(name) + item = Directory(name, preload=stats, + path_is_abs=True) else: - item = File(name) + item = File(name, preload=stats, path_is_abs=True) item.load_if_outdated() files.append(item) yield @@ -205,9 +213,10 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): self._clear_marked_items() for item in self.files: if item.path in marked_paths: - self.mark_item(item, True) + item._mark(True) + self.marked_items.append(item) else: - self.mark_item(item, False) + item._mark(False) self.sort() @@ -402,8 +411,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): def __len__(self): """The number of containing files""" - if not self.accessible or not self.content_loaded: - raise ranger.fsobject.NotLoadedYet() + assert self.accessible + assert self.content_loaded assert self.files is not None return len(self.files) diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index aa44016e..4618df33 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -13,11 +13,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -control_characters = set(chr(n) for n in set(range(0, 9)) | set(range(14,32))) N_FIRST_BYTES = 20 +control_characters = set(chr(n) for n in + set(range(0, 9)) | set(range(14, 32))) -from .fsobject import FileSystemObject as SuperClass -class File(SuperClass): +from ranger.fsobject import FileSystemObject +class File(FileSystemObject): is_file = True @property @@ -37,4 +38,3 @@ class File(SuperClass): if self.firstbytes and control_characters & set(self.firstbytes): return True return False - diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index f3d40614..6bb8b6ec 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -14,19 +14,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. CONTAINER_EXTENSIONS = 'rar zip tar gz bz bz2 tgz 7z iso cab'.split() -DOCUMENT_EXTENSIONS = 'pdf doc ppt odt'.split() -DOCUMENT_BASENAMES = 'README TODO LICENSE COPYING INSTALL'.split() -DOCUMENT_EXTENSIONS = () -DOCUMENT_BASENAMES = () import stat import os +from stat import S_ISLNK, S_ISCHR, S_ISBLK, S_ISSOCK, S_ISFIFO, \ + S_ISDIR, S_IXUSR from time import time -from subprocess import Popen, PIPE from os.path import abspath, basename, dirname, realpath -from . import T_FILE, T_DIRECTORY, T_UNKNOWN, T_NONEXISTANT, BAD_INFO +from . import BAD_INFO from ranger.shared import MimeTypeAware, FileManagerAware from ranger.ext.shell_escape import shell_escape +from ranger.ext.spawn import spawn from ranger.ext.human_readable import human_readable class FileSystemObject(MimeTypeAware, FileManagerAware): @@ -55,7 +53,6 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): stat = None infostring = None permissions = None - type = T_UNKNOWN size = 0 last_used = None @@ -70,15 +67,17 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): container = False mimetype_tuple = () - def __init__(self, path): + def __init__(self, path, preload=None, path_is_abs=False): MimeTypeAware.__init__(self) - path = abspath(path) + if not path_is_abs: + path = abspath(path) self.path = path self.basename = basename(path) self.basename_lower = self.basename.lower() self.dirname = dirname(path) self.realpath = self.path + self.preload = preload try: lastdot = self.basename.rindex('.') + 1 @@ -86,7 +85,6 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): except ValueError: self.extension = None - self.set_mimetype() self.use() def __repr__(self): @@ -102,8 +100,7 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): def filetype(self): if self._filetype is None: try: - got = Popen(["file", '-Lb', '--mime-type', self.path], - stdout=PIPE).communicate()[0] + got = spawn(["file", '-Lb', '--mime-type', self.path]) except OSError: self._filetype = '' else: @@ -129,24 +126,38 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): def set_mimetype(self): """assign attributes such as self.video according to the mimetype""" - self.mimetype = self.mimetypes.guess_type(self.basename, False)[0] - if self.mimetype is None: - self.mimetype = '' + self._mimetype = self.mimetypes.guess_type(self.basename, False)[0] + if self._mimetype is None: + self._mimetype = '' - self.video = self.mimetype.startswith('video') - self.image = self.mimetype.startswith('image') - self.audio = self.mimetype.startswith('audio') + self.video = self._mimetype.startswith('video') + self.image = self._mimetype.startswith('image') + self.audio = self._mimetype.startswith('audio') self.media = self.video or self.image or self.audio - self.document = self.mimetype.startswith('text') \ - or (self.extension in DOCUMENT_EXTENSIONS) \ - or (self.basename in DOCUMENT_BASENAMES) + self.document = self._mimetype.startswith('text') self.container = self.extension in CONTAINER_EXTENSIONS keys = ('video', 'audio', 'image', 'media', 'document', 'container') - self.mimetype_tuple = tuple(key for key in keys if getattr(self, key)) + self._mimetype_tuple = tuple(key for key in keys if getattr(self, key)) - if self.mimetype == '': - self.mimetype = None + if self._mimetype == '': + self._mimetype = None + + @property + def mimetype(self): + try: + return self._mimetype + except: + self.set_mimetype() + return self._mimetype + + @property + def mimetype_tuple(self): + try: + return self._mimetype_tuple + except: + self.set_mimetype() + return self._mimetype_tuple def mark(self, boolean): directory = self.env.get_directory(self.dirname) @@ -166,20 +177,20 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.infostring = 'sock' elif self.is_directory: try: - self.size = len(os.listdir(self.path)) + self.size = len(os.listdir(self.path)) # bite me except OSError: self.infostring = BAD_INFO self.accessible = False else: - self.infostring = " %d" % self.size + self.infostring = ' %d' % self.size self.accessible = True self.runnable = True elif self.is_file: - try: + if self.stat: self.size = self.stat.st_size self.infostring = ' ' + human_readable(self.size) - except: - pass + else: + self.infostring = BAD_INFO if self.is_link: self.infostring = '->' + self.infostring @@ -190,42 +201,48 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): """ self.loaded = True - try: - self.stat = os.lstat(self.path) - except OSError: - self.stat = None - self.is_link = False - self.accessible = False - else: - self.is_link = stat.S_ISLNK(self.stat.st_mode) + + # Get the stat object, either from preload or from os.[l]stat + self.stat = None + if self.preload: + self.stat = self.preload[1] + self.is_link = S_ISLNK(self.stat.st_mode) if self.is_link: - try: # try to resolve the link - self.readlink = os.readlink(self.path) - self.realpath = realpath(self.path) - self.stat = os.stat(self.path) - except: # it failed, so it must be a broken link - pass + self.stat = self.preload[0] + self.preload = None + else: + try: + self.stat = os.lstat(self.path) + except: + pass + else: + self.is_link = S_ISLNK(self.stat.st_mode) + if self.is_link: + try: + self.stat = os.stat(self.path) + except: + pass + + # Set some attributes + if self.stat: mode = self.stat.st_mode - self.is_device = bool(stat.S_ISCHR(mode) or stat.S_ISBLK(mode)) - self.is_socket = bool(stat.S_ISSOCK(mode)) - self.is_fifo = bool(stat.S_ISFIFO(mode)) - self.accessible = True - - if self.accessible and os.access(self.path, os.F_OK): - self.exists = True + self.is_device = bool(S_ISCHR(mode) or S_ISBLK(mode)) + self.is_socket = bool(S_ISSOCK(mode)) + self.is_fifo = bool(S_ISFIFO(mode)) self.accessible = True - if os.path.isdir(self.path): - self.type = T_DIRECTORY - self.runnable = bool(mode & stat.S_IXUSR) - elif os.path.isfile(self.path): - self.type = T_FILE + if os.access(self.path, os.F_OK): + self.exists = True + if S_ISDIR(mode): + self.runnable = bool(mode & S_IXUSR) else: - self.type = T_UNKNOWN - + self.exists = False + self.runnable = False + if self.is_link: + self.realpath = realpath(self.path) + self.readlink = os.readlink(self.path) else: - self.type = T_NONEXISTANT - self.exists = False - self.runnable = False + self.accessible = False + self.determine_infostring() def get_permission_string(self): @@ -237,9 +254,9 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): except: return '----??----' - if stat.S_ISDIR(mode): + if S_ISDIR(mode): perms = ['d'] - elif stat.S_ISLNK(mode): + elif S_ISLNK(mode): perms = ['l'] else: perms = ['-'] diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py index f3955825..4a2860b8 100644 --- a/ranger/gui/mouse_event.py +++ b/ranger/gui/mouse_event.py @@ -21,6 +21,7 @@ class MouseEvent(object): curses.BUTTON2_PRESSED, curses.BUTTON3_PRESSED, curses.BUTTON4_PRESSED ] + CTRL_SCROLLWHEEL_MULTIPLIER = 5 def __init__(self, getmouse): """Creates a MouseEvent object from the result of win.getmouse()""" @@ -42,11 +43,15 @@ class MouseEvent(object): return False def mouse_wheel_direction(self): + """Returns the direction of the scroll action, 0 if there was none""" + # If the bstate > ALL_MOUSE_EVENTS, it's an invalid mouse button. + # I interpret invalid buttons as "scroll down" because all tested + # systems have a broken curses implementation and this is a workaround. if self.bstate & curses.BUTTON4_PRESSED: - return -1 + return self.ctrl() and -self.CTRL_SCROLLWHEEL_MULTIPLIER or -1 elif self.bstate & curses.BUTTON2_PRESSED \ or self.bstate > curses.ALL_MOUSE_EVENTS: - return 1 + return self.ctrl() and self.CTRL_SCROLLWHEEL_MULTIPLIER or 1 else: return 0 diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index e40003a2..e9c20395 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -185,12 +185,12 @@ class UI(DisplayableContainer): keys = [27, keys[1] - 128] self.handle_keys(*keys) self.set_load_mode(previous_load_mode) - if self.settings.flushinput: + if self.settings.flushinput and not self.console.visible: curses.flushinp() else: # Handle simple key presses, CTRL+X, etc here: if key > 0: - if self.settings.flushinput: + if self.settings.flushinput and not self.console.visible: curses.flushinp() if key == curses.KEY_MOUSE: self.handle_mouse() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index e4ba4b64..6e020e5f 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -87,7 +87,8 @@ class BrowserColumn(Pager): def click(self, event): """Handle a MouseEvent""" - if not (event.pressed(1) or event.pressed(3)): + direction = event.mouse_wheel_direction() + if not (event.pressed(1) or event.pressed(3) or direction): return False if self.target is None: @@ -97,7 +98,12 @@ class BrowserColumn(Pager): if self.target.accessible and self.target.content_loaded: index = self.scroll_begin + event.y - self.y - if event.pressed(1): + if direction: + if self.level == -1: + self.fm.move_parent(direction) + else: + return False + elif event.pressed(1): if not self.main_column: self.fm.enter_dir(self.target.path) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index 33418a2f..a90231f2 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -24,7 +24,7 @@ from ..displayable import DisplayableContainer class BrowserView(Widget, DisplayableContainer): ratios = None preview = True - preview_available = True + is_collapsed = False draw_bookmarks = False stretch_ratios = None need_clear = False @@ -196,6 +196,11 @@ class BrowserView(Widget, DisplayableContainer): except: pass + def _collapse(self): + # Should the last column be cut off? (Because there is no preview) + return self.settings.collapse_preview and self.preview and \ + not self.columns[-1].has_preview() and self.stretch_ratios + def resize(self, y, x, hei, wid): """Resize all the columns according to the given ratio""" DisplayableContainer.resize(self, y, x, hei, wid) @@ -203,10 +208,8 @@ class BrowserView(Widget, DisplayableContainer): pad = 1 if borders else 0 left = pad - cut_off_last = self.preview and not self.preview_available \ - and self.stretch_ratios - - if cut_off_last: + self.is_collapsed = self._collapse() + if self.is_collapsed: generator = enumerate(self.stretch_ratios) else: generator = enumerate(self.ratios) @@ -232,12 +235,12 @@ class BrowserView(Widget, DisplayableContainer): left += wid def click(self, event): - n = event.ctrl() and 5 or 1 + if DisplayableContainer.click(self, event): + return True direction = event.mouse_wheel_direction() if direction: self.main_column.scroll(direction) - else: - DisplayableContainer.click(self, event) + return False def open_pager(self): self.pager.visible = True @@ -263,8 +266,5 @@ class BrowserView(Widget, DisplayableContainer): def poke(self): DisplayableContainer.poke(self) - if self.settings.collapse_preview and self.preview: - has_preview = self.columns[-1].has_preview() - if self.preview_available != has_preview: - self.preview_available = has_preview - self.resize(self.y, self.x, self.hei, self.wid) + if self.preview and self.is_collapsed != self._collapse(): + self.resize(self.y, self.x, self.hei, self.wid) diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 59a779de..fa9e438e 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -452,17 +452,29 @@ class OpenConsole(ConsoleWithTab): if file.shell_escaped_basename.startswith(start_of_word)) def _substitute_metachars(self, command): - dct = {} + macros = {} if self.fm.env.cf: - dct['f'] = shell_quote(self.fm.env.cf.basename) + macros['f'] = shell_quote(self.fm.env.cf.basename) else: - dct['f'] = '' + macros['f'] = '' - dct['s'] = ' '.join(shell_quote(fl.basename) \ + macros['s'] = ' '.join(shell_quote(fl.basename) \ for fl in self.fm.env.get_selection()) - return _CustomTemplate(command).safe_substitute(dct) + macros['c'] = ' '.join(shell_quote(fl.path) + for fl in self.fm.env.copy) + + macros['t'] = ' '.join(shell_quote(fl.basename) + for fl in self.fm.env.cwd.files + if fl.realpath in self.fm.tags) + + if self.fm.env.cwd: + macros['d'] = shell_quote(self.fm.env.cwd.path) + else: + macros['d'] = '.' + + return _CustomTemplate(command).safe_substitute(macros) def _parse(self): if '!' in self.line: diff --git a/ranger/help/__init__.py b/ranger/help/__init__.py index 1a651d56..f304c7bc 100644 --- a/ranger/help/__init__.py +++ b/ranger/help/__init__.py @@ -24,7 +24,8 @@ NO_HELP = """No help was found. Possibly the program was invoked with "python -OO" which discards all documentation.""" -HELP_TOPICS = ('index', 'movement', 'starting', 'console', 'fileop') +HELP_TOPICS = ('index', 'movement', 'starting', 'console', 'fileop', + 'invocation') def get_docstring_of_module(path, module_name): imported = __import__(path, fromlist=[module_name]) diff --git a/ranger/help/console.py b/ranger/help/console.py index 04372f38..d1764841 100644 --- a/ranger/help/console.py +++ b/ranger/help/console.py @@ -19,6 +19,7 @@ 3.2. List of Commands 3.3. The (Quick) Command Console 3.4. The Open Console +3.5. The Quick Open Console ============================================================================== @@ -144,9 +145,9 @@ ranger/ext/command_parser.py is used. The tab method should return None, a string or an iterable sequence containing the strings which should be cycled through by pressing tab. -Only those commands which implement the quick_open method will be specially +Only those commands which implement the quick() method will be specially treated by the Quick Command Console. For the rest, both consoles are equal. -quick_open is called after each key press and if it returns True, the +quick() is called after each key press and if it returns True, the command will be executed immediately. @@ -163,8 +164,21 @@ Open this console by pressing "!" or "s" The Open Console allows you to execute shell commands: !vim * will run vim and open all files in the directory. -%f will be replaced with the basename of the highlighted file -%s will be selected with all files in the selection +Like in similar filemanagers there are some macros. Use them in +commands and they will be replaced with a list of files. + %f the highlighted file + %d the path of the current directory + %s the selected files in the current directory. If no files are + selected, it defaults to the same as %f + %t all tagged files in the current directory + %c the full paths of the currently copied/cut files + +%c is the only macro which ranges out of the current directory. So you may +"abuse" the copying function for other purposes, like diffing two files which +are in different directories: + + Yank the file A (type yy), move to the file B and use: + !p!diff %c %f There is a special syntax for more control: @@ -175,7 +189,12 @@ There is a special syntax for more control: Those two can be combinated: !d!@mplayer will open the selection with a detached mplayer - (again, this is equivalent to !d!mplayer %s) + (again, this is equivalent to !d!mplayer %s) + +These keys open the console with a predefined text: + @ "!@" Suffixes %s. Good for things like "@mount" + # "!p!" Pipes output through a pager. For commands with output. + Note: A plain "!p!" will be translated to "!p!cat %f" For a list of other flags than "d", check chapter 2.5 of the documentation @@ -207,7 +226,7 @@ open with: 1 open it with the default handler in mode 1 open with: d open it detached with the default handler open with: p open it as usual, but pipe the output to "less" open with: totem 1 Ds open in totem in mode 1, will not detach the - process (flag D) but discard the output (flag s) + process (flag D) but discard the output (flag s) ============================================================================== diff --git a/ranger/help/index.py b/ranger/help/index.py index 794e3ea9..316e975f 100644 --- a/ranger/help/index.py +++ b/ranger/help/index.py @@ -26,6 +26,7 @@ |2?| Running Files |3?| The console |4?| File operations + |5?| Ranger invocation ============================================================================== diff --git a/ranger/help/invocation.py b/ranger/help/invocation.py new file mode 100644 index 00000000..3de574cc --- /dev/null +++ b/ranger/help/invocation.py @@ -0,0 +1,106 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +5. Ranger invocation + +5.1. Command Line Arguments +5.2. Python Options + + +============================================================================== +5.1. Command Line Arguments + +These options can be passed to ranger when starting it from the +command line. + +--version + Print the version and exit. + +-h, --help + Print a list of options and exit. + +-d, --debug + Activate the debug mode: Whenever an error occurs, ranger + will exit and print a full backtrace. The default behaviour + is to merely print the name of the exception in the statusbar/log + and to try to keep running. + +-c, --clean + Activate the clean mode: Ranger will not access or create any + configuration files nor will it leave any traces on your system. + This is useful when your configuration is broken, when you want + to avoid clutter, etc. + +--fail-if-run + Return the exit code 1 if ranger is used to run a file, for example + with `ranger --fail-if-run filename`. This can be useful for scripts. + +-r <dir>, --confdir=<dir> + Define a different configuration directory. The default is + $HOME/.ranger. + +-m <n>, --mode=<n> + When a filename is supplied, make it run in mode <n> |2| + +-f <flags>, --flags=<flags> + When a filename is supplied, run it with the flags <flags> |2| + +(Optional) Positional Argument + The positional argument should be a path to the directory you + want ranger to start in, or the file which you want to run. + Only one positional argument is accepted as of now. + +-- + Stop looking for options. All following arguments are treated as + positional arguments. + +Examples: + ranger episode1.avi + ranger --debug /usr/bin + ranger --confdir=~/.config/ranger --fail-if-run + + +============================================================================== +5.2. Python Options + +Ranger makes use of python optimize flags. To use them, run ranger like this: + PYTHONOPTIMIZE=1 ranger +An alternative is: + python -O `which ranger` +Or you could change the first line of the ranger script and add -O/-OO. +The first way is the recommended one. Of course you can make an alias or +a shell fuction to save typing. + +Using PYTHONOPTIMIZE=1 (-O) will make python discard assertion statements. +Assertions are little pieces of code which are helpful for finding errors, +but unless you're touching sensitive parts of ranger, you may want to +disable them to save some computing power. + +Using PYTHONOPTIMIZE=2 (-OO) will additionally discard any docstrings. +In ranger, most built-in documentation (F1/? keys) is implemented with +docstrings. Use this option if you don't need the documentation. + +Examples: + PYTHONOPTIMIZE=1 ranger episode1.avi + PYTHONOPTIMIZE=2 ranger --debug /usr/bin + python -OO `which ranger` --confdir=~/.config/ranger --fail-if-run + +Note: The author expected "-OO" to reduce the memory usage, but that +doesn't seem to happen. + + +============================================================================== +""" +# vim:tw=78:sw=8:sts=8:ts=8:ft=help diff --git a/ranger/help/movement.py b/ranger/help/movement.py index e85bf336..194dd3a9 100644 --- a/ranger/help/movement.py +++ b/ranger/help/movement.py @@ -19,10 +19,11 @@ 1.1. Move around 1.2. Browser control 1.3. Searching -1.4. Cycling +1.4. Sorting 1.5. Bookmarks 1.6. Tabs 1.7. Mouse usage +1.8. Misc keys ============================================================================== @@ -64,6 +65,15 @@ These keys work like in vim: ^B move up by one screen ^F move down by one screen +This keys can be used to make movements beyond the current directory + + ] move down in the parent directory + [ move up in the parent directory + + } traverse the directory tree, visiting each directory + { traverse in the other direction. (not implemented yet, + currently this only moves back in history) + ============================================================================== 1.2. Browser control @@ -74,6 +84,7 @@ These keys work like in vim: ^L redraw the window : open the console |3?| z toggle options + u undo certain things (unyank, unmark,...) i inspect the content of the file E edit the file @@ -88,7 +99,7 @@ of the file you're pointing at. V remove all marks By "tagging" files, you can highlight them and mark them to be -special in whatever context you want. +special in whatever context you want. Tags are persistent across sessions. t tag/untag the selection T untag the selection @@ -115,10 +126,10 @@ visible files. Pressing "n" will move you to the next occurance, "N" to the previous one. You can search for more than just strings: - ct search tagged files - cc cycle through all files by their ctime (last modification) + cc cycle through all files by their ctime (last inode change) cm cycle by mime type, connecting similar files cs cycle by size, large items first + ct search tagged files ============================================================================== @@ -139,7 +150,9 @@ be reversed. 1.5. Bookmarks Type "m<key>" to bookmark the current directory. You can re-enter this -directory by typing "`<key>". <key> can be any letter or digit. +directory by typing "`<key>". <key> can be any letter or digit. Unlike vim, +both lowercase and uppercase bookmarks are persistent. + Each time you jump to a bookmark, the special bookmark at key ` will be set to the last directory. So typing "``" gets you back to where you were before. @@ -155,8 +168,9 @@ In Ranger, tabs are very simple though and only store the directory path. gt Go to the next tab. (also TAB) gT Go to the previous tab. (also Shift+TAB) gn, ^N Create a new tab - g<N> Open a tab. N has to be a number from 0 to 9. + g<N> Open a tab. N has to be a number from 1 to 9. If the tab doesn't exist yet, it will be created. + On most terminals, Alt-1, Alt-2, etc., also work. gc, ^W Close the current tab. The last tab cannot be closed. @@ -172,5 +186,16 @@ Clicking into the preview window will usually run the file. |2?| ============================================================================== +1.8 Misc keys + + ^P Display the message log + du Display the disk usage of the current directory + cd Open the console with ":cd " + cw Open the console with ":rename " + A Open the console with ":rename <current filename>" + I Same as A, put the cursor at the beginning of the filename + + +============================================================================== """ # vim:tw=78:sw=4:sts=8:ts=8:ft=help diff --git a/ranger/help/starting.py b/ranger/help/starting.py index 8fd8b0da..069d6a04 100644 --- a/ranger/help/starting.py +++ b/ranger/help/starting.py @@ -37,7 +37,7 @@ use them. Otherwise use the file under the cursor. ============================================================================== -2.2. open with: +2.2. The "open with" prompt If the automatic filetype detection fails or starts the file in a wrong way, you can press "r" to manually tell ranger how to run it. diff --git a/ranger/shared/mimetype.py b/ranger/shared/mimetype.py index 1a7f79a0..c6577056 100644 --- a/ranger/shared/mimetype.py +++ b/ranger/shared/mimetype.py @@ -17,9 +17,7 @@ from ranger import relpath import mimetypes class MimeTypeAware(object): mimetypes = {} - __initialized = False def __init__(self): - if not MimeTypeAware.__initialized: - MimeTypeAware.mimetypes = mimetypes.MimeTypes() - MimeTypeAware.mimetypes.read(relpath('data/mime.types')) - MimeTypeAware.__initialized = True + MimeTypeAware.__init__ = lambda _: None # refuse multiple inits + MimeTypeAware.mimetypes = mimetypes.MimeTypes() + MimeTypeAware.mimetypes.read(relpath('data/mime.types')) |