diff options
78 files changed, 1813 insertions, 2024 deletions
diff --git a/Makefile b/Makefile index f13de38d..20cf6f25 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. NAME = ranger VERSION = $(shell grep -m 1 -o '[0-9][0-9.]\+' README) @@ -72,7 +60,7 @@ doc: cleandoc test: @for FILE in $(shell grep -IHm 1 doctest -r ranger | cut -d: -f1); do \ echo "Testing $$FILE..."; \ - PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \ + RANGER_DOCTEST=1 PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \ done man: diff --git a/README b/README index d1f9f513..0b0cf9a7 100644 --- a/README +++ b/README @@ -7,8 +7,8 @@ open your files with. This file describes ranger and how to get it to run. For instructions on the usage, please read the man page. See doc/HACKING for development specific -information. For configuration, check the files in ranger/defaults/. They -are usually installed to /usr/lib/python*/site-packages/ranger/defaults/ +information. For configuration, check the files in ranger/config/. They +are usually installed to /usr/lib/python*/site-packages/ranger/config/ and can be obtained with ranger's --copy-config option. A note to packagers: Versions meant for packaging are listed in the changelog @@ -92,5 +92,5 @@ directory. Ranger can automatically copy default configuration files to ~/.config/ranger if you run it with the switch --copy-config. (see ranger --help for a -description of that switch.) Also check ranger/defaults/ for the default +description of that switch.) Also check ranger/config/ for the default configuration. diff --git a/doc/HACKING b/doc/HACKING index f6d5d064..89452398 100644 --- a/doc/HACKING +++ b/doc/HACKING @@ -46,7 +46,7 @@ In ranger/fsobject/file.py the constant PREVIEW_BLACKLIST * Adding options: -In ranger/defaults/options.py +In ranger/config/options.py add the default value, like: my_option = True In ranger/container/settingobject.py add the name of your option to the constant ALLOWED_SETTINGS @@ -58,12 +58,12 @@ assuming <self> is a "SettingsAware" object. Copy ranger/colorschemes/default.py to ranger/colorschemes/myscheme.py and modify it according to your needs. Alternatively, mimic the jungle colorscheme. It subclasses the default scheme and just modifies a few things. -In ranger/defaults/options.py (or ~/.config/ranger/options.py), change +In ranger/config/options.py (or ~/.config/ranger/options.py), change colorscheme = 'default' to: colorscheme = 'myscheme' * Change the file type => application associations: -In ranger/defaults/apps.py +In ranger/config/apps.py modify the method app_default. The variable "f" is a filesystem-object with attributes like mimetype, extension, etc. For a full list, check ranger/fsobject/fsobject.py diff --git a/doc/ranger.1 b/doc/ranger.1 index e5a79160..cfdbd455 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -146,8 +146,8 @@ open your files with. .PP This manual mainly contains information on the usage of ranger. Refer to the \&\fI\s-1README\s0\fR for install instructions and to \fIdoc/HACKING\fR for development -specific information. For configuration, see the files in \fIranger/defaults\fR. -They are usually installed to \fI/usr/lib/python*/site\-packages/ranger/defaults\fR +specific information. For configuration, see the files in \fIranger/config\fR. +They are usually installed to \fI/usr/lib/python*/site\-packages/ranger/config\fR and can be obtained with ranger's \-\-copy\-config option. .PP Inside ranger, you can press \fI1?\fR for a list of key bindings, \fI2?\fR for a list @@ -321,7 +321,7 @@ to extract an archive. See the \fIapps.py\fR configuration file for all program and modes. .SH "KEY BINDINGS" .IX Header "KEY BINDINGS" -Key bindings are defined in the file \fIranger/defaults/rc.conf\fR. Check this +Key bindings are defined in the file \fIranger/config/rc.conf\fR. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the \-\-copy\-config=rc option. .PP diff --git a/doc/ranger.pod b/doc/ranger.pod index 9f8b4f04..2a3a345b 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -24,8 +24,8 @@ open your files with. This manual mainly contains information on the usage of ranger. Refer to the F<README> for install instructions and to F<doc/HACKING> for development -specific information. For configuration, see the files in F<ranger/defaults>. -They are usually installed to F</usr/lib/python*/site-packages/ranger/defaults> +specific information. For configuration, see the files in F<ranger/config>. +They are usually installed to F</usr/lib/python*/site-packages/ranger/config> and can be obtained with ranger's --copy-config option. Inside ranger, you can press I<1?> for a list of key bindings, I<2?> for a list @@ -229,7 +229,7 @@ and modes. =head1 KEY BINDINGS -Key bindings are defined in the file F<ranger/defaults/rc.conf>. Check this +Key bindings are defined in the file F<ranger/config/rc.conf>. Check this file for a list of all key bindings. You can copy it to your local configuration directory with the --copy-config=rc option. diff --git a/examples/README b/examples/README new file mode 100644 index 00000000..60f29ac6 --- /dev/null +++ b/examples/README @@ -0,0 +1,2 @@ +Thes files in this directory contain applications or extensions of ranger which +are put here for your inspiration and as references. diff --git a/examples/bash_automatic_cd.sh b/examples/bash_automatic_cd.sh new file mode 100644 index 00000000..a63280d1 --- /dev/null +++ b/examples/bash_automatic_cd.sh @@ -0,0 +1,19 @@ +# Automatically change the directory in bash after closing ranger +# +# This is a bash function for .bashrc to automatically change the directory to +# the last visited one after ranger quits. +# To undo the effect of this function, you can type "cd -" to return to the +# original directory. + +function ranger-cd { + tempfile='/tmp/chosendir' + /usr/bin/ranger --choosedir="$tempfile" "${@:-$(pwd)}" + test -f "$tempfile" && + if [ "$(cat -- "$tempfile")" != "$(echo -n `pwd`)" ]; then + cd -- "$(cat "$tempfile")" + fi + rm -f -- "$tempfile" +} + +# This binds Ctrl-O to ranger-cd: +bind '"\C-o":"ranger-cd\C-m"' diff --git a/examples/bash_subshell_notice.sh b/examples/bash_subshell_notice.sh new file mode 100644 index 00000000..b6b03d74 --- /dev/null +++ b/examples/bash_subshell_notice.sh @@ -0,0 +1,5 @@ +# Change the prompt when you open a shell from inside ranger +# +# Add this line to your .bashrc for it to work. + +[ -n "$RANGER_LEVEL" ] && PS1="$PS1"'(in ranger) ' diff --git a/examples/rifle_sxiv.sh b/examples/rifle_sxiv.sh new file mode 100755 index 00000000..efa935b2 --- /dev/null +++ b/examples/rifle_sxiv.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# This script searches image files in a directory, opens them all with sxiv +# and sets the first argument to the first image displayed by sxiv. +# +# This is supposed to be used in rifle.conf as a workaround for the fact that +# sxiv takes no file name arguments for the first image, just the number. +# Copy this file somewhere into your $PATH and add this at the top of rifle.conf: +# +# mime ^image, has sxiv, X, flag f = path/to/this/script -- "$@" +# + +[ "$1" == '--' ] && shift +target="$(realpath -s "$1")" +function listfiles { + find -L "$(dirname "$target")" -maxdepth 1 -type f -iregex \ + '.*\(jpe?g\|bmp\|png\|gif\)$' -print0 | sort -z +} + +count="$(listfiles | grep -m 1 -Zznx "$target" | cut -d: -f1)" + +if [ -n "$count" ]; then + listfiles | xargs -0 sxiv -n "$count" -- +else + sxiv -- "$@" # fallback +fi diff --git a/examples/vim_file_chooser.vim b/examples/vim_file_chooser.vim new file mode 100644 index 00000000..a44ab2b7 --- /dev/null +++ b/examples/vim_file_chooser.vim @@ -0,0 +1,15 @@ +" Add ranger as a file chooser in vim +" +" If you add this function and the key binding to the .vimrc, ranger can be +" started using the keybinding ",r". Once you select a file by pressing +" enter, ranger will quit again and vim will open the selected file. + +fun! RangerChooser() + exec "silent !ranger --choosefile=/tmp/chosenfile " . expand("%:p:h") + if filereadable('/tmp/chosenfile') + exec 'edit ' . system('cat /tmp/chosenfile') + call system('rm /tmp/chosenfile') + endif + redraw! +endfun +map ,r :call RangerChooser()<CR> diff --git a/ranger.py b/ranger.py index c763a8d9..efd81f4d 100755 --- a/ranger.py +++ b/ranger.py @@ -1,19 +1,7 @@ #!/usr/bin/python -O # ranger - a vim-inspired file manager for the console (coding: utf-8) # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. # ===================== # This embedded bash script can be executed by sourcing this file. diff --git a/ranger/__init__.py b/ranger/__init__.py index fa6800b3..ea94f53c 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ A console file manager with VI key bindings. @@ -33,13 +21,38 @@ __email__ = 'romanz@lavabit.com' RANGERDIR = os.path.dirname(__file__) TICKS_BEFORE_COLLECTING_GARBAGE = 100 TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200 +MAX_RESTORABLE_TABS = 3 MACRO_DELIMITER = '%' +DEFAULT_PAGER = 'less' LOGFILE = '/tmp/ranger_errorlog' USAGE = '%prog [options] [path/filename]' -STABLE = True +VERSION = 'ranger-git based on %s' % __version__ # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead. CONFDIR = '~/.config/ranger' +# Debugging functions. These will be activated when run with --debug. +# Example usage in the code: +# import ranger; ranger.log("hello world") +def log(*objects, **keywords): + """ + Writes objects to a logfile (for the purpose of debugging only.) + Has the same arguments as print() in python3. + """ + from ranger import arg + if LOGFILE is None or not arg.debug or arg.clean: return + start = 'start' in keywords and keywords['start'] or 'ranger:' + sep = 'sep' in keywords and keywords['sep'] or ' ' + _file = 'file' in keywords and keywords['file'] or open(LOGFILE, 'a') + end = 'end' in keywords and keywords['end'] or '\n' + _file.write(sep.join(map(str, (start, ) + objects)) + end) + + +def log_traceback(): + from ranger import arg + if LOGFILE is None or not arg.debug or arg.clean: return + import traceback + traceback.print_stack(file=open(LOGFILE, 'a')) + from ranger.core.main import main diff --git a/ranger/api/apps.py b/ranger/api/apps.py deleted file mode 100644 index 56f8afd2..00000000 --- a/ranger/api/apps.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (C) 2009, 2010, 2011 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/>. - -import os, sys, re -from ranger.api import * -from ranger.ext.iter_tools import flatten -from ranger.ext.get_executables import get_executables -from ranger.core.runner import Context -from ranger.core.shared import FileManagerAware - - -class Applications(FileManagerAware): - """ - This class contains definitions on how to run programs and should - be extended in ranger.apps - - The user can decide what program to run, and if he uses eg. 'vim', the - function app_vim() will be called. However, usually the user - simply wants to "start" the file without specific instructions. - In such a case, app_default() is called, where you should examine - the context and decide which program to use. - - All app functions have a name starting with app_ and return a string - containing the whole command or a tuple containing a list of the - arguments. They are supplied with one argument, which is the - AppContext instance. - - You should define at least app_default, app_pager and app_editor since - internal functions depend on those. Here are sample implementations: - - def app_default(self, context): - if context.file.media: - if context.file.video: - # detach videos from the filemanager - context.flags += 'd' - return self.app_mplayer(context) - else: - return self.app_editor(context) - - def app_pager(self, context): - return 'less', context - - def app_editor(self, context): - return ('vim', context) - """ - - def _meets_dependencies(self, fnc): - try: - deps = fnc.dependencies - except AttributeError: - return True - - for dep in deps: - if dep == 'X': - if 'DISPLAY' not in os.environ or not os.environ['DISPLAY']: - return False - continue - if hasattr(dep, 'dependencies') \ - and not self._meets_dependencies(dep): - return False - if dep not in get_executables(): - return False - - return True - - def either(self, context, *args): - for app in args: - try: - application_handler = getattr(self, 'app_' + app) - except AttributeError: - if app in get_executables(): - return _generic_app(app, context) - continue - if self._meets_dependencies(application_handler): - return application_handler(context) - - def app_self(self, context): - """Run the file itself""" - return "./" + context.file.basename - - def get(self, app): - """Looks for an application, returns app_default if it doesn't exist""" - try: - return getattr(self, 'app_' + app) - except AttributeError: - return self.app_default - - def apply(self, app, context): - if not app: - app = 'default' - try: - handler = getattr(self, 'app_' + app) - except AttributeError: - if app in get_executables(): - return [app] + list(context) - handler = self.app_default - arguments = handler(context) - # flatten - if isinstance(arguments, str): - return arguments - if arguments is None: - return None - result = [] - for obj in arguments: - if isinstance(obj, (tuple, list, Context)): - result.extend(obj) - else: - result.append(obj) - return result - - def has(self, app): - """Returns whether an application is defined""" - return hasattr(self, 'app_' + app) - - def all(self): - """Returns a list with all application functions""" - result = set() - # go through all the classes in the mro (method resolution order) - # so subclasses will return the apps of their superclasses. - for cls in self.__class__.__mro__: - result |= set(m[4:] for m in cls.__dict__ if m.startswith('app_')) - return sorted(result) - - @classmethod - def generic(cls, *args, **keywords): - flags = 'flags' in keywords and keywords['flags'] or "" - deps = 'deps' in keywords and keywords['deps'] or () - for name in args: - assert isinstance(name, str) - if not hasattr(cls, "app_" + name): - fnc = _generic_wrapper(name, flags=flags) - fnc = depends_on(*deps)(fnc) - setattr(cls, "app_" + name, fnc) - - -def tup(*args): - """ - This helper function creates a tuple out of the arguments. - - ('a', ) + tuple(some_iterator) - is equivalent to: - tup('a', *some_iterator) - """ - return args - - -def depends_on(*args): - args = tuple(flatten(args)) - def decorator(fnc): - try: - fnc.dependencies += args - except: - fnc.dependencies = args - return fnc - return decorator - - -def _generic_app(name, context, flags=''): - """Use this function when no other information is given""" - context.flags += flags - return name, context - - -def _generic_wrapper(name, flags=''): - """Wraps _generic_app into a method for Applications""" - assert isinstance(name, str) - return depends_on(name)(lambda self, context: - _generic_app(name, context, flags)) diff --git a/ranger/api/commands.py b/ranger/api/commands.py index a2501c7f..aaadde5d 100644 --- a/ranger/api/commands.py +++ b/ranger/api/commands.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. # TODO: Add an optional "!" to all commands and set a flag if it's there @@ -35,9 +23,15 @@ class CommandContainer(object): def __getitem__(self, key): return self.commands[key] - def alias(self, new, old): + def alias(self, name, full_command): try: - self.commands[new] = self.commands[old] + cmd = type(name, (AliasCommand, ), dict()) + cmd._based_function = name + cmd._function_name = name + cmd._object_name = name + cmd._line = full_command + self.commands[name] = cmd + except: pass @@ -185,7 +179,7 @@ class Command(FileManagerAware): def _tab_only_directories(self): from os.path import dirname, basename, expanduser, join - cwd = self.fm.env.cwd.path + cwd = self.fm.thisdir.path rel_dest = self.rest(1) @@ -231,7 +225,7 @@ class Command(FileManagerAware): def _tab_directory_content(self): from os.path import dirname, basename, expanduser, join - cwd = self.fm.env.cwd.path + cwd = self.fm.thisdir.path rel_dest = self.rest(1) @@ -329,3 +323,11 @@ class FunctionCommand(Command): self.fm.notify("Bad arguments for %s.%s: %s, %s" % (self._object_name, self._function_name, repr(args), repr(keywords)), bad=True) + +class AliasCommand(Command): + _based_function = None + _object_name = "" + _function_name = "unknown" + _line = "" + def execute(self): + self.fm.execute_console(self._line) diff --git a/ranger/api/options.py b/ranger/api/options.py index e2558ffb..38e5f760 100644 --- a/ranger/api/options.py +++ b/ranger/api/options.py @@ -1,18 +1,7 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. +# THIS WHOLE FILE IS OBSOLETE AND EXISTS FOR BACKWARDS COMPATIBILITIY import re from re import compile as regexp from ranger.api import * diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index fb46dd43..aed6812a 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * @@ -114,4 +102,10 @@ class Default(ColorScheme): if context.selected: attr |= reverse + if context.loaded: + if context.selected: + fg = green + else: + bg = green + return fg, bg, attr diff --git a/ranger/colorschemes/default88.py b/ranger/colorschemes/default88.py index 8bf33807..2a2d4825 100644 --- a/ranger/colorschemes/default88.py +++ b/ranger/colorschemes/default88.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The default colorscheme, using 88 colors. diff --git a/ranger/colorschemes/jungle.py b/ranger/colorschemes/jungle.py index ea2e0d94..4f2b30ba 100644 --- a/ranger/colorschemes/jungle.py +++ b/ranger/colorschemes/jungle.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.gui.color import * from ranger.colorschemes.default import Default diff --git a/ranger/colorschemes/snow.py b/ranger/colorschemes/snow.py index a8125ee6..c5940cc8 100644 --- a/ranger/colorschemes/snow.py +++ b/ranger/colorschemes/snow.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.gui.colorscheme import ColorScheme from ranger.gui.color import * diff --git a/ranger/defaults/__init__.py b/ranger/config/__init__.py index 71df3cb3..71df3cb3 100644 --- a/ranger/defaults/__init__.py +++ b/ranger/config/__init__.py diff --git a/ranger/defaults/commands.py b/ranger/config/commands.py index a89dd0f7..2eff68e6 100644 --- a/ranger/defaults/commands.py +++ b/ranger/config/commands.py @@ -54,16 +54,16 @@ # self.fm.notify(string): Print the given string on the screen. # self.fm.notify(string, bad=True): Print the given string in RED. # self.fm.reload_cwd(): Reload the current working directory. -# self.fm.env.cwd: The current working directory. (A File object.) -# self.fm.env.cf: The current file. (A File object too.) -# self.fm.env.cwd.get_selection(): A list of all selected files. +# self.fm.thisdir: The current working directory. (A File object.) +# self.fm.thisfile: The current file. (A File object too.) +# self.fm.thistab.get_selection(): A list of all selected files. # self.fm.execute_console(string): Execute the string as a ranger command. # self.fm.open_console(string): Open the console with the given string # already typed in for you. # self.fm.move(direction): Moves the cursor in the given direction, which # can be something like down=3, up=5, right=1, left=1, to=6, ... # -# File objects (for example self.fm.env.cf) have these useful attributes and +# File objects (for example self.fm.thisfile) have these useful attributes and # methods: # # cf.path: The path to the file. @@ -86,11 +86,15 @@ class alias(Command): Copies the oldcommand as newcommand. """ + + context = 'browser' + resolve_macros = False + def execute(self): if not self.arg(1) or not self.arg(2): self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True) else: - self.fm.commands.alias(self.arg(1), self.arg(2)) + self.fm.commands.alias(self.arg(1), self.rest(2)) class cd(Command): """ @@ -122,7 +126,7 @@ class cd(Command): def tab(self): from os.path import dirname, basename, expanduser, join - cwd = self.fm.env.cwd.path + cwd = self.fm.thisdir.path rel_dest = self.rest(1) bookmarks = [v.path for v in self.fm.bookmarks.dct.values() @@ -218,7 +222,7 @@ class shell(Command): return (start + program + ' ' for program \ in get_executables() if program.startswith(command)) if position_of_last_space == len(command) - 1: - selection = self.fm.env.get_selection() + selection = self.fm.thistab.get_selection() if len(selection) == 1: return self.line + selection[0].shell_escaped_basename + ' ' else: @@ -226,14 +230,14 @@ class shell(Command): else: before_word, start_of_word = self.line.rsplit(' ', 1) return (before_word + ' ' + file.shell_escaped_basename \ - for file in self.fm.env.cwd.files \ + for file in self.fm.thisdir.files \ if file.shell_escaped_basename.startswith(start_of_word)) class open_with(Command): def execute(self): app, flags, mode = self._get_app_flags_mode(self.rest(1)) self.fm.execute_file( - files = [f for f in self.fm.env.cwd.get_selection()], + files = [f for f in self.fm.thistab.get_selection()], app = app, flags = flags, mode = mode) @@ -311,18 +315,8 @@ class open_with(Command): return app, flags, int(mode) - def tab(self): - data = self.rest(1) - if ' ' not in data: - all_apps = self.fm.apps.all() - if all_apps: - return (self.firstpart + app for app in all_apps if app.startswith(data)) - - return None - def _is_app(self, arg): - return self.fm.apps.has(arg) or \ - (not self._is_flags(arg) and arg in get_executables()) + return not self._is_flags(arg) and not arg.isdigit() def _is_flags(self, arg): return all(x in ALLOWED_FLAGS for x in arg) @@ -352,7 +346,7 @@ class find(Command): def quick(self): self.count = 0 - cwd = self.fm.env.cwd + cwd = self.fm.thisdir arg = self.rest(1) if not arg: return False @@ -375,7 +369,7 @@ class find(Command): self.count += 1 if self.count == 1: cwd.move(to=(cwd.pointer + i) % len(cwd.files)) - self.fm.env.cf = cwd.pointed_obj + self.fm.thisfile = cwd.pointed_obj if self.count > 1: return False i += 1 @@ -496,8 +490,8 @@ class delete(Command): # user did not confirm deletion return - cwd = self.fm.env.cwd - cf = self.fm.env.cf + cwd = self.fm.thisdir + cf = self.fm.thisfile if cwd.marked_items or (cf.is_directory and not cf.is_link \ and len(os.listdir(cf.path)) > 0): @@ -519,7 +513,7 @@ class mark(Command): def execute(self): import re - cwd = self.fm.env.cwd + cwd = self.fm.thisdir input = self.rest(1) searchflags = re.UNICODE if input.lower() == input: # "smartcase" @@ -565,7 +559,7 @@ class load_copy_buffer(Command): except: return self.fm.notify("Cannot open %s" % \ (fname or self.copy_buffer_filename), bad=True) - self.fm.env.copy = set(File(g) \ + self.fm.copy_buffer = set(File(g) \ for g in f.read().split("\n") if exists(g)) f.close() self.fm.ui.redraw_main_column() @@ -586,7 +580,7 @@ class save_copy_buffer(Command): except: return self.fm.notify("Cannot open %s" % \ (fname or self.copy_buffer_filename), bad=True) - f.write("\n".join(f.path for f in self.fm.env.copy)) + f.write("\n".join(f.path for f in self.fm.copy_buffer)) f.close() @@ -610,7 +604,7 @@ class mkdir(Command): from os.path import join, expanduser, lexists from os import mkdir - dirname = join(self.fm.env.cwd.path, expanduser(self.rest(1))) + dirname = join(self.fm.thisdir.path, expanduser(self.rest(1))) if not lexists(dirname): mkdir(dirname) else: @@ -627,7 +621,7 @@ class touch(Command): def execute(self): from os.path import join, expanduser, lexists - fname = join(self.fm.env.cwd.path, expanduser(self.rest(1))) + fname = join(self.fm.thisdir.path, expanduser(self.rest(1))) if not lexists(fname): open(fname, 'a').close() else: @@ -643,7 +637,7 @@ class edit(Command): def execute(self): if not self.arg(1): - self.fm.edit_file(self.fm.env.cf.path) + self.fm.edit_file(self.fm.thisfile.path) else: self.fm.edit_file(self.rest(1)) @@ -661,7 +655,7 @@ class eval_(Command): Examples: :eval fm - :eval len(fm.env.directories) + :eval len(fm.directories) :eval p("Hello World!") """ name = 'eval' @@ -708,16 +702,16 @@ class rename(Command): if not new_name: return self.fm.notify('Syntax: rename <newname>', bad=True) - if new_name == self.fm.env.cf.basename: + if new_name == self.fm.thisfile.basename: return if access(new_name, os.F_OK): return self.fm.notify("Can't rename: file already exists!", bad=True) - self.fm.rename(self.fm.env.cf, new_name) + self.fm.rename(self.fm.thisfile, new_name) f = File(new_name) - self.fm.env.cwd.pointed_obj = f - self.fm.env.cf = f + self.fm.thisdir.pointed_obj = f + self.fm.thisfile = f def tab(self): return self._tab_directory_content() @@ -749,7 +743,7 @@ class chmod(Command): self.fm.notify("Need an octal number between 0 and 777!", bad=True) return - for file in self.fm.env.get_selection(): + for file in self.fm.thistab.get_selection(): try: os.chmod(file.path, mode) except Exception as ex: @@ -758,7 +752,7 @@ class chmod(Command): try: # reloading directory. maybe its better to reload the selected # files only. - self.fm.env.cwd.load_content() + self.fm.thisdir.load_content() except: pass @@ -782,7 +776,7 @@ class bulkrename(Command): py3 = sys.version > "3" # Create and edit the file list - filenames = [f.basename for f in self.fm.env.get_selection()] + filenames = [f.basename for f in self.fm.thistab.get_selection()] listfile = tempfile.NamedTemporaryFile() if py3: @@ -829,7 +823,7 @@ class relink(Command): from ranger.fsobject import File new_path = self.rest(1) - cf = self.fm.env.cf + cf = self.fm.thisfile if not new_path: return self.fm.notify('Syntax: relink <newpath>', bad=True) @@ -847,12 +841,12 @@ class relink(Command): self.fm.notify(err) self.fm.reset() - self.fm.env.cwd.pointed_obj = cf - self.fm.env.cf = cf + self.fm.thisdir.pointed_obj = cf + self.fm.thisfile = cf def tab(self): if not self.rest(1): - return self.line+os.readlink(self.fm.env.cf.path) + return self.line+os.readlink(self.fm.thisfile.path) else: return self._tab_directory_content() @@ -887,7 +881,7 @@ class copymap(Command): return self.notify("Not enough arguments", bad=True) for arg in self.args[2:]: - self.fm.env.keymaps.copy(self.context, self.arg(1), arg) + self.fm.ui.keymaps.copy(self.context, self.arg(1), arg) class copypmap(copymap): @@ -923,7 +917,7 @@ class unmap(Command): def execute(self): for arg in self.args[1:]: - self.fm.env.keymaps.unbind(self.context, arg) + self.fm.ui.keymaps.unbind(self.context, arg) class cunmap(unmap): @@ -964,7 +958,7 @@ class map_(Command): resolve_macros = False def execute(self): - self.fm.env.keymaps.bind(self.context, self.arg(1), self.rest(2)) + self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2)) class cmap(map_): @@ -1015,5 +1009,5 @@ class grep(Command): if self.rest(1): action = ['grep', '--color=always', '--line-number'] action.extend(['-e', self.rest(1), '-r']) - action.extend(f.path for f in self.fm.env.get_selection()) + action.extend(f.path for f in self.fm.thistab.get_selection()) self.fm.execute_command(action, flags='p') diff --git a/ranger/defaults/options.py b/ranger/config/options.py index 5e30f042..681feabc 100644 --- a/ranger/defaults/options.py +++ b/ranger/config/options.py @@ -14,8 +14,6 @@ # But make sure you update your configs when you update ranger. # =================================================================== -from ranger.api.options import * - # Load the deault rc.conf file? If you've copied it to your configuration # direcory, then you should deactivate this option. load_default_rc = True @@ -25,8 +23,7 @@ column_ratios = (1, 3, 4) # Which files should be hidden? Toggle this by typing `zh' or # changing the setting `show_hidden' -hidden_filter = regexp( - r'^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^__(py)?cache__$') +hidden_filter = r'^\.|\.(?:pyc|pyo|bak|swp)$|^lost\+found$|^__(py)?cache__$' show_hidden = False # Which script is used to generate file previews? diff --git a/ranger/defaults/rc.conf b/ranger/config/rc.conf index 77ffa5c3..1b344465 100644 --- a/ranger/defaults/rc.conf +++ b/ranger/config/rc.conf @@ -53,7 +53,7 @@ map ! console shell map @ console -p6 shell %%s map # console shell -p map s console shell -map r console open_with +map r chain draw_possible_programs; console open_with map f console find map cd console cd @@ -139,8 +139,8 @@ map yn shell -d echo -n %f | xsel -i map = chmod map cw console rename -map A eval fm.open_console('rename ' + fm.env.cf.basename) -map I eval fm.open_console('rename ' + fm.env.cf.basename, position=7) +map A eval fm.open_console('rename ' + fm.thisfile.basename) +map I eval fm.open_console('rename ' + fm.thisfile.basename, position=7) map pp paste map po paste overwrite=True @@ -191,6 +191,7 @@ map gt tab_move 1 map gT tab_move -1 map gn tab_new ~ map gc tab_close +map uq tab_restore map <a-1> tab_open 1 map <a-2> tab_open 2 map <a-3> tab_open 3 @@ -244,13 +245,18 @@ map um<any> unset_bookmark %any map m<bg> draw_bookmarks copymap m<bg> um<bg> `<bg> '<bg> -# Beware. I haven't figured out how to make these keybindings pretty yet: - -# map +ow shell -d chmod o+w (one mapping for each combination) -eval import itertools; [cmd("map +%s%s shell -d chmod %s+%s %%s" % (mode+mode)) for mode in itertools.product("ugoa", "rwxXst")] - -# map -ow shell -d chmod o+w (one mapping for each combination) -eval import itertools; [cmd("map -%s%s shell -d chmod %s-%s %%s" % (mode+mode)) for mode in itertools.product("ugoa", "rwxXst")] +# Generate all the chmod bindings with some python help: +eval for arg in "rwxXst": cmd("map +u{0} shell -d chmod u+{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map +g{0} shell -d chmod g+{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map +o{0} shell -d chmod o+{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map +a{0} shell -d chmod a+{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map +{0} shell -d chmod u+{0} %s".format(arg)) + +eval for arg in "rwxXst": cmd("map -u{0} shell -d chmod u-{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map -g{0} shell -d chmod g-{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map -o{0} shell -d chmod o-{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map -a{0} shell -d chmod a-{0} %s".format(arg)) +eval for arg in "rwxXst": cmd("map -{0} shell -d chmod u-{0} %s".format(arg)) # =================================================================== # == Define keys for the console diff --git a/ranger/config/rifle.conf b/ranger/config/rifle.conf new file mode 100644 index 00000000..e4528875 --- /dev/null +++ b/ranger/config/rifle.conf @@ -0,0 +1,195 @@ +# vim: ft=cfg +# +# This is the configuration file of "rifle", ranger's file executor/opener. +# Each line consists of conditions and a command. For each line the conditions +# are checked and if they are met, the respective command is run. +# +# Syntax: +# <condition1> , <condition2> , ... = command +# +# The command can contain these environment variables: +# $1-$9 | The n-th selected file +# $@ | All selected files +# +# If you use the special command "ask", rifle will ask you what program to run. +# +# Prefixing a condition with "!" will negate its result. +# These conditions are currently supported: +# match <regexp> | The regexp matches $1 +# ext <regexp> | The regexp matches the extension of $1 +# mime <regexp> | The regexp matches the mime type of $1 +# name <regexp> | The regexp matches the basename of $1 +# path <regexp> | The regexp matches the absolute path of $1 +# has <program> | The program is installed (i.e. located in $PATH) +# file | $1 is a file +# directory | $1 is a directory +# number <n> | change the number of this command to n +# terminal | stdin, stderr and stdout are connected to a terminal +# X | $DISPLAY is not empty (i.e. Xorg runs) +# +# There are also pseudo-conditions which have a "side effect": +# flag <flags> | Change how the program is run. See below. +# label <label> | Assign a label or name to the command so it can +# | be started with :open_with <label> in ranger +# | or `rifle -p <label>` in the standalone executable. +# else | Always true. +# +# Flags are single characters which slightly transform the command: +# f | Fork the program, make it run in the background. +# | New command = setsid $command >& /dev/null & +# r | Execute the command with root permissions +# | New command = sudo $command +# t | Run the program in a new terminal. If $TERMCMD is not defined, +# | rifle will attempt to extract it from $TERM. +# | New command = $TERMCMD -e $command + +#------------------------------------------- +# Websites +#------------------------------------------- +# Rarely installed browsers get higher priority; It is assumed that if you +# install a rare browser, you probably use it. Firefox/konqueror/w3m on the +# other hand are often only installed as fallback browsers. +ext x?html?, has surf, X, flag f = surf -- file://"$1" +ext x?html?, has vimprobable, X, flag f = vimprobable -- "$@" +ext x?html?, has vimprobable2, X, flag f = vimprobable2 -- "$@" +ext x?html?, has jumanji, X, flag f = jumanji -- "$@" +ext x?html?, has luakit, X, flag f = luakit -- "$@" +ext x?html?, has uzbl, X, flag f = uzbl -- "$@" +ext x?html?, has uzbl-browser, X, flag f = uzbl-browser -- "$@" +ext x?html?, has uzbl-core, X, flag f = uzbl-core -- "$@" +ext x?html?, has midori, X, flag f = midori -- "$@" +ext x?html?, has opera, X, flag f = opera -- "$@" +ext x?html?, has firefox, X, flag f = firefox -- "$@" +ext x?html?, has seamonkey, X, flag f = seamonkey -- "$@" +ext x?html?, has iceweasel, X, flag f = iceweasel -- "$@" +ext x?html?, has epiphany, X, flag f = epiphany -- "$@" +ext x?html?, has konqueror, X, flag f = konqueror -- "$@" +ext x?html?, has elinks, terminal = elinks "$@" +ext x?html?, has links2, terminal = links2 "$@" +ext x?html?, has links, terminal = links "$@" +ext x?html?, has lynx, terminal = lynx -- "$@" +ext x?html?, has w3m, terminal = w3m "$@" + +#------------------------------------------- +# Misc +#------------------------------------------- +# Define the "editor" for text files as first action +mime ^text, label editor = "$EDITOR" -- "$@" +mime ^text, label pager = "$PAGER" -- "$@" +!mime ^text, label editor, ext xml|csv|tex|py|pl|rb|sh|php = "$EDITOR" -- "$@" +!mime ^text, label editor, ext xml|csv|tex|py|pl|rb|sh|php = "$PAGER" -- "$@" + +ext 1 = man "$1" +ext s[wmf]c, has zsnes, X = zsnes "$1" +ext nes, has fceux, X = fceux "$1" +ext exe = wine "$1" +name ^[mM]akefile$ = make + +#-------------------------------------------- +# Code +#------------------------------------------- +ext py = python -- "$1" +ext pl = perl -- "$1" +ext rb = ruby -- "$1" +ext sh = bash -- "$1" +ext php = php -- "$1" + +#-------------------------------------------- +# Audio without X +#------------------------------------------- +mime ^audio|ogg$, terminal, has mplayer = mplayer -- "$@" +mime ^audio|ogg$, terminal, has mplayer2 = mplayer2 -- "$@" +ext midi?, terminal, has wildmidi = wildmidi -- "$@" + +#-------------------------------------------- +# Video/Audio with a GUI +#------------------------------------------- +mime ^video|audio, has gmplayer, X, flag f = gmplayer -- "$@" +mime ^video|audio, has smplayer, X, flag f = smplayer "$@" +mime ^video, has mplayer2, X, flag f = mplayer2 -- "$@" +mime ^video, has mplayer2, X, flag f = mplayer2 -fs -- "$@" +mime ^video, has mplayer, X, flag f = mplayer -- "$@" +mime ^video, has mplayer, X, flag f = mplayer -fs -- "$@" +mime ^video|audio, has vlc, X, flag f = vlc -- "$@" +mime ^video|audio, has totem, X, flag f = totem -- "$@" +mime ^video|audio, has totem, X, flag f = totem --fullscreen -- "$@" + +# MKV videos are sometimes not recognized as video/x-matroska +!mime ^video|audio, ext mkv, has gmplayer, X, flag f = gmplayer -- "$@" +!mime ^video|audio, ext mkv, has smplayer, X, flag f = smplayer "$@" +!mime ^video, ext mkv, has mplayer2, X, flag f = mplayer2 -- "$@" +!mime ^video, ext mkv, has mplayer2, X, flag f = mplayer2 -fs -- "$@" +!mime ^video, ext mkv, has mplayer, X, flag f = mplayer -- "$@" +!mime ^video, ext mkv, has mplayer, X, flag f = mplayer -fs -- "$@" +!mime ^video|audio, ext mkv, has vlc, X, flag f = vlc -- "$@" +!mime ^video|audio, ext mkv, has totem, X, flag f = totem -- "$@" +!mime ^video|audio, ext mkv, has totem, X, flag f = totem --fullscreen -- "$@" + +#-------------------------------------------- +# Video without X: +#------------------------------------------- +mime ^video, terminal, !X, has mplayer2 = mplayer2 -- "$@" +mime ^video, terminal, !X, has mplayer = mplayer -- "$@" +!mime ^video, ext mkv, terminal, !X, has mplayer2 = mplayer2 -- "$@" +!mime ^video, ext mkv, terminal, !X, has mplayer = mplayer -- "$@" + +#------------------------------------------- +# Image Viewing: +#------------------------------------------- +mime ^image, has eog, X, flag f = eog -- "$@" +mime ^image, has sxiv, X, flag f = sxiv -- "$@" +mime ^image, has feh, X, flag f = feh -- "$@" +mime ^image, has mirage, X, flag f = mirage -- "$@" +mime ^image, has gimp, X, flag f = gimp -- "$@" +ext xcf, X, flag f = gimp -- "$@" + +#------------------------------------------- +# Documents +#------------------------------------------- +ext pdf, has llpp, X, flag f = llpp "$@" +ext pdf, has zathura, X, flag f = zathura -- "$@" +ext pdf, has mupdf, X, flag f = mupdf -- "$@" +ext pdf, has apvlv, X, flag f = apvlv -- "$@" +ext pdf, has xpdf, X, flag f = xpdf -- "$@" +ext pdf, has evince, X, flag f = evince -- "$@" +ext pdf, has okular, X, flag f = okular -- "$@" +ext pdf, has epdfview, X, flag f = epdfview -- "$@" + +ext sxc|xlsx?|xlt|xlw|gnm|gnumeric, has gnumeric, X, flag f = gnumeric -- "$@" +ext sxc|xlsx?|xlt|xlw|gnm|gnumeric, has kspread, X, flag f = kspread -- "$@" +ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has libreoffice, X, flag f = libreoffice "$@" +ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has soffice, X, flag f = soffice "$@" +ext od[dfgpst]|sxc|xlsx?|xlt|xlw|gnm|gnumeric, has ooffice, X, flag f = ooffice "$@" + +ext docx?, has catdoc, terminal = catdoc -- "$@" | "$PAGER" +ext docx?, has libreoffice, X, flag f = libreoffice -- "$@" +ext docx?, has soffice, X, flag f = soffice -- "$@" +ext docx?, has ooffice, X, flag f = ooffice -- "$@" + +ext djvu, has evince, X, flag f = evince -- "$@" + +#------------------------------------------- +# Archives +#------------------------------------------- +# This requires atool +ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz, has als = als -- "$@" | "$PAGER" +ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has als = als -- "$@" | "$PAGER" +ext 7z|ace|ar|arc|bz2?|cab|cpio|cpt|deb|dgc|dmg|gz, has aunpack = aunpack -- "$@" +ext iso|jar|msi|pkg|rar|shar|tar|tgz|xar|xpi|xz|zip, has aunpack = aunpack -- "$@" + +# Fallback: +ext tar|gz, has tar = tar vvtf "$@" | "$PAGER" +ext tar|gz, has tar = tar vvxf "$@" + +#------------------------------------------- +# Misc +#------------------------------------------- +label wallpaper, number 11, mime ^image, X = feh --bg-scale "$1" +label wallpaper, number 12, mime ^image, X = feh --bg-tile "$1" +label wallpaper, number 13, mime ^image, X = feh --bg-center "$1" +label wallpaper, number 14, mime ^image, X = feh --bg-fill "$1" + +# Define the editor for non-text files + pager as last action + !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php = ask +label editor, !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php = "$EDITOR" -- "$@" +label pager, !mime ^text, !ext xml|csv|tex|py|pl|rb|sh|php = "$PAGER" -- "$@" diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index 750515c5..4d692989 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import string import re diff --git a/ranger/container/history.py b/ranger/container/history.py index dd511d0e..8ba092bc 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -1,17 +1,7 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. + +# TODO: rewrite to use deque instead of list class HistoryEmptyException(Exception): pass @@ -61,6 +51,14 @@ class History(object): except IndexError: self.add(item) + def rebase(self, other_history): + assert isinstance(other_history, History) + index_offset = len(self._history) - self._index + self._history[:self._index] = list(other_history._history) + if len(self._history) > self.maxlen: + self._history = self._history[-self.maxlen:] + self._index = len(self._history) - index_offset + def __len__(self): return len(self._history) diff --git a/ranger/container/settingobject.py b/ranger/container/settingobject.py index e7ded15e..cbc56278 100644 --- a/ranger/container/settingobject.py +++ b/ranger/container/settingobject.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from inspect import isfunction from ranger.ext.signals import SignalDispatcher diff --git a/ranger/container/tags.py b/ranger/container/tags.py index c08abdaa..7212345e 100644 --- a/ranger/container/tags.py +++ b/ranger/container/tags.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from os.path import isdir, exists, dirname, abspath, realpath, expanduser import string diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 4e72de77..2f76527a 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import codecs import os @@ -29,8 +17,10 @@ from ranger.ext.relative_symlink import relative_symlink from ranger.ext.keybinding_parser import key_to_string, construct_keybinding from ranger.ext.shell_escape import shell_quote from ranger.ext.next_available_filename import next_available_filename +from ranger.ext.rifle import squash_flags, ASK_COMMAND from ranger.core.shared import FileManagerAware, EnvironmentAware, \ SettingsAware +from ranger.core.tab import Tab from ranger.fsobject import File from ranger.core.loader import CommandLoader @@ -41,13 +31,6 @@ class _MacroTemplate(string.Template): delimiter = ranger.MACRO_DELIMITER class Actions(FileManagerAware, EnvironmentAware, SettingsAware): - search_method = 'ctime' - mode = 'normal' # either 'normal' or 'visual'. - _visual_reverse = False - _visual_start = None - _visual_start_pos = None - _previous_selection = None - # -------------------------- # -- Basic Commands # -------------------------- @@ -58,9 +41,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def reset(self): """Reset the filemanager, clearing the directory buffer""" - old_path = self.env.cwd.path + old_path = self.thisdir.path + self.restorable_tabs = {} self.previews = {} - self.env.garbage_collect(-1, self.tabs) + self.garbage_collect(-1) self.enter_dir(old_path) self.change_mode('normal') @@ -68,9 +52,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if mode == self.mode: return if mode == 'visual': - self._visual_start = self.env.cwd.pointed_obj - self._visual_start_pos = self.env.cwd.pointer - self._previous_selection = set(self.env.cwd.marked_items) + self._visual_start = self.thisdir.pointed_obj + self._visual_start_pos = self.thisdir.pointer + self._previous_selection = set(self.thisdir.marked_items) self.mark_files(val=not self._visual_reverse, movedown=False) elif mode == 'normal': if self.mode == 'visual': @@ -91,7 +75,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def reload_cwd(self): try: - cwd = self.env.cwd + cwd = self.thisdir except: pass cwd.unload() @@ -120,7 +104,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.loader.remove(index=0) def get_cumulative_size(self): - for f in self.env.get_selection() or (): + for f in self.thistab.get_selection() or (): f.look_up_cumulative_size() self.ui.status.request_redraw() self.ui.redraw_main_column() @@ -186,74 +170,76 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): macros['rangerdir'] = ranger.RANGERDIR - if self.fm.env.cf: - macros['f'] = self.fm.env.cf.basename + if self.fm.thisfile: + macros['f'] = self.fm.thisfile.basename else: macros['f'] = MACRO_FAIL - if self.fm.env.get_selection: - macros['s'] = [fl.basename for fl in self.fm.env.get_selection()] + if self.fm.thistab.get_selection: + macros['s'] = [fl.basename for fl in self.fm.thistab.get_selection()] else: macros['s'] = MACRO_FAIL - if self.fm.env.copy: - macros['c'] = [fl.path for fl in self.fm.env.copy] + if self.fm.copy_buffer: + macros['c'] = [fl.path for fl in self.fm.copy_buffer] else: macros['c'] = MACRO_FAIL - if self.fm.env.cwd.files: - macros['t'] = [fl.basename for fl in self.fm.env.cwd.files + if self.fm.thisdir.files: + macros['t'] = [fl.basename for fl in self.fm.thisdir.files if fl.realpath in (self.fm.tags or [])] else: macros['t'] = MACRO_FAIL - if self.fm.env.cwd: - macros['d'] = self.fm.env.cwd.path + if self.fm.thisdir: + macros['d'] = self.fm.thisdir.path else: macros['d'] = '.' # define d/f/s macros for each tab for i in range(1,10): try: - tab_dir_path = self.fm.tabs[i] + tab = self.fm.tabs[i] except: continue - tab_dir = self.fm.env.get_directory(tab_dir_path) + tabdir = tab.thisdir + if not tabdir: + continue i = str(i) - macros[i + 'd'] = tab_dir_path - if tab_dir.get_selection(): - macros[i + 's'] = [fl.path for fl in tab_dir.get_selection()] + macros[i + 'd'] = tabdir.path + if tabdir.get_selection(): + macros[i + 's'] = [fl.path for fl in tabdir.get_selection()] else: macros[i + 's'] = MACRO_FAIL - if tab_dir.pointed_obj: - macros[i + 'f'] = tab_dir.pointed_obj.path + if tabdir.pointed_obj: + macros[i + 'f'] = tabdir.pointed_obj.path else: macros[i + 'f'] = MACRO_FAIL # define D/F/S for the next tab found_current_tab = False - next_tab_path = None + next_tab = None first_tab = None - for tab in self.fm.tabs: + for tabname in self.fm.tabs: if not first_tab: - first_tab = tab + first_tab = tabname if found_current_tab: - next_tab_path = self.fm.tabs[tab] + next_tab = self.fm.tabs[tabname] break - if self.fm.current_tab == tab: + if self.fm.current_tab == tabname: found_current_tab = True - if found_current_tab and not next_tab_path: - next_tab_path = self.fm.tabs[first_tab] - next_tab = self.fm.env.get_directory(next_tab_path) - - if next_tab: - macros['D'] = str(next_tab.path) - if next_tab.pointed_obj: - macros['F'] = next_tab.pointed_obj.path + if found_current_tab and next_tab is None: + next_tab = self.fm.tabs[first_tab] + next_tab_dir = next_tab.thisdir + + if next_tab_dir: + macros['D'] = str(next_tab_dir.path) + if next_tab_dir.pointed_obj: + macros['F'] = next_tab_dir.pointed_obj.path else: macros['F'] = MACRO_FAIL - if next_tab.get_selection(): - macros['S'] = [fl.path for fl in next_tab.get_selection()] + if next_tab_dir.get_selection(): + macros['S'] = [fl.path for fl in next_tab_dir.get_selection()] else: macros['S'] = MACRO_FAIL else: @@ -285,14 +271,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): mode is a positive integer. Both flags and mode specify how the program is run.""" + mode = kw['mode'] if 'mode' in kw else 0 + # ranger can act as a file chooser when running with --choosefile=... - if ('mode' not in kw or kw['mode'] == 0) and 'app' not in kw: + if mode == 0 and 'label' not in kw: if ranger.arg.choosefile: - open(ranger.arg.choosefile, 'w').write(self.fm.env.cf.path) + open(ranger.arg.choosefile, 'w').write(self.fm.thisfile.path) if ranger.arg.choosefiles: open(ranger.arg.choosefiles, 'w').write("".join( - f.path + "\n" for f in self.fm.env.get_selection())) + f.path + "\n" for f in self.fm.thistab.get_selection())) if ranger.arg.choosefile or ranger.arg.choosefiles: raise SystemExit() @@ -302,16 +290,15 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): elif type(files) not in (list, tuple): files = [files] - if 'flags' in kw: - from ranger.core.runner import Context - context = Context(files=list(files), flags=kw['flags']) - context.squash_flags() - if 'c' in context.flags: - files = [self.fm.env.cf] + flags = kw.get('flags', '') + if 'c' in squash_flags(flags): + files = [self.fm.thisfile] self.signal_emit('execute.before', keywords=kw) + filenames = [f.path for f in files] + label = kw.get('label', kw.get('app', None)) try: - return self.run(files=list(files), **kw) + return self.rifle.execute(filenames, mode, label, flags, None) finally: self.signal_emit('execute.after') @@ -335,7 +322,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.move(to=2, pages=True) # moves to page 2. self.move(to=1, percentage=True) # moves to 80% """ - cwd = self.env.cwd + cwd = self.thisdir direction = Direction(kw) if 'left' in direction or direction.left() > 0: steps = direction.left() @@ -345,17 +332,18 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): directory = os.path.join(*(['..'] * steps)) except: return - self.env.enter_dir(directory) + self.thistab.enter_dir(directory) self.change_mode('normal') if cwd and cwd.accessible and cwd.content_loaded: if 'right' in direction: mode = 0 if narg is not None: mode = narg - cf = self.env.cf - selection = self.env.get_selection() - if not self.env.enter_dir(cf) and selection: - if self.execute_file(selection, mode=mode) is False: + cf = self.thisfile + selection = self.thistab.get_selection() + if not self.thistab.enter_dir(cf) and selection: + result = self.execute_file(selection, mode=mode) + if result in (False, ASK_COMMAND): self.open_console('open_with ') elif direction.vertical() and cwd.files: newpos = direction.move( @@ -395,35 +383,36 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.change_mode('normal') if narg is not None: n *= narg - parent = self.env.at_level(-1) + parent = self.thistab.at_level(-1) if parent is not None: if parent.pointer + n < 0: n = 0 - parent.pointer try: - self.env.enter_dir(parent.files[parent.pointer+n]) + self.thistab.enter_dir(parent.files[parent.pointer+n]) except IndexError: pass def select_file(self, path): path = path.strip() if self.enter_dir(os.path.dirname(path)): - self.env.cwd.move_to_obj(path) + self.thisdir.move_to_obj(path) def history_go(self, relative): """Move back and forth in the history""" - self.env.history_go(int(relative)) + self.thistab.history_go(int(relative)) + # TODO: remove this method since it is not used? def scroll(self, relative): """Scroll down by <relative> lines""" if self.ui.browser and self.ui.browser.main_column: self.ui.browser.main_column.scroll(relative) - self.env.cf = self.env.cwd.pointed_obj + self.thisfile = self.thisdir.pointed_obj def enter_dir(self, path, remember=False, history=True): """Enter the directory at the given path""" - cwd = self.env.cwd - result = self.env.enter_dir(path, history=history) - if cwd != self.env.cwd: + cwd = self.thisdir + result = self.thistab.enter_dir(path, history=history) + if cwd != self.thisdir: if remember: self.bookmarks.remember(cwd) self.change_mode('normal') @@ -435,14 +424,14 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def traverse(self): self.change_mode('normal') - cf = self.env.cf - cwd = self.env.cwd + cf = self.thisfile + cwd = self.thisdir if cf is not None and cf.is_directory: self.enter_dir(cf.path) elif cwd.pointer >= len(cwd) - 1: while True: self.move(left=1) - cwd = self.env.cwd + cwd = self.thisdir if cwd.pointer < len(cwd) - 1: break if cwd.path == '/': @@ -463,6 +452,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def taskview_move(self, narg=None, **kw): self.ui.taskview.move(narg=narg, **kw) + def pause_tasks(self): + self.loader.pause(-1) + def pager_close(self): if self.ui.pager.visible: self.ui.close_pager() @@ -479,34 +471,34 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): return self.run(cmd, **kw) def edit_file(self, file=None): - """Calls execute_file with the current file and app='editor'""" + """Calls execute_file with the current file and label='editor'""" if file is None: - file = self.env.cf + file = self.thisfile elif isinstance(file, str): file = File(os.path.expanduser(file)) if file is None: return - self.execute_file(file, app = 'editor') + self.execute_file(file, label='editor') def toggle_option(self, string): """Toggle a boolean option named <string>""" - if isinstance(self.env.settings[string], bool): - self.env.settings[string] ^= True + if isinstance(self.settings[string], bool): + self.settings[string] ^= True def set_option(self, optname, value): """Set the value of an option named <optname>""" - self.env.settings[optname] = value + self.settings[optname] = value def sort(self, func=None, reverse=None): if reverse is not None: - self.env.settings['sort_reverse'] = bool(reverse) + self.settings['sort_reverse'] = bool(reverse) if func is not None: - self.env.settings['sort'] = str(func) + self.settings['sort'] = str(func) def set_filter(self, fltr): try: - self.env.cwd.filter = fltr + self.thisdir.filter = fltr except: pass @@ -520,10 +512,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): val - mark or unmark? """ - if self.env.cwd is None: + if self.thisdir is None: return - cwd = self.env.cwd + cwd = self.thisdir if not cwd.accessible: return @@ -557,10 +549,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.ui.status.need_redraw = True def mark_in_direction(self, val=True, dirarg=None): - cwd = self.env.cwd + cwd = self.thisdir direction = Direction(dirarg) pos, selected = direction.select(lst=cwd.files, current=cwd.pointer, - pagesize=self.env.termsize[0]) + pagesize=self.ui.termsize[0]) cwd.pointer = pos cwd.correct_pointer() for item in selected: @@ -576,7 +568,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): text = re.compile(text, re.L | re.U | re.I) except: return False - self.env.last_search = text + self.thistab.last_search = text self.search_next(order='search', offset=offset) def search_next(self, order=None, offset=1, forward=True): @@ -589,7 +581,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if order in ('search', 'tag'): if order == 'search': - arg = self.env.last_search + arg = self.thistab.last_search if arg is None: return False if hasattr(arg, 'search'): @@ -599,10 +591,10 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): elif order == 'tag': fnc = lambda x: x.realpath in self.tags - return self.env.cwd.search_fnc(fnc=fnc, offset=offset, forward=forward) + return self.thisdir.search_fnc(fnc=fnc, offset=offset, forward=forward) elif order in ('size', 'mimetype', 'ctime', 'mtime', 'atime'): - cwd = self.env.cwd + cwd = self.thisdir if original_order is not None or not cwd.cycle_list: lst = list(cwd.files) if order == 'size': @@ -635,7 +627,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if not self.tags: return if paths is None: - tags = tuple(x.realpath for x in self.env.get_selection()) + tags = tuple(x.realpath for x in self.thistab.get_selection()) else: tags = [realpath(path) for path in paths] if value is True: @@ -668,7 +660,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): try: self.bookmarks.update_if_outdated() destination = self.bookmarks[str(key)] - cwd = self.env.cwd + cwd = self.thisdir if destination.path != cwd.path: self.bookmarks.enter(str(key)) self.bookmarks.remember(cwd) @@ -678,7 +670,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def set_bookmark(self, key): """Set the bookmark with the name <key> to the current directory""" self.bookmarks.update_if_outdated() - self.bookmarks[str(key)] = self.env.cwd + self.bookmarks[str(key)] = self.thisdir def unset_bookmark(self, key): """Delete the bookmark with the name <key>""" @@ -691,6 +683,19 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def hide_bookmarks(self): self.ui.browser.draw_bookmarks = False + def draw_possible_programs(self): + try: + target = self.thistab.get_selection()[0] + except: + self.ui.browser.draw_info = [] + return + programs = self.rifle.list_commands([target.path], None) + programs = ['%s | %s' % program[0:2] for program in programs] + self.ui.browser.draw_info = programs + + def hide_console_info(self): + self.ui.browser.draw_info = False + # -------------------------- # -- Pager # -------------------------- @@ -734,11 +739,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): pager.set_source(["Message Log:", "No messages!"]) def display_file(self): - if not self.env.cf or not self.env.cf.is_file: + if not self.thisfile or not self.thisfile.is_file: return pager = self.ui.open_embedded_pager() - pager.set_source(self.env.cf.get_preview_source(pager.wid, pager.hei)) + pager.set_source(self.thisfile.get_preview_source(pager.wid, pager.hei)) # -------------------------- # -- Previews @@ -802,12 +807,12 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): f.close() else: data[(-1, -1)] = None - if self.env.cf.realpath == path: + if self.thisfile.realpath == path: self.ui.browser.need_redraw = True data['loading'] = False pager = self.ui.browser.pager - if self.env.cf and self.env.cf.is_file: - pager.set_source(self.env.cf.get_preview_source( + if self.thisfile and self.thisfile.is_file: + pager.set_source(self.thisfile.get_preview_source( pager.wid, pager.hei)) def on_destroy(signal): try: @@ -829,23 +834,35 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): # -------------------------- # -- Tabs # -------------------------- - # This implementation of tabs is very simple and keeps track of - # directory paths only. - def tab_open(self, name, path=None): - tab_has_changed = name != self.current_tab + tab_has_changed = (name != self.current_tab) self.current_tab = name - if path or (name in self.tabs): - self.enter_dir(path or self.tabs[name]) + previous_tab = self.thistab + try: + tab = self.tabs[name] + except KeyError: + # create a new tab + if path: + tab = Tab(path) + else: + tab = Tab(self.thistab.path) + self.tabs[name] = tab + self.thistab = tab + tab.enter_dir(tab.path, history=True) + if previous_tab: + tab.inherit_history(previous_tab.history) else: - self._update_current_tab() + self.thistab = tab + tab.enter_dir(tab.path, history=False) + if tab_has_changed: self.change_mode('normal') - self.signal_emit('tab.change') + self.signal_emit('tab.change', old=previous_tab, new=self.thistab) def tab_close(self, name=None): if name is None: name = self.current_tab + tab = self.tabs[name] if name == self.current_tab: direction = -1 if name == self._get_tab_list()[-1] else 1 previous = self.current_tab @@ -854,6 +871,23 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): return # can't close last tab if name in self.tabs: del self.tabs[name] + self.restorable_tabs.append(tab) + + def tab_restore(self): + # NOTE: The name of the tab is not restored. + previous_tab = self.thistab + if self.restorable_tabs: + tab = self.restorable_tabs.pop() + for name in range(1, len(self.tabs) + 2): + if not name in self.tabs: + self.current_tab = name + self.tabs[name] = tab + tab.enter_dir(tab.path, history=False) + self.thistab = tab + self.change_mode('normal') + self.signal_emit('tab.change', old=previous_tab, + new=self.thistab) + break def tab_move(self, offset): assert isinstance(offset, int) @@ -873,9 +907,6 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): assert len(self.tabs) > 0, "There must be >=1 tabs at all times" return sorted(self.tabs) - def _update_current_tab(self): - self.tabs[self.current_tab] = self.env.cwd.path - # -------------------------- # -- Overview of internals # -------------------------- @@ -898,14 +929,15 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): for context in contexts: write("Keybindings in `%s'\n" % context) - if context in self.env.keymaps: - recurse([], self.env.keymaps[context]) + if context in self.fm.ui.keymaps: + recurse([], self.fm.ui.keymaps[context]) else: write(" None\n") write("\n") temporary_file.flush() - self.run(app='pager', files=[File(temporary_file.name)]) + pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) + self.run([pager, temporary_file.name]) def dump_commands(self): temporary_file = tempfile.NamedTemporaryFile() @@ -927,7 +959,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): write(" :%s\n" % cmd.get_name()) temporary_file.flush() - self.run(app='pager', files=[File(temporary_file.name)]) + pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) + self.run([pager, temporary_file.name]) def dump_settings(self): from ranger.container.settingobject import ALLOWED_SETTINGS @@ -939,23 +972,24 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): write("%30s = %s\n" % (setting, getattr(self.settings, setting))) temporary_file.flush() - self.run(app='pager', files=[File(temporary_file.name)]) + pager = os.environ.get('PAGER', ranger.DEFAULT_PAGER) + self.run([pager, temporary_file.name]) # -------------------------- # -- File System Operations # -------------------------- def uncut(self): - self.env.copy = set() - self.env.cut = False + self.copy_buffer = set() + self.do_cut = False self.ui.browser.main_column.request_redraw() def copy(self, mode='set', narg=None, dirarg=None): """Copy the selected items. Modes are: 'set', 'add', 'remove'.""" assert mode in ('set', 'add', 'remove') - cwd = self.env.cwd + cwd = self.thisdir if not narg and not dirarg: - selected = (f for f in self.env.get_selection() if f in cwd.files) + selected = (f for f in self.thistab.get_selection() if f in cwd.files) else: if not dirarg and narg: direction = Direction(down=1) @@ -965,25 +999,25 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): offset = 1 pos, selected = direction.select( override=narg, lst=cwd.files, current=cwd.pointer, - pagesize=self.env.termsize[0], offset=offset) + pagesize=self.ui.termsize[0], offset=offset) cwd.pointer = pos cwd.correct_pointer() if mode == 'set': - self.env.copy = set(selected) + self.copy_buffer = set(selected) elif mode == 'add': - self.env.copy.update(set(selected)) + self.copy_buffer.update(set(selected)) elif mode == 'remove': - self.env.copy.difference_update(set(selected)) - self.env.cut = False + self.copy_buffer.difference_update(set(selected)) + self.do_cut = False self.ui.browser.main_column.request_redraw() def cut(self, mode='set', narg=None, dirarg=None): self.copy(mode=mode, narg=narg, dirarg=dirarg) - self.env.cut = True + self.do_cut = True self.ui.browser.main_column.request_redraw() def paste_symlink(self, relative=False): - copied_files = self.env.copy + copied_files = self.copy_buffer for f in copied_files: self.notify(next_available_filename(f.basename)) try: @@ -996,7 +1030,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.notify(x) def paste_hardlink(self): - for f in self.env.copy: + for f in self.copy_buffer: try: new_name = next_available_filename(f.basename) link(f.path, join(getcwd(), new_name)) @@ -1004,7 +1038,7 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): self.notify(x) def paste_hardlinked_subtree(self): - for f in self.env.copy: + for f in self.copy_buffer: try: target_path = join(getcwd(), f.basename) self._recurse_hardlinked_tree(f.path, target_path) @@ -1027,16 +1061,16 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def paste(self, overwrite=False): """Paste the selected items into the current directory""" - copied_files = tuple(self.env.copy) + copied_files = tuple(self.copy_buffer) if not copied_files: return def refresh(_): - cwd = self.env.get_directory(original_path) + cwd = self.get_directory(original_path) cwd.load_content() - cwd = self.env.cwd + cwd = self.thisdir original_path = cwd.path one_file = copied_files[0] if overwrite: @@ -1046,9 +1080,9 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): cp_flags = ['--backup=numbered', '-a', '--'] mv_flags = ['--backup=numbered', '--'] - if self.env.cut: - self.env.copy.clear() - self.env.cut = False + if self.do_cut: + self.copy_buffer.clear() + self.do_cut = False if len(copied_files) == 1: descr = "moving: " + one_file.path else: @@ -1078,8 +1112,8 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def delete(self): # XXX: warn when deleting mount points/unseen marked files? self.notify("Deleting!") - selected = self.env.get_selection() - self.env.copy -= set(selected) + selected = self.thistab.get_selection() + self.copy_buffer -= set(selected) if selected: for f in selected: if isdir(f.path) and not os.path.islink(f.path): @@ -1092,11 +1126,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): os.remove(f.path) except OSError as err: self.notify(err) - self.env.ensure_correct_pointer() + self.thistab.ensure_correct_pointer() def mkdir(self, name): try: - os.mkdir(os.path.join(self.env.cwd.path, name)) + os.mkdir(os.path.join(self.thisdir.path, name)) except OSError as err: self.notify(err) diff --git a/ranger/core/environment.py b/ranger/core/environment.py index b5ab223d..61bbb6b2 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -1,221 +1,111 @@ # Copyright (C) 2009, 2010, 2011 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/>. - -import curses -import os -import pwd -import socket -from os.path import abspath, normpath, join, expanduser, isdir +# This software is distributed under the terms of the GNU GPL version 3. + +# THIS WHOLE FILE IS OBSOLETE AND EXISTS FOR BACKWARDS COMPATIBILITIY -from ranger.fsobject import Directory -from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps -from ranger.container.history import History +import os from ranger.ext.signals import SignalDispatcher -from ranger.core.shared import SettingsAware - -class Environment(SettingsAware, SignalDispatcher): - """ - A collection of data which is relevant for more than one class. - """ - - cwd = None # current directory - copy = None - cmd = None - cut = None - termsize = None - history = None - directories = None - last_search = None - pathway = None - path = None +from ranger.core.shared import SettingsAware, FileManagerAware +# COMPAT +class Environment(SettingsAware, FileManagerAware, SignalDispatcher): def __init__(self, path): SignalDispatcher.__init__(self) - self.path = abspath(expanduser(path)) - self._cf = None - self.pathway = () - self.directories = {} - self.keybuffer = KeyBuffer() - self.keymaps = KeyMaps(self.keybuffer) - self.copy = set() - self.history = History(self.settings.max_history_size, unique=False) - - try: - self.username = pwd.getpwuid(os.geteuid()).pw_name - except: - self.username = 'uid:' + str(os.geteuid()) - self.hostname = socket.gethostname() - self.home_path = os.path.expanduser('~') - - self.signal_bind('move', self._set_cf_from_signal, priority=0.1, - weak=True) - def _set_cf_from_signal(self, signal): - self._cf = signal.new + def _get_copy(self): return self.fm.copy_buffer + def _set_copy(self, obj): self.fm.copy_buffer = obj + copy = property(_get_copy, _set_copy) + + def _get_cut(self): return self.fm.do_cut + def _set_cut(self, obj): self.fm.do_cut = obj + cut = property(_get_cut, _set_cut) + + def _get_keymaps(self): return self.fm.ui.keymaps + def _set_keymaps(self, obj): self.fm.ui.keymaps = obj + keymaps = property(_get_keymaps, _set_keymaps) + + def _get_keybuffer(self): return self.fm.ui.keybuffer + def _set_keybuffer(self, obj): self.fm.ui.keybuffer = obj + keybuffer = property(_get_keybuffer, _set_keybuffer) - def _set_cf(self, value): - if value is not self._cf: - previous = self._cf - self.signal_emit('move', previous=previous, new=value) + def _get_username(self): return self.fm.username + def _set_username(self, obj): self.fm.username = obj + username = property(_get_username, _set_username) - def _get_cf(self): - return self._cf + def _get_hostname(self): return self.fm.hostname + def _set_hostname(self, obj): self.fm.hostname = obj + hostname = property(_get_hostname, _set_hostname) + def _get_home_path(self): return self.fm.home_path + def _set_home_path(self, obj): self.fm.home_path = obj + home_path = property(_get_home_path, _set_home_path) + + def _get_get_directory(self): return self.fm.get_directory + def _set_get_directory(self, obj): self.fm.get_directory = obj + get_directory = property(_get_get_directory, _set_get_directory) + + def _get_garbage_collect(self): return self.fm.garbage_collect + def _set_garbage_collect(self, obj): self.fm.garbage_collect = obj + garbage_collect = property(_get_garbage_collect, _set_garbage_collect) + + def _get_cwd(self): return self.fm.thisdir + def _set_cwd(self, obj): self.fm.thisdir = obj + cwd = property(_get_cwd, _set_cwd) + + def _get_cf(self): return self.fm.thisfile + def _set_cf(self, obj): self.fm.thisfile = obj cf = property(_get_cf, _set_cf) - def key_append(self, key): - """Append a key to the keybuffer""" - - # special keys: - if key == curses.KEY_RESIZE: - self.keybuffer.clear() - - self.keybuffer.add(key) - - def key_clear(self): - """Clear the keybuffer""" - self.keybuffer.clear() - - def at_level(self, level): - """ - Returns the FileSystemObject at the given level. - level >0 => previews - level 0 => current file/directory - level <0 => parent directories - """ - if level <= 0: - try: - return self.pathway[level - 1] - except IndexError: - return None - else: - directory = self.cf - for i in range(level - 1): - if directory is None: - return None - if directory.is_directory: - directory = directory.pointed_obj - else: - return None - try: - return self.directories[directory.path] - except AttributeError: - return None - except KeyError: - return directory - - def garbage_collect(self, age, tabs): - """Delete unused directory objects""" - for key in tuple(self.directories): - value = self.directories[key] - if age != -1: - if not value.is_older_than(age) or value in self.pathway: - continue - if value in tabs.values(): - continue - del self.directories[key] - if value.is_directory: - value.files = None - self.settings.signal_garbage_collect() - self.signal_garbage_collect() - - def get_selection(self): - if self.cwd: - return self.cwd.get_selection() - return set() - - def get_directory(self, path): - """Get the directory object at the given path""" - path = abspath(path) - try: - return self.directories[path] - except KeyError: - obj = Directory(path) - self.directories[path] = obj - return obj + def _get_history(self): return self.fm.thistab.history + def _set_history(self, obj): self.fm.thistab.history = obj + history = property(_get_history, _set_history) + + def _get_last_search(self): return self.fm.thistab.last_search + def _set_last_search(self, obj): self.fm.thistab.last_search = obj + last_search = property(_get_last_search, _set_last_search) + + def _get_path(self): return self.fm.thistab.path + def _set_path(self, obj): self.fm.thistab.path = obj + path = property(_get_path, _set_path) + + def _get_pathway(self): return self.fm.thistab.pathway + def _set_pathway(self, obj): self.fm.thistab.pathway = obj + pathway = property(_get_pathway, _set_pathway) + + def _get_enter_dir(self): return self.fm.thistab.enter_dir + def _set_enter_dir(self, obj): self.fm.thistab.enter_dir = obj + enter_dir = property(_get_enter_dir, _set_enter_dir) + + def _get_at_level(self): return self.fm.thistab.at_level + def _set_at_level(self, obj): self.fm.thistab.at_level = obj + at_level = property(_get_at_level, _set_at_level) + + def _get_get_selection(self): return self.fm.thistab.get_selection + def _set_get_selection(self, obj): self.fm.thistab.get_selection = obj + get_selection = property(_get_get_selection, _set_get_selection) + + def _get_assign_cursor_positions_for_subdirs(self): + return self.fm.thistab.assign_cursor_positions_for_subdirs + def _set_assign_cursor_positions_for_subdirs(self, obj): + self.fm.thistab.assign_cursor_positions_for_subdirs = obj + assign_cursor_positions_for_subdirs = property( + _get_assign_cursor_positions_for_subdirs, + _set_assign_cursor_positions_for_subdirs) + + def _get_ensure_correct_pointer(self): + return self.fm.thistab.ensure_correct_pointer + def _set_ensure_correct_pointer(self, obj): + self.fm.thistab.ensure_correct_pointer = obj + ensure_correct_pointer = property(_get_ensure_correct_pointer, + _set_ensure_correct_pointer) + + def _get_history_go(self): return self.fm.thistab.history_go + def _set_history_go(self, obj): self.fm.thistab.history_go = obj + history_go = property(_get_history_go, _set_history_go) + + def _set_cf_from_signal(self, signal): + self.fm._cf = signal.new def get_free_space(self, path): stat = os.statvfs(path) return stat.f_bavail * stat.f_bsize - - def assign_cursor_positions_for_subdirs(self): - """Assign correct cursor positions for subdirectories""" - last_path = None - for path in reversed(self.pathway): - if last_path is None: - last_path = path - continue - - path.move_to_obj(last_path) - last_path = path - - def ensure_correct_pointer(self): - if self.cwd: - self.cwd.correct_pointer() - - def history_go(self, relative): - """Move relative in history""" - if self.history: - self.history.move(relative).go(history=False) - - def enter_dir(self, path, history = True): - """Enter given path""" - if path is None: return - path = str(path) - - previous = self.cwd - - # get the absolute path - path = normpath(join(self.path, expanduser(path))) - - if not isdir(path): - return False - new_cwd = self.get_directory(path) - - try: - os.chdir(path) - except: - return True - self.path = path - self.cwd = new_cwd - - self.cwd.load_content_if_outdated() - - # build the pathway, a tuple of directory objects which lie - # on the path to the current directory. - if path == '/': - self.pathway = (self.get_directory('/'), ) - else: - pathway = [] - currentpath = '/' - for dir in path.split('/'): - currentpath = join(currentpath, dir) - pathway.append(self.get_directory(currentpath)) - self.pathway = tuple(pathway) - - self.assign_cursor_positions_for_subdirs() - - # set the current file. - self.cwd.sort_directories_first = self.settings.sort_directories_first - self.cwd.sort_reverse = self.settings.sort_reverse - self.cwd.sort_if_outdated() - self.cf = self.cwd.pointed_obj - - if history: - self.history.add(new_cwd) - - self.signal_emit('cd', previous=previous, new=self.cwd) - - return True diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 20327a71..f554ab1b 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The File Manager, putting the pieces together @@ -20,18 +8,21 @@ The File Manager, putting the pieces together from time import time from collections import deque import mimetypes -import os +import os.path +import pwd +import socket import stat import sys import ranger -from ranger import * from ranger.core.actions import Actions +from ranger.core.tab import Tab from ranger.container.tags import Tags from ranger.gui.ui import UI from ranger.container.bookmarks import Bookmarks from ranger.core.runner import Runner from ranger.ext.get_executables import get_executables +from ranger.ext.rifle import Rifle from ranger.fsobject import Directory from ranger.ext.signals import SignalDispatcher from ranger import __version__ @@ -40,19 +31,42 @@ from ranger.core.loader import Loader class FM(Actions, SignalDispatcher): input_blocked = False input_blocked_until = 0 - def __init__(self, ui=None, bookmarks=None, tags=None): + mode = 'normal' # either 'normal' or 'visual'. + search_method = 'ctime' + + _previous_selection = None + _visual_reverse = False + _visual_start = None + _visual_start_pos = None + + def __init__(self, ui=None, bookmarks=None, tags=None, paths=['.']): """Initialize FM.""" Actions.__init__(self) SignalDispatcher.__init__(self) - self.ui = ui + if ui is None: + self.ui = UI() + else: + self.ui = ui + self.start_paths = paths + self.directories = dict() self.log = deque(maxlen=20) self.bookmarks = bookmarks - self.tags = tags + self.current_tab = 1 self.tabs = {} + self.tags = tags + self.restorable_tabs = deque([], ranger.MAX_RESTORABLE_TABS) self.py3 = sys.version_info >= (3, ) self.previews = {} - self.current_tab = 1 self.loader = Loader() + self.copy_buffer = set() + self.do_cut = False + + try: + self.username = pwd.getpwuid(os.geteuid()).pw_name + except: + self.username = 'uid:' + str(os.geteuid()) + self.hostname = socket.gethostname() + self.home_path = os.path.expanduser('~') self.log.append('ranger {0} started! Process ID is {1}.' \ .format(__version__, os.getpid())) @@ -64,6 +78,24 @@ class FM(Actions, SignalDispatcher): def initialize(self): """If ui/bookmarks are None, they will be initialized here.""" + + self.tabs = dict((n+1, Tab(path)) for n, path in + enumerate(self.start_paths)) + tab_list = self._get_tab_list() + if tab_list: + self.current_tab = tab_list[0] + self.thistab = self.tabs[self.current_tab] + else: + self.current_tab = 1 + self.tabs[self.current_tab] = self.thistab = Tab('.') + + if not ranger.arg.clean and os.path.isfile(self.confpath('rifle.conf')): + rifleconf = self.confpath('rifle.conf') + else: + rifleconf = self.relpath('config/rifle.conf') + self.rifle = Rifle(rifleconf) + self.rifle.reload_config() + if self.bookmarks is None: if ranger.arg.clean: bookmarkfile = None @@ -78,16 +110,18 @@ class FM(Actions, SignalDispatcher): if not ranger.arg.clean and self.tags is None: self.tags = Tags(self.confpath('tagged')) - if self.ui is None: - self.ui = UI() - self.ui.initialize() + self.ui.setup_curses() + self.ui.initialize() + + self.rifle.hook_before_executing = lambda a, b, flags: \ + self.ui.suspend() if 'f' not in flags else None + self.rifle.hook_after_executing = lambda a, b, flags: \ + self.ui.initialize() if 'f' not in flags else None + self.rifle.hook_logger = self.notify def mylogfunc(text): self.notify(text, bad=True) - self.run = Runner(ui=self.ui, apps=self.apps, - logfunc=mylogfunc, fm=self) - - self.env.signal_bind('cd', self._update_current_tab) + self.run = Runner(ui=self.ui, logfunc=mylogfunc, fm=self) if self.settings.init_function: self.settings.init_function(self) @@ -107,6 +141,21 @@ class FM(Actions, SignalDispatcher): if debug: raise + def _get_thisfile(self): + return self.thistab.thisfile + + def _set_thisfile(self, obj): + self.thistab.thisfile = obj + + def _get_thisdir(self): + return self.thistab.thisdir + + def _set_thisdir(self, obj): + self.thistab.thisdir = obj + + thisfile = property(_get_thisfile, _set_thisfile) + thisdir = property(_get_thisdir, _set_thisdir) + def block_input(self, sec=0): self.input_blocked = sec != 0 self.input_blocked_until = time() + sec @@ -130,20 +179,20 @@ class FM(Actions, SignalDispatcher): shutil.copy(self.relpath(_from), self.confpath(to)) except Exception as e: sys.stderr.write(" ERROR: %s\n" % str(e)) - if which == 'apps' or which == 'all': - copy('defaults/apps.py', 'apps.py') + if which == 'rifle' or which == 'all': + copy('config/rifle.conf', 'rifle.conf') if which == 'commands' or which == 'all': - copy('defaults/commands.py', 'commands.py') + copy('config/commands.py', 'commands.py') if which == 'rc' or which == 'all': - copy('defaults/rc.conf', 'rc.conf') + copy('config/rc.conf', 'rc.conf') if which == 'options' or which == 'all': - copy('defaults/options.py', 'options.py') + copy('config/options.py', 'options.py') if which == 'scope' or which == 'all': copy('data/scope.sh', 'scope.sh') os.chmod(self.confpath('scope.sh'), os.stat(self.confpath('scope.sh')).st_mode | stat.S_IXUSR) if which not in \ - ('all', 'apps', 'scope', 'commands', 'rc', 'options'): + ('all', 'rifle', 'scope', 'commands', 'rc', 'options'): sys.stderr.write("Unknown config file `%s'\n" % which) def confpath(self, *paths): @@ -157,6 +206,30 @@ class FM(Actions, SignalDispatcher): """returns the path relative to rangers library directory""" return os.path.join(ranger.RANGERDIR, *paths) + def get_directory(self, path): + """Get the directory object at the given path""" + path = os.path.abspath(path) + try: + return self.directories[path] + except KeyError: + obj = Directory(path) + self.directories[path] = obj + return obj + + def garbage_collect(self, age, tabs=None): # tabs=None is for COMPATibility + """Delete unused directory objects""" + for key in tuple(self.directories): + value = self.directories[key] + if age != -1: + if not value.is_older_than(age) \ + or any(value in tab.pathway for tab in self.tabs.values()): + continue + del self.directories[key] + if value.is_directory: + value.files = None + self.settings.signal_garbage_collect() + self.signal_garbage_collect() + def loop(self): """ The main loop consists of: @@ -167,7 +240,7 @@ class FM(Actions, SignalDispatcher): 5. after X loops: collecting unused directory objects """ - self.env.enter_dir(self.env.path) + self.enter_dir(self.thistab.path) gc_tick = 0 @@ -175,7 +248,6 @@ class FM(Actions, SignalDispatcher): ui = self.ui throbber = ui.throbber loader = self.loader - env = self.env has_throbber = hasattr(ui, 'throbber') zombies = self.run.zombies @@ -190,7 +262,7 @@ class FM(Actions, SignalDispatcher): ui.redraw() - ui.set_load_mode(loader.has_work()) + ui.set_load_mode(not loader.paused and loader.has_work()) ui.handle_input() @@ -200,10 +272,9 @@ class FM(Actions, SignalDispatcher): zombies.remove(zombie) gc_tick += 1 - if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE: + if gc_tick > ranger.TICKS_BEFORE_COLLECTING_GARBAGE: gc_tick = 0 - env.garbage_collect(TIME_BEFORE_FILE_BECOMES_GARBAGE, - self.tabs) + self.garbage_collect(ranger.TIME_BEFORE_FILE_BECOMES_GARBAGE) except KeyboardInterrupt: # this only happens in --debug mode. By default, interrupts @@ -211,9 +282,9 @@ class FM(Actions, SignalDispatcher): raise SystemExit finally: - if ranger.arg.choosedir and self.env.cwd and self.env.cwd.path: + if ranger.arg.choosedir and self.thisdir and self.thisdir.path: # XXX: UnicodeEncodeError: 'utf-8' codec can't encode character # '\udcf6' in position 42: surrogates not allowed - open(ranger.arg.choosedir, 'w').write(self.env.cwd.path) - self.bookmarks.remember(env.cwd) + open(ranger.arg.choosedir, 'w').write(self.thisdir.path) + self.bookmarks.remember(self.thisdir) self.bookmarks.save() diff --git a/ranger/core/helper.py b/ranger/core/helper.py deleted file mode 100644 index c556b9bd..00000000 --- a/ranger/core/helper.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (C) 2009, 2010, 2011 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/>. - -"""Helper functions""" - -from errno import EEXIST -import os.path -import sys -from ranger import * - -def parse_arguments(): - """Parse the program arguments""" - from optparse import OptionParser - from ranger import __version__ - from ranger.ext.openstruct import OpenStruct - from os.path import expanduser - - if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']: - default_confdir = os.environ['XDG_CONFIG_HOME'] + '/ranger' - else: - default_confdir = CONFDIR - - parser = OptionParser(usage=USAGE, version='ranger %s%s' \ - % (__version__, " (stable)" if STABLE else "")) - - parser.add_option('-d', '--debug', action='store_true', - help="activate debug mode") - parser.add_option('-c', '--clean', action='store_true', - help="don't touch/require any config files. ") - parser.add_option('--copy-config', type='string', metavar='which', - help="copy the default configs to the local config directory. " - "Possible values: all, rc, apps, commands, options, scope") - parser.add_option('--fail-unless-cd', action='store_true', - help="experimental: return the exit code 1 if ranger is" \ - "used to run a file (with `ranger filename`)") - parser.add_option('-r', '--confdir', type='string', - metavar='dir', default=default_confdir, - help="the configuration directory. (%default)") - parser.add_option('-m', '--mode', type='int', default=0, metavar='n', - help="if a filename is supplied, run it with this mode") - parser.add_option('-f', '--flags', type='string', default='', - metavar='string', - help="if a filename is supplied, run it with these flags.") - parser.add_option('--choosefile', type='string', metavar='TARGET', - help="Makes ranger act like a file chooser. When opening " - "a file, it will quit and write the name of the selected " - "file to TARGET.") - parser.add_option('--choosefiles', type='string', metavar='TARGET', - help="Makes ranger act like a file chooser for multiple files " - "at once. When opening a file, it will quit and write the name " - "of all selected files to TARGET.") - parser.add_option('--choosedir', type='string', metavar='TARGET', - help="Makes ranger act like a directory chooser. When ranger quits" - ", it will write the name of the last visited directory to TARGET") - parser.add_option('--list-unused-keys', action='store_true', - help="List common keys which are not bound to any action.") - parser.add_option('--selectfile', type='string', metavar='filepath', - help="Open ranger with supplied file selected.") - parser.add_option('--list-tagged-files', type='string', default=None, - metavar='tag', - help="List all files which are tagged with the given tag, default: *") - parser.add_option('--profile', action='store_true', - help="Print statistics of CPU usage on exit.") - parser.add_option('--cmd', action='append', type='string', metavar='COMMAND', - help="Execute COMMAND after the configuration has been read. " - "Use this option multiple times to run multiple commands.") - - options, positional = parser.parse_args() - arg = OpenStruct(options.__dict__, targets=positional) - arg.confdir = expanduser(arg.confdir) - - return arg - - -def load_settings(fm, clean): - from ranger.core.actions import Actions - import ranger.core.shared - import ranger.api.commands - from ranger.defaults import commands - - # Load default commands - fm.commands = ranger.api.commands.CommandContainer() - exclude = ['settings'] - include = [name for name in dir(Actions) if name not in exclude] - fm.commands.load_commands_from_object(fm, include) - fm.commands.load_commands_from_module(commands) - - if not clean: - allow_access_to_confdir(ranger.arg.confdir, True) - - # Load custom commands - try: - import commands - fm.commands.load_commands_from_module(commands) - except ImportError: - pass - - # Load apps - try: - import apps - except ImportError: - from ranger.defaults import apps - fm.apps = apps.CustomApplications() - - # Load rc.conf - custom_conf = fm.confpath('rc.conf') - default_conf = fm.relpath('defaults', 'rc.conf') - load_default_rc = fm.settings.load_default_rc - - if load_default_rc: - fm.source(default_conf) - if os.access(custom_conf, os.R_OK): - fm.source(custom_conf) - - # XXX Load plugins (experimental) - try: - plugindir = fm.confpath('plugins') - plugins = [p[:-3] for p in os.listdir(plugindir) \ - if p.endswith('.py') and not p.startswith('_')] - except: - pass - else: - if not os.path.exists(fm.confpath('plugins', '__init__.py')): - f = open(fm.confpath('plugins', '__init__.py'), 'w') - f.close() - - ranger.fm = fm - for plugin in sorted(plugins): - try: - module = __import__('plugins', fromlist=[plugin]) - fm.log.append("Loaded plugin '%s'." % module) - except Exception as e: - fm.log.append("Error in plugin '%s'" % plugin) - import traceback - for line in traceback.format_exception_only(type(e), e): - fm.log.append(line) - ranger.fm = None - - allow_access_to_confdir(ranger.arg.confdir, False) - else: - from ranger.defaults import apps - fm.apps = apps.CustomApplications() - fm.source(fm.relpath('defaults', 'rc.conf')) - - -def load_apps(fm, clean): - import ranger - if not clean: - allow_access_to_confdir(ranger.arg.confdir, True) - try: - import apps - except ImportError: - from ranger.defaults import apps - allow_access_to_confdir(ranger.arg.confdir, False) - else: - from ranger.defaults import apps - fm.apps = apps.CustomApplications() - - -def allow_access_to_confdir(confdir, allow): - if allow: - try: - os.makedirs(confdir) - except OSError as err: - if err.errno != EEXIST: # EEXIST means it already exists - print("This configuration directory could not be created:") - print(confdir) - print("To run ranger without the need for configuration") - print("files, use the --clean option.") - raise SystemExit() - if not confdir in sys.path: - sys.path[0:0] = [confdir] - else: - if sys.path[0] == confdir: - del sys.path[0] - - -# Debugging functions. These will be activated when run with --debug. -# Example usage in the code: -# import ranger; ranger.log("hello world") -def log(*objects, **keywords): - """ - Writes objects to a logfile (for the purpose of debugging only.) - Has the same arguments as print() in python3. - """ - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: return - start = 'start' in keywords and keywords['start'] or 'ranger:' - sep = 'sep' in keywords and keywords['sep'] or ' ' - _file = 'file' in keywords and keywords['file'] or open(LOGFILE, 'a') - end = 'end' in keywords and keywords['end'] or '\n' - _file.write(sep.join(map(str, (start, ) + objects)) + end) - - -def log_traceback(): - from ranger import arg - if LOGFILE is None or not arg.debug or arg.clean: return - import traceback - traceback.print_stack(file=open(LOGFILE, 'a')) diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 59d3e6c0..7bbd2abf 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from collections import deque from time import time, sleep @@ -32,6 +20,7 @@ class Loadable(object): def __init__(self, gen, descr): self.load_generator = gen self.description = descr + self.percent = -1 def get_description(self): return self.description @@ -123,8 +112,8 @@ class CommandLoader(Loadable, SignalDispatcher, FileManagerAware): self.process.send_signal(20) except: pass - Loadable.pause(self) - self.signal_emit('pause', process=self.process, loader=self) + Loadable.pause(self) + self.signal_emit('pause', process=self.process, loader=self) def unpause(self): if not self.finished and self.paused: @@ -132,8 +121,8 @@ class CommandLoader(Loadable, SignalDispatcher, FileManagerAware): self.process.send_signal(18) except: pass - Loadable.unpause(self) - self.signal_emit('unpause', process=self.process, loader=self) + Loadable.unpause(self) + self.signal_emit('unpause', process=self.process, loader=self) def destroy(self): self.signal_emit('destroy', process=self.process, loader=self) @@ -154,6 +143,8 @@ def safeDecode(string): class Loader(FileManagerAware): seconds_of_work_time = 0.03 throbber_chars = r'/-\|' + throbber_paused = '#' + paused = False def __init__(self): self.queue = deque() @@ -178,6 +169,10 @@ class Loader(FileManagerAware): while obj in self.queue: self.queue.remove(obj) self.queue.appendleft(obj) + if self.paused: + obj.pause() + else: + obj.unpause() def move(self, _from, to): try: @@ -213,11 +208,34 @@ class Loader(FileManagerAware): item.destroy() del self.queue[index] + def pause(self, state): + """ + Change the pause-state to 1 (pause), 0 (no pause) or -1 (toggle) + """ + if state == -1: + state = not self.paused + elif state == self.paused: + return + + self.paused = state + + if not self.queue: + return + + if state: + self.queue[0].pause() + else: + self.queue[0].unpause() + def work(self): """ Load items from the queue if there are any. Stop after approximately self.seconds_of_work_time. """ + if self.paused: + self.status = self.throbber_paused + return + while True: # get the first item with a proper load_generator try: @@ -229,6 +247,8 @@ class Loader(FileManagerAware): except IndexError: return + item.unpause() + self.rotate() if item != self.old_item: if self.old_item: diff --git a/ranger/core/main.py b/ranger/core/main.py index b4629801..c3a3d76b 100644 --- a/ranger/core/main.py +++ b/ranger/core/main.py @@ -1,32 +1,18 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The main function responsible to initialize the FM object and stuff. """ -from ranger.core.helper import * +import os.path def main(): """initialize objects and run the filemanager""" import locale - import os.path import ranger import sys - from ranger.core.shared import (EnvironmentAware, FileManagerAware, - SettingsAware) + from ranger.core.shared import FileManagerAware, SettingsAware from ranger.core.fm import FM if not sys.stdin.isatty(): @@ -85,29 +71,28 @@ def main(): elif os.path.isfile(target): def print_function(string): print(string) - from ranger.core.runner import Runner - from ranger.fsobject import File + from ranger.ext.rifle import Rifle fm = FM() - runner = Runner(logfunc=print_function, fm=fm) - load_apps(runner, arg.clean) - runner(files=[File(target)], mode=arg.mode, flags=arg.flags) + if not arg.clean and os.path.isfile(fm.confpath('rifle.conf')): + rifleconf = fm.confpath('rifle.conf') + else: + rifleconf = fm.relpath('config/rifle.conf') + rifle = Rifle(rifleconf) + rifle.reload_config() + rifle.execute(targets, number=ranger.arg.mode, flags=ranger.arg.flags) return 1 if arg.fail_unless_cd else 0 crash_traceback = None try: # Initialize objects - from ranger.core.environment import Environment - fm = FM() + fm = FM(paths=targets) FileManagerAware.fm = fm - EnvironmentAware.env = Environment(target) - fm.tabs = dict((n+1, os.path.abspath(path)) for n, path \ - in enumerate(targets[:9])) load_settings(fm, arg.clean) if arg.list_unused_keys: from ranger.ext.keybinding_parser import (special_keys, reversed_special_keys) - maps = fm.env.keymaps['browser'] + maps = fm.ui.keymaps['browser'] for key in sorted(special_keys.values(), key=lambda x: str(x)): if key not in maps: print("<%s>" % reversed_special_keys[key]) @@ -116,7 +101,7 @@ def main(): print(chr(key)) return 1 if arg.fail_unless_cd else 0 - if fm.env.username == 'root': + if fm.username == 'root': fm.settings.preview_files = False fm.settings.use_preview_script = False if not arg.debug: @@ -151,7 +136,7 @@ def main(): finally: if crash_traceback: try: - filepath = fm.env.cf.path if fm.env.cf else "None" + filepath = fm.thisfile.path if fm.thisfile else "None" except: filepath = "None" try: @@ -174,3 +159,149 @@ def main(): print("http://savannah.nongnu.org/bugs/?group=ranger&func=additem") return 1 return 0 + + +def parse_arguments(): + """Parse the program arguments""" + from optparse import OptionParser + from os.path import expanduser + from ranger import CONFDIR, USAGE, VERSION + from ranger.ext.openstruct import OpenStruct + + if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']: + default_confdir = os.environ['XDG_CONFIG_HOME'] + '/ranger' + else: + default_confdir = CONFDIR + + parser = OptionParser(usage=USAGE, version=VERSION) + + parser.add_option('-d', '--debug', action='store_true', + help="activate debug mode") + parser.add_option('-c', '--clean', action='store_true', + help="don't touch/require any config files. ") + parser.add_option('--copy-config', type='string', metavar='which', + help="copy the default configs to the local config directory. " + "Possible values: all, rc, rifle, commands, options, scope") + parser.add_option('--fail-unless-cd', action='store_true', + help="experimental: return the exit code 1 if ranger is" \ + "used to run a file (with `ranger filename`)") + parser.add_option('-r', '--confdir', type='string', + metavar='dir', default=default_confdir, + help="the configuration directory. (%default)") + parser.add_option('-m', '--mode', type='int', default=0, metavar='n', + help="if a filename is supplied, run it with this mode") + parser.add_option('-f', '--flags', type='string', default='', + metavar='string', + help="if a filename is supplied, run it with these flags.") + parser.add_option('--choosefile', type='string', metavar='TARGET', + help="Makes ranger act like a file chooser. When opening " + "a file, it will quit and write the name of the selected " + "file to TARGET.") + parser.add_option('--choosefiles', type='string', metavar='TARGET', + help="Makes ranger act like a file chooser for multiple files " + "at once. When opening a file, it will quit and write the name " + "of all selected files to TARGET.") + parser.add_option('--choosedir', type='string', metavar='TARGET', + help="Makes ranger act like a directory chooser. When ranger quits" + ", it will write the name of the last visited directory to TARGET") + parser.add_option('--list-unused-keys', action='store_true', + help="List common keys which are not bound to any action.") + parser.add_option('--selectfile', type='string', metavar='filepath', + help="Open ranger with supplied file selected.") + parser.add_option('--list-tagged-files', type='string', default=None, + metavar='tag', + help="List all files which are tagged with the given tag, default: *") + parser.add_option('--profile', action='store_true', + help="Print statistics of CPU usage on exit.") + parser.add_option('--cmd', action='append', type='string', metavar='COMMAND', + help="Execute COMMAND after the configuration has been read. " + "Use this option multiple times to run multiple commands.") + + options, positional = parser.parse_args() + arg = OpenStruct(options.__dict__, targets=positional) + arg.confdir = expanduser(arg.confdir) + + return arg + + +def load_settings(fm, clean): + from ranger.core.actions import Actions + import ranger.core.shared + import ranger.api.commands + from ranger.config import commands + + # Load default commands + fm.commands = ranger.api.commands.CommandContainer() + exclude = ['settings'] + include = [name for name in dir(Actions) if name not in exclude] + fm.commands.load_commands_from_object(fm, include) + fm.commands.load_commands_from_module(commands) + + if not clean: + allow_access_to_confdir(ranger.arg.confdir, True) + + # Load custom commands + try: + import commands + fm.commands.load_commands_from_module(commands) + except ImportError: + pass + + # Load rc.conf + custom_conf = fm.confpath('rc.conf') + default_conf = fm.relpath('config', 'rc.conf') + load_default_rc = fm.settings.load_default_rc + + if load_default_rc: + fm.source(default_conf) + if os.access(custom_conf, os.R_OK): + fm.source(custom_conf) + + # XXX Load plugins (experimental) + try: + plugindir = fm.confpath('plugins') + plugins = [p[:-3] for p in os.listdir(plugindir) \ + if p.endswith('.py') and not p.startswith('_')] + except: + pass + else: + if not os.path.exists(fm.confpath('plugins', '__init__.py')): + f = open(fm.confpath('plugins', '__init__.py'), 'w') + f.close() + + ranger.fm = fm + for plugin in sorted(plugins): + try: + module = __import__('plugins', fromlist=[plugin]) + fm.log.append("Loaded plugin '%s'." % module) + except Exception as e: + fm.log.append("Error in plugin '%s'" % plugin) + import traceback + for line in traceback.format_exception_only(type(e), e): + fm.log.append(line) + ranger.fm = None + + allow_access_to_confdir(ranger.arg.confdir, False) + else: + fm.source(fm.relpath('config', 'rc.conf')) + + +def allow_access_to_confdir(confdir, allow): + import sys + from errno import EEXIST + + if allow: + try: + os.makedirs(confdir) + except OSError as err: + if err.errno != EEXIST: # EEXIST means it already exists + print("This configuration directory could not be created:") + print(confdir) + print("To run ranger without the need for configuration") + print("files, use the --clean option.") + raise SystemExit() + if not confdir in sys.path: + sys.path[0:0] = [confdir] + else: + if sys.path[0] == confdir: + del sys.path[0] diff --git a/ranger/core/runner.py b/ranger/core/runner.py index c02ad6b3..c5decf4c 100644 --- a/ranger/core/runner.py +++ b/ranger/core/runner.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ This module is an abstract layer over subprocess.Popen @@ -41,7 +29,9 @@ from subprocess import Popen, PIPE from ranger.ext.get_executables import get_executables -ALLOWED_FLAGS = 'sdpwcrtSDPWCRT' +# TODO: Remove unused parts of runner.py +#ALLOWED_FLAGS = 'sdpwcrtSDPWCRT' +ALLOWED_FLAGS = 'cfrtCFRT' def press_enter(): @@ -97,11 +87,10 @@ class Context(object): class Runner(object): - def __init__(self, ui=None, logfunc=None, apps=None, fm=None): + def __init__(self, ui=None, logfunc=None, fm=None): self.ui = ui self.fm = fm self.logfunc = logfunc - self.apps = apps self.zombies = set() def _log(self, text): @@ -140,16 +129,6 @@ class Runner(object): flags=flags, wait=wait, popen_kws=popen_kws, file=files and files[0] or None) - if self.apps: - if try_app_first and action is not None: - test = self.apps.apply(app, context) - if test: - action = test - if action is None: - action = self.apps.apply(app, context) - if action is None: - return self._log("No action found!") - if action is None: return self._log("No way of determining the action!") diff --git a/ranger/core/shared.py b/ranger/core/shared.py index 9da79578..26023e43 100644 --- a/ranger/core/shared.py +++ b/ranger/core/shared.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """Shared objects contain singleton variables which can be inherited, essentially acting like global variables.""" @@ -83,7 +71,7 @@ class SettingsAware(Awareness): settings._setting_sources.append(my_options) del sys.path[0] - from ranger.defaults import options as default_options + from ranger.config import options as default_options settings._setting_sources.append(default_options) assert all(hasattr(default_options, setting) \ for setting in ALLOWED_SETTINGS), \ diff --git a/ranger/core/tab.py b/ranger/core/tab.py new file mode 100644 index 00000000..6059eb6b --- /dev/null +++ b/ranger/core/tab.py @@ -0,0 +1,159 @@ +# Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com> +# This software is distributed under the terms of the GNU GPL version 3. + +import os +import sys +from os.path import abspath, normpath, join, expanduser, isdir + +from ranger.container.history import History +from ranger.core.shared import FileManagerAware, SettingsAware +from ranger.ext.signals import SignalDispatcher + +class Tab(FileManagerAware, SettingsAware): + def __init__(self, path): + self.thisdir = None # Current Working Directory + self._thisfile = None # Current File + self.history = History(self.settings.max_history_size, unique=False) + self.last_search = None + self.pointer = 0 + self.path = abspath(expanduser(path)) + self.pathway = () + # NOTE: in the line below, weak=True works only in python3. In python2, + # weak references are not equal to the original object when tested with + # "==", and this breaks _set_thisfile_from_signal and _on_tab_change. + self.fm.signal_bind('move', self._set_thisfile_from_signal, priority=0.1, + weak=(sys.version > '3')) + self.fm.signal_bind('tab.change', self._on_tab_change, + weak=(sys.version > '3')) + + def _set_thisfile_from_signal(self, signal): + if self == signal.tab: + self._thisfile = signal.new + if self == self.fm.thistab: + self.pointer = self.thisdir.pointer + + def _on_tab_change(self, signal): + if self == signal.new and self.thisdir: + # restore the pointer whenever this tab is reopened + self.thisdir.pointer = self.pointer + self.thisdir.correct_pointer() + + def _set_thisfile(self, value): + if value is not self._thisfile: + previous = self._thisfile + self.fm.signal_emit('move', previous=previous, new=value, tab=self) + + def _get_thisfile(self): + return self._thisfile + + thisfile = property(_get_thisfile, _set_thisfile) + + def at_level(self, level): + """ + Returns the FileSystemObject at the given level. + level >0 => previews + level 0 => current file/directory + level <0 => parent directories + """ + if level <= 0: + try: + return self.pathway[level - 1] + except IndexError: + return None + else: + directory = self.thisfile + for i in range(level - 1): + if directory is None: + return None + if directory.is_directory: + directory = directory.pointed_obj + else: + return None + try: + return self.fm.directories[directory.path] + except AttributeError: + return None + except KeyError: + return directory + + def get_selection(self): + if self.thisdir: + return self.thisdir.get_selection() + return set() + + def assign_cursor_positions_for_subdirs(self): + """Assign correct cursor positions for subdirectories""" + last_path = None + for path in reversed(self.pathway): + if last_path is None: + last_path = path + continue + + path.move_to_obj(last_path) + last_path = path + + def ensure_correct_pointer(self): + if self.thisdir: + self.thisdir.correct_pointer() + + def history_go(self, relative): + """Move relative in history""" + if self.history: + self.history.move(relative).go(history=False) + + def inherit_history(self, other_history): + self.history.rebase(other_history) + + def enter_dir(self, path, history = True): + """Enter given path""" + # TODO: Ensure that there is always a self.thisdir + if path is None: return + path = str(path) + + previous = self.thisdir + + # get the absolute path + path = normpath(join(self.path, expanduser(path))) + + if not isdir(path): + return False + new_thisdir = self.fm.get_directory(path) + + try: + os.chdir(path) + except: + return True + self.path = path + self.thisdir = new_thisdir + + self.thisdir.load_content_if_outdated() + + # build the pathway, a tuple of directory objects which lie + # on the path to the current directory. + if path == '/': + self.pathway = (self.fm.get_directory('/'), ) + else: + pathway = [] + currentpath = '/' + for dir in path.split('/'): + currentpath = join(currentpath, dir) + pathway.append(self.fm.get_directory(currentpath)) + self.pathway = tuple(pathway) + + self.assign_cursor_positions_for_subdirs() + + # set the current file. + self.thisdir.sort_directories_first = self.fm.settings.sort_directories_first + self.thisdir.sort_reverse = self.fm.settings.sort_reverse + self.thisdir.sort_if_outdated() + self._thisfile = self.thisdir.pointed_obj + + if history: + self.history.add(new_thisdir) + + self.fm.signal_emit('cd', previous=previous, new=self.thisdir) + + return True + + def __repr__(self): + return "<Tab '%s'>" % self.thisdir diff --git a/ranger/data/ranger b/ranger/data/ranger deleted file mode 120000 index 99d43c63..00000000 --- a/ranger/data/ranger +++ /dev/null @@ -1 +0,0 @@ -../../ranger.py \ No newline at end of file diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py deleted file mode 100644 index ffab8bea..00000000 --- a/ranger/defaults/apps.py +++ /dev/null @@ -1,325 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com> -# This configuration file is licensed under the same terms as ranger. -# =================================================================== -# This is the configuration file for file type detection and application -# handling. It's all in python; lines beginning with # are comments. -# -# You can customize this in the file ~/.config/ranger/apps.py. -# It has the same syntax as this file. In fact, you can just copy this -# file there with `ranger --copy-config=apps' and make your modifications. -# But make sure you update your configs when you update ranger. -# -# In order to add application definitions "on top of" the default ones -# in your ~/.config/ranger/apps.py, you should subclass the class defined -# here like this: -# -# from ranger.defaults.apps import CustomApplications as DefaultApps -# class CustomApplications(DeafultApps): -# <your definitions here> -# -# To override app_defaults, you can write something like: -# -# def app_defaults(self, c): -# f = c.file -# if f.extension == 'lol': -# return "lolopener", c -# return DefaultApps.app_default(self, c) -# -# =================================================================== -# This system is based on things called MODES and FLAGS. You can read -# in the man page about them. To remind you, here's a list of all flags. -# An uppercase flag inverts previous flags of the same name. -# s Silent mode. Output will be discarded. -# d Detach the process. (Run in background) -# p Redirect output to the pager -# w Wait for an Enter-press when the process is done -# c Run the current file only, instead of the selection -# r Run application with root privilege -# t Run application in a new terminal window -# -# To implement flags in this file, you could do this: -# context.flags += "d" -# Another example: -# context.flags += "Dw" -# -# To implement modes in this file, you can do something like: -# if context.mode == 1: -# <run in one way> -# elif context.mode == 2: -# <run in another way> -# -# =================================================================== -# The methods are called with a "context" object which provides some -# attributes that transfer information. Relevant attributes are: -# -# mode -- a number, mainly used in determining the action in app_xyz() -# flags -- a string with flags which change the way programs are run -# files -- a list containing files, mainly used in app_xyz -# filepaths -- a list of the paths of each file -# file -- an arbitrary file from that list (or None) -# fm -- the filemanager instance -# popen_kws -- keyword arguments which are directly passed to Popen -# -# =================================================================== -# The return value of the functions should be either: -# 1. A reference to another app, like: -# return self.app_editor(context) -# -# 2. A call to the "either" method, which uses the first program that -# is installed on your system. If none are installed, None is returned. -# return self.either(context, "libreoffice", "soffice", "ooffice") -# -# 3. A tuple of arguments that should be run. -# return "mplayer", "-fs", context.file.path -# If you use lists instead of strings, they will be flattened: -# args = ["-fs", "-shuf"] -# return "mplayer", args, context.filepaths -# "context.filepaths" can, and will often be abbreviated with just "context": -# return "mplayer", context -# -# 4. "None" to indicate that no action was found. -# return None -# -# =================================================================== -# When using the "either" method, ranger determines which program to -# pick by looking at its dependencies. You can set dependencies by -# adding the decorator "depends_on": -# @depends_on("vim") -# def app_vim(self, context): -# .... -# There is a special keyword which you can use as a dependence: "X" -# This ensures that the program will only run when X is running. -# =================================================================== - -import ranger -from ranger.api.apps import * -from ranger.ext.get_executables import get_executables - -class CustomApplications(Applications): - def app_default(self, c): - """How to determine the default application?""" - f = c.file - - if f.basename.lower() == 'makefile' and c.mode == 1: - made = self.either(c, 'make') - if made: return made - - if f.extension is not None: - if f.extension in ('pdf', ): - return self.either(c, 'llpp', 'zathura', 'mupdf', 'apvlv', - 'evince', 'okular', 'epdfview') - if f.extension == 'djvu': - return self.either(c, 'evince') - if f.extension in ('xml', 'csv'): - return self.either(c, 'editor') - if f.extension == 'mid': - return self.either(c, 'wildmidi') - if f.extension in ('html', 'htm', 'xhtml') or f.extension == 'swf': - c.flags += 'd' - handler = self.either(c, - 'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji', - 'firefox', 'seamonkey', 'iceweasel', 'opera', - 'surf', 'midori', 'epiphany', 'konqueror') - # Only return if some program was found: - if handler: - return handler - if f.extension in ('html', 'htm', 'xhtml'): - # These browsers can't handle flash, so they're not called above. - c.flags += 'D' - return self.either(c, 'elinks', 'links', 'links2', 'lynx', 'w3m') - if f.extension == 'nes': - return self.either(c, 'fceux') - if f.extension in ('swc', 'smc', 'sfc'): - return self.either(c, 'zsnes') - if f.extension == 'doc': - return self.either(c, 'abiword', 'libreoffice', - 'soffice', 'ooffice') - if f.extension in ('odt', 'ods', 'odp', 'odf', 'odg', 'odb', 'sxc', - 'stc', 'xls', 'xlsx', 'xlt', 'xlw', 'gnm', 'gnumeric'): - return self.either(c, 'gnumeric', 'kspread', - 'libreoffice', 'soffice', 'ooffice') - - if f.mimetype is not None: - if INTERPRETED_LANGUAGES.match(f.mimetype): - return self.either(c, 'edit_or_run') - - if f.container: - return self.either(c, 'aunpack', 'file_roller') - - if f.video or f.audio: - if f.video: - c.flags += 'd' - return self.either(c, 'smplayer', 'gmplayer', 'mplayer2', - 'mplayer', 'vlc', 'totem') - - if f.image: - if c.mode in (11, 12, 13, 14): - return self.either(c, 'set_bg_with_feh') - else: - return self.either(c, 'sxiv', 'feh', 'eog', 'mirage') - - if f.document or f.filetype.startswith('text') or f.size == 0: - return self.either(c, 'editor') - - # You can put this at the top of the function and mimeopen will - # always be used for every file. - return self.either(c, 'mimeopen') - - - # ----------------------------------------- application definitions - # Note: Trivial application definitions are at the bottom - def app_pager(self, c): - return 'less', '-R', c - - def app_editor(self, c): - try: - default_editor = os.environ['EDITOR'] - except KeyError: - pass - else: - parts = default_editor.split() - exe_name = os.path.basename(parts[0]) - if exe_name in get_executables(): - return tuple(parts) + tuple(c) - - return self.either(c, 'vim', 'emacs', 'nano') - - def app_edit_or_run(self, c): - if c.mode is 1: - return self.app_self(c) - return self.app_editor(c) - - @depends_on('mplayer') - def app_mplayer(self, c): - if c.mode is 1: - return 'mplayer', '-fs', c - - elif c.mode is 2: - args = "mplayer -fs -sid 0 -vfm ffmpeg -lavdopts " \ - "lowres=1:fast:skiploopfilter=all:threads=8".split() - args.extend(c) - return args - - elif c.mode is 3: - return 'mplayer', '-mixer', 'software', c - - else: - return 'mplayer', c - - @depends_on('mplayer2') - def app_mplayer2(self, c): - args = list(self.app_mplayer(c)) - args[0] += '2' - return args - - # A dependence on "X" means: this programs requires a running X server! - @depends_on('feh', 'X') - def app_set_bg_with_feh(self, c): - c.flags += 'd' - arg = {11: '--bg-scale', 12: '--bg-tile', 13: '--bg-center', - 14: '--bg-fill'} - if c.mode in arg: - return 'feh', arg[c.mode], c.file.path - return 'feh', arg[11], c.file.path - - @depends_on('feh', 'X') - def app_feh(self, c): - c.flags += 'd' - if c.mode is 0 and len(c.files) is 1 and self.fm.env.cwd: - # view all files in the cwd - images = [f.basename for f in self.fm.env.cwd.files if f.image] - return 'feh', '--start-at', c.file.basename, images - return 'feh', c - - @depends_on('sxiv', 'X') - def app_sxiv(self, c): - c.flags = 'd' + c.flags - if len(c.files) is 1 and self.fm.env.cwd: - images = [f.basename for f in self.fm.env.cwd.files if f.image] - try: - position = images.index(c.file.basename) + 1 - except: - return None - return 'sxiv', '-n', str(position), images - return 'sxiv', c - - @depends_on('aunpack') - def app_aunpack(self, c): - if c.mode is 0: - c.flags += 'p' - return 'aunpack', '-l', c.file.path - return 'aunpack', c.file.path - - @depends_on('file-roller', 'X') - def app_file_roller(self, c): - c.flags += 'd' - return 'file-roller', c.file.path - - @depends_on('make') - def app_make(self, c): - if c.mode is 0: - return "make" - if c.mode is 1: - return "make", "install" - if c.mode is 2: - return "make", "clear" - - @depends_on('java') - def app_java(self, c): - def strip_extensions(file): - if '.' in file.basename: - return file.path[:file.path.index('.')] - return file.path - files_without_extensions = map(strip_extensions, c.files) - return "java", files_without_extensions - - @depends_on('totem', 'X') - def app_totem(self, c): - if c.mode is 0: - return "totem", c - if c.mode is 1: - return "totem", "--fullscreen", c - - @depends_on('mimeopen') - def app_mimeopen(self, c): - if c.mode is 0: - return "mimeopen", c - if c.mode is 1: - # Will ask user to select program - # aka "Open with..." - return "mimeopen", "--ask", c - - -# Often a programs invocation is trivial. For example: -# vim test.py readme.txt [...] -# -# This could be implemented like: -# @depends_on("vim") -# def app_vim(self, context): -# return "vim", context -# -# But this is redundant and ranger does this automatically. However, sometimes -# you want to change some properties like flags or dependencies. -# The method "generic" defines a generic method for the given programs which -# looks like the one above, but you can add dependencies and flags here. -# Add programs (that are not defined yet) here if they should only run in X: -CustomApplications.generic('fceux', 'wine', 'zsnes', deps=['X']) - -# Add those which should only run in X AND should be detached/forked here: -CustomApplications.generic( - 'luakit', 'uzbl', 'vimprobable', 'vimprobable2', 'jumanji', - 'firefox', 'seamonkey', 'iceweasel', 'opera', - 'surf', 'midori', 'epiphany', 'konqueror', - 'evince', 'zathura', 'apvlv', 'okular', 'epdfview', 'mupdf', 'llpp', - 'eog', 'mirage', 'gimp', - 'libreoffice', 'soffice', 'ooffice', 'gnumeric', 'kspread', 'abiword', - 'gmplayer', 'smplayer', 'vlc', - flags='d', deps=['X']) - -# What filetypes are recognized as scripts for interpreted languages? -# This regular expression is used in app_default() -INTERPRETED_LANGUAGES = re.compile(r''' - ^(text|application)/x-( - haskell|perl|python|ruby|sh - )$''', re.VERBOSE) diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py index d68bc656..7665c99f 100644 --- a/ranger/ext/accumulator.py +++ b/ranger/ext/accumulator.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.ext.direction import Direction diff --git a/ranger/ext/cached_function.py b/ranger/ext/cached_function.py index 4d9ded18..ad7c5c11 100644 --- a/ranger/ext/cached_function.py +++ b/ranger/ext/cached_function.py @@ -1,17 +1,5 @@ # Copyright (C) 2012 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/>. +# This software is distributed under the terms of the GNU GPL version 3. def cached_function(fnc): cache = {} diff --git a/ranger/ext/curses_interrupt_handler.py b/ranger/ext/curses_interrupt_handler.py index 7c5b153e..eb86e0a9 100644 --- a/ranger/ext/curses_interrupt_handler.py +++ b/ranger/ext/curses_interrupt_handler.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ This module can catch interrupt signals which would otherwise diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 8a37c54a..742f54da 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ Directions provide convenient methods for movement operations. diff --git a/ranger/ext/get_executables.py b/ranger/ext/get_executables.py index 03f438dd..0c01ffd6 100644 --- a/ranger/ext/get_executables.py +++ b/ranger/ext/get_executables.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from stat import S_IXOTH, S_IFREG from ranger.ext.iter_tools import unique @@ -23,13 +11,11 @@ _cached_executables = None def get_executables(): """ - Return all executable files in each of the given directories. - - Looks in $PATH by default. + Return all executable files in $PATH. Cached version. """ global _cached_executables if _cached_executables is None: - _cached_executables = sorted(get_executables_uncached()) + _cached_executables = get_executables_uncached() return _cached_executables diff --git a/ranger/ext/human_readable.py b/ranger/ext/human_readable.py index 1a667519..9667421a 100644 --- a/ranger/ext/human_readable.py +++ b/ranger/ext/human_readable.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. def human_readable(byte, separator=' '): """ diff --git a/ranger/ext/iter_tools.py b/ranger/ext/iter_tools.py index e515fa07..341230f4 100644 --- a/ranger/ext/iter_tools.py +++ b/ranger/ext/iter_tools.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from collections import deque diff --git a/ranger/ext/keybinding_parser.py b/ranger/ext/keybinding_parser.py index 295855fb..eed85a2b 100644 --- a/ranger/ext/keybinding_parser.py +++ b/ranger/ext/keybinding_parser.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import sys import copy diff --git a/ranger/ext/mount_path.py b/ranger/ext/mount_path.py index 31d6c602..19747eba 100644 --- a/ranger/ext/mount_path.py +++ b/ranger/ext/mount_path.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from os.path import realpath, abspath, dirname, ismount diff --git a/ranger/ext/next_available_filename.py b/ranger/ext/next_available_filename.py index 696063cf..705e1558 100644 --- a/ranger/ext/next_available_filename.py +++ b/ranger/ext/next_available_filename.py @@ -1,17 +1,5 @@ # Copyright (C) 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import os.path diff --git a/ranger/ext/openstruct.py b/ranger/ext/openstruct.py index b538d384..fa521d34 100644 --- a/ranger/ext/openstruct.py +++ b/ranger/ext/openstruct.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. # prepend __ to arguments because one might use "args" # or "keywords" as a keyword argument. diff --git a/ranger/ext/relative_symlink.py b/ranger/ext/relative_symlink.py index 420f186c..de1cb908 100644 --- a/ranger/ext/relative_symlink.py +++ b/ranger/ext/relative_symlink.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from os import symlink, sep diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py new file mode 100755 index 00000000..feef4cac --- /dev/null +++ b/ranger/ext/rifle.py @@ -0,0 +1,420 @@ +#!/usr/bin/python -S +# Copyright (C) 2012 Roman Zimbelmann <romanz@lavabit.com> +# This software is distributed under the terms of the GNU GPL version 3. + +""" +rifle, the file executor/opener of ranger + +This can be used as a standalone program or can be embedded in python code. +When used together with ranger, it doesn't have to be installed to $PATH. + +Example usage: + + rifle = Rifle("rilfe.conf") + rifle.reload_config() + rifle.execute(["file1", "file2"]) +""" + +import os.path +import re +from subprocess import Popen, PIPE +import sys + +DEFAULT_PAGER = 'less' +DEFAULT_EDITOR = 'nano' +ASK_COMMAND = 'ask' +ENCODING = 'utf-8' + +try: + from ranger.ext.get_executables import get_executables +except ImportError: + _cached_executables = None + + def get_executables(): + """ + Return all executable files in $PATH + Cache them. + """ + global _cached_executables + if _cached_executables is not None: + return _cached_executables + + if 'PATH' in os.environ: + paths = os.environ['PATH'].split(':') + else: + paths = ['/usr/bin', '/bin'] + + from stat import S_IXOTH, S_IFREG + paths_seen = set() + _cached_executables = set() + for path in paths: + if path in paths_seen: + continue + paths_seen.add(path) + try: + content = os.listdir(path) + except OSError: + continue + for item in content: + abspath = path + '/' + item + try: + filestat = os.stat(abspath) + except OSError: + continue + if filestat.st_mode & (S_IXOTH | S_IFREG): + _cached_executables.add(item) + return _cached_executables + + +def _is_terminal(): + # Check if stdin (file descriptor 0), stdout (fd 1) and + # stderr (fd 2) are connected to a terminal + try: + os.ttyname(0) + os.ttyname(1) + os.ttyname(2) + except: + return False + return True + + +def squash_flags(flags): + """ + Remove lowercase flags if the respective uppercase flag exists + + >>> squash_flags('abc') + 'abc' + >>> squash_flags('abcC') + 'ab' + >>> squash_flags('CabcAd') + 'bd' + """ + exclude = ''.join(f.upper() + f.lower() for f in flags if f == f.upper()) + return ''.join(f for f in flags if f not in exclude) + + +class Rifle(object): + delimiter1 = '=' + delimiter2 = ',' + + def hook_before_executing(self, command, mimetype, flags): + pass + + def hook_after_executing(self, command, mimetype, flags): + pass + + def hook_command_preprocessing(self, command): + return command + + def hook_command_postprocessing(self, command): + return command + + def hook_environment(self, env): + return env + + def hook_logger(self, string): + sys.stderr.write(string + "\n") + + def __init__(self, config_file): + self.config_file = config_file + self._app_flags = '' + self._app_label = None + + def reload_config(self, config_file=None): + """Replace the current configuration with the one in config_file""" + if config_file is None: + config_file = self.config_file + f = open(config_file, 'r') + self.rules = [] + lineno = 1 + for line in f: + if line.startswith('#') or line == '\n': + continue + line = line.strip() + try: + if self.delimiter1 not in line: + raise Exception("Line without delimiter") + tests, command = line.split(self.delimiter1, 1) + tests = tests.split(self.delimiter2) + tests = tuple(tuple(f.strip().split(None, 1)) for f in tests) + command = command.strip() + self.rules.append((command, tests)) + except Exception as e: + self.hook_logger("Syntax error in %s line %d (%s)" % \ + (config_file, lineno, str(e))) + lineno += 1 + f.close() + + def _eval_condition(self, condition, files, label): + # Handle the negation of conditions starting with an exclamation mark, + # then pass on the arguments to _eval_condition2(). + + if not condition: + return True + if condition[0].startswith('!'): + new_condition = tuple([condition[0][1:]]) + tuple(condition[1:]) + return not self._eval_condition2(new_condition, files, label) + return self._eval_condition2(condition, files, label) + + def _eval_condition2(self, rule, files, label): + # This function evaluates the condition, after _eval_condition() handled + # negation of conditions starting with a "!". + + if not files: + return False + + function = rule[0] + argument = rule[1] if len(rule) > 1 else '' + + if function == 'ext': + extension = os.path.basename(files[0]).rsplit('.', 1)[-1].lower() + return bool(re.search('^(' + argument + ')$', extension)) + elif function == 'name': + return bool(re.search(argument, os.path.basename(files[0]))) + elif function == 'match': + return bool(re.search(argument, files[0])) + elif function == 'file': + return os.path.isfile(files[0]) + elif function == 'directory': + return os.path.isdir(files[0]) + elif function == 'path': + return bool(re.search(argument, os.path.abspath(files[0]))) + elif function == 'mime': + return bool(re.search(argument, self._get_mimetype(files[0]))) + elif function == 'has': + return argument in get_executables() + elif function == 'terminal': + return _is_terminal() + elif function == 'number': + if argument.isdigit(): + self._skip = int(argument) + return True + elif function == 'label': + self._app_label = argument + if label: + return argument == label + return True + elif function == 'flag': + self._app_flags = argument + return True + elif function == 'X': + return 'DISPLAY' in os.environ + elif function == 'else': + return True + + def _get_mimetype(self, fname): + # Spawn "file" to determine the mime-type of the given file. + if self._mimetype: + return self._mimetype + process = Popen(["file", "--mime-type", "-Lb", fname], + stdout=PIPE, stderr=PIPE) + mimetype, _ = process.communicate() + self._mimetype = mimetype.decode(ENCODING).strip() + return self._mimetype + + def _build_command(self, files, action, flags): + # Get the flags + if isinstance(flags, str): + self._app_flags += flags + self._app_flags = squash_flags(self._app_flags) + flags = self._app_flags + + _filenames = "' '".join(f.replace("'", "'\\\''") for f in files + if "\x00" not in f) + command = "set -- '%s'" % _filenames + '\n' + + # Apply flags + command += action + return command + + def _apply_flags(self, action, flags): + # FIXME: Flags do not work properly when pipes are in the command. + # NOTE: "r" flag is handled in execute() + if 't' in flags: + if 'TERMCMD' not in os.environ: + term = os.environ['TERM'] + if term.startswith('rxvt-unicode'): + term = 'urxvt' + if term not in get_executables(): + self.hook_logger("Can not determine terminal command. " + "Please set $TERMCMD manually.") + os.environ['TERMCMD'] = term + action = "$TERMCMD -e %s" % action + if 'f' in flags: + if 'setsid' in get_executables(): + action = "setsid %s > /dev/null 2> /dev/null &" % action + else: + action = "nohup %s > /dev/null 2> /dev/null &" % action + return action + + def list_commands(self, files, mimetype=None): + """ + Returns one 4-tuple for all currently applicable commands + The 4-tuple contains (count, command, label, flags). + count is the index, counted from 0 upwards, + command is the command that will be executed. + label and flags are the label and flags specified in the rule. + """ + self._mimetype = mimetype + count = -1 + for cmd, tests in self.rules: + self._skip = None + self._app_flags = '' + self._app_label = None + for test in tests: + if not self._eval_condition(test, files, None): + break + else: + if self._skip is None: + count += 1 + else: + count = self._skip + yield (count, cmd, self._app_label, self._app_flags) + + def execute(self, files, number=0, label=None, flags="", mimetype=None): + """ + Executes the given list of files. + + By default, this executes the first command where all conditions apply, + but by specifying number=N you can run the 1+Nth command. + + If a label is specified, only rules with this label will be considered. + + If you specify the mimetype, rifle will not try to determine it itself. + + By specifying a flag, you extend the flag that is defined in the rule. + Uppercase flags negate the respective lowercase flags. + For example: if the flag in the rule is "pw" and you specify "Pf", then + the "p" flag is negated and the "f" flag is added, resulting in "wf". + """ + command = None + found_at_least_one = None + + # Determine command + for count, cmd, lbl, flgs in self.list_commands(files, mimetype): + if label and label == lbl or not label and count == number: + cmd = self.hook_command_preprocessing(cmd) + if cmd == ASK_COMMAND: + return ASK_COMMAND + command = self._build_command(files, cmd, flags + flgs) + flags = self._app_flags + break + else: + found_at_least_one = True + else: + if label and label in get_executables(): + cmd = '%s "$@"' % label + command = self._build_command(files, cmd, flags) + + # Execute command + if command is None: + if found_at_least_one: + if label: + self.hook_logger("Label '%s' is undefined" % label) + else: + self.hook_logger("Method number %d is undefined." % number) + else: + self.hook_logger("No action found.") + else: + if 'PAGER' not in os.environ: + os.environ['PAGER'] = DEFAULT_PAGER + if 'EDITOR' not in os.environ: + os.environ['EDITOR'] = DEFAULT_EDITOR + command = self.hook_command_postprocessing(command) + self.hook_before_executing(command, self._mimetype, self._app_flags) + try: + if 'r' in flags: + prefix = ['sudo', '-E', 'su', '-mc'] + else: + prefix = ['/bin/sh', '-c'] + if 't' in flags: + if 'TERMCMD' not in os.environ: + term = os.environ['TERM'] + if term.startswith('rxvt-unicode'): + term = 'urxvt' + if term not in get_executables(): + self.hook_logger("Can not determine terminal command. " + "Please set $TERMCMD manually.") + os.environ['TERMCMD'] = term + cmd = [os.environ['TERMCMD'], '-e'] + prefix + [command] + elif 'f' in flags: + exe = 'setsid' if 'setsid' in get_executables() else 'nohup' + cmd = [exe] + prefix + [command] + else: + cmd = prefix + [command] + if 'f' in flags or 't' in flags: + devnull_r = open(os.devnull, 'r') + devnull_w = open(os.devnull, 'w') + p = Popen(cmd, env=self.hook_environment(os.environ), + stdin=devnull_r, stdout=devnull_w, stderr=devnull_w) + else: + p = Popen(cmd, env=self.hook_environment(os.environ)) + p.wait() + finally: + self.hook_after_executing(command, self._mimetype, self._app_flags) + + +def main(): + """The main function which is run when you start this program direectly.""" + import sys + + # Find configuration file path + if 'XDG_CONFIG_HOME' in os.environ and os.environ['XDG_CONFIG_HOME']: + conf_path = os.environ['XDG_CONFIG_HOME'] + '/ranger/rifle.conf' + else: + conf_path = os.path.expanduser('~/.config/ranger/rifle.conf') + if not os.path.isfile(conf_path): + conf_path = os.path.normpath(os.path.join(os.path.dirname(__file__), + '../config/rifle.conf')) + + # Evaluate arguments + from optparse import OptionParser + parser = OptionParser(usage="%prog [-fhlpw] [files]") + parser.add_option('-f', type="string", default="", metavar="FLAGS", + help="use additional flags: f=fork, r=root, t=terminal. " + "Uppercase flag negates respective lowercase flags.") + parser.add_option('-l', action="store_true", + help="list possible ways to open the files (id:label:flags:command)") + parser.add_option('-p', type='string', default='0', metavar="KEYWORD", + help="pick a method to open the files. KEYWORD is either the " + "number listed by 'rifle -l' or a string that matches a label in " + "the configuration file") + parser.add_option('-w', type='string', default=None, metavar="PROGRAM", + help="open the files with PROGRAM") + options, positional = parser.parse_args() + if not positional: + parser.print_help() + raise SystemExit(1) + + if options.p.isdigit(): + number = int(options.p) + label = None + else: + number = 0 + label = options.p + + if options.w is not None and not options.l: + p = Popen([options.w] + list(positional)) + p.wait() + else: + # Start up rifle + rifle = Rifle(conf_path) + rifle.reload_config() + #print(rifle.list_commands(sys.argv[1:])) + if options.l: + for count, cmd, label, flags in rifle.list_commands(positional): + print("%d:%s:%s:%s" % (count, label or '', flags, cmd)) + else: + result = rifle.execute(positional, number=number, label=label, + flags=options.f) + if result == ASK_COMMAND: + # TODO: implement interactive asking for file type? + print("Unknown file type: %s" % rifle._get_mimetype(positional[0])) + + + +if __name__ == '__main__': + if 'RANGER_DOCTEST' in os.environ: + import doctest + doctest.testmod() + else: + main() diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py index b68afc33..c9a22074 100644 --- a/ranger/ext/shell_escape.py +++ b/ranger/ext/shell_escape.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ Functions to escape metacharacters of arguments for shell commands. diff --git a/ranger/ext/signals.py b/ranger/ext/signals.py index 0df39fe0..c3e546e3 100644 --- a/ranger/ext/signals.py +++ b/ranger/ext/signals.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ An efficient and minimalistic signaling/hook module. diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py index 5e5d6a95..0f3a629a 100644 --- a/ranger/ext/spawn.py +++ b/ranger/ext/spawn.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from subprocess import Popen, PIPE ENCODING = 'utf-8' diff --git a/ranger/ext/widestring.py b/ranger/ext/widestring.py index 525e1bc1..44972c9c 100644 --- a/ranger/ext/widestring.py +++ b/ranger/ext/widestring.py @@ -1,18 +1,6 @@ # -*- encoding: utf8 -*- # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import sys from unicodedata import east_asian_width diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index 81e50ed9..823eaeaf 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -1,19 +1,8 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import os.path +import re from os import stat as os_stat, lstat as os_lstat from collections import deque from time import time @@ -95,6 +84,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): def __init__(self, path, **kw): assert not os.path.isfile(path), "No directory given!" + Loadable.__init__(self, None, None) Accumulator.__init__(self) FileSystemObject.__init__(self, path, **kw) @@ -176,6 +166,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): """ self.loading = True + self.percent = 0 self.load_if_outdated() try: @@ -185,8 +176,17 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): self.mount_path = mount_path(mypath) - hidden_filter = not self.settings.show_hidden \ - and self.settings.hidden_filter + if not self.settings.show_hidden and self.settings.hidden_filter: + # COMPAT + # hidden_filter used to be a regex, not a string. If an + # old config is used, we don't need to re.compile it. + if hasattr(self.settings.hidden_filter, 'search'): + hidden_filter = self.settings.hidden_filter + else: + hidden_filter = re.compile(self.settings.hidden_filter) + else: + hidden_filter = None + filelist = os.listdir(mypath) if self._cumulative_size_calculated: @@ -232,7 +232,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): is_a_dir = False if is_a_dir: try: - item = self.fm.env.get_directory(name) + item = self.fm.get_directory(name) item.load_if_outdated() except: item = Directory(name, preload=stats, @@ -243,6 +243,7 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): item.load() disk_usage += item.size files.append(item) + self.percent = 100 * len(files) // len(filenames) yield self.disk_usage = disk_usage @@ -456,8 +457,8 @@ class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): Accumulator.correct_pointer(self) try: - if self == self.fm.env.cwd: - self.fm.env.cf = self.pointed_obj + if self == self.fm.thisdir: + self.fm.thisfile = self.pointed_obj except: pass diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index 66bc549b..39146cd5 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import re from ranger.fsobject import FileSystemObject diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index 943c8aa4..073da74f 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -1,21 +1,14 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. CONTAINER_EXTENSIONS = ('7z', 'ace', 'ar', 'arc', 'bz', 'bz2', 'cab', 'cpio', 'cpt', 'deb', 'dgc', 'dmg', 'gz', 'iso', 'jar', 'msi', 'pkg', 'rar', 'shar', 'tar', 'tbz', 'tgz', 'xar', 'xpi', 'xz', 'zip') +DOCUMENT_EXTENSIONS = ('cfg', 'css', 'cvs', 'djvu', 'doc', 'docx', 'gnm', + 'gnumeric', 'htm', 'html', 'md', 'odf', 'odg', 'odp', 'ods', 'odt', 'pdf', + 'pod', 'ps', 'rtf', 'sxc', 'txt', 'xls', 'xlw', 'xml', 'xslx') +DOCUMENT_BASENAMES = ('bugs', 'bugs', 'changelog', 'copying', 'credits', + 'hacking', 'help', 'install', 'license', 'readme', 'todo') import re from os import lstat, stat @@ -84,6 +77,7 @@ class FileSystemObject(FileManagerAware): self.extension = splitext(self.basename)[1].lstrip(extsep) or None self.dirname = dirname(path) self.preload = preload + self.display_data = {} try: lastdot = self.basename.rindex('.') + 1 @@ -147,7 +141,9 @@ class FileSystemObject(FileManagerAware): 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') + self.document = self._mimetype.startswith('text') \ + or self.extension in DOCUMENT_EXTENSIONS \ + or self.basename.lower() in DOCUMENT_BASENAMES self.container = self.extension in CONTAINER_EXTENSIONS keys = ('video', 'audio', 'image', 'media', 'document', 'container') @@ -173,7 +169,7 @@ class FileSystemObject(FileManagerAware): return self._mimetype_tuple def mark(self, boolean): - directory = self.env.get_directory(self.dirname) + directory = self.fm.get_directory(self.dirname) directory.mark_item(self) def _mark(self, boolean): @@ -195,6 +191,7 @@ class FileSystemObject(FileManagerAware): filesystem and caches it for later use """ + self.display_data = {} self.fm.update_preview(self.path) self.loaded = True diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py index 65038120..96b95669 100644 --- a/ranger/gui/ansi.py +++ b/ranger/gui/ansi.py @@ -1,18 +1,6 @@ # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.com> # Copyright (C) 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ A library to help to convert ANSI codes to curses instructions. diff --git a/ranger/gui/bar.py b/ranger/gui/bar.py index 40218fb1..ae07dd35 100644 --- a/ranger/gui/bar.py +++ b/ranger/gui/bar.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.ext.widestring import WideString, utf_char_width import sys diff --git a/ranger/gui/color.py b/ranger/gui/color.py index 889f9e9a..037ff5c1 100644 --- a/ranger/gui/color.py +++ b/ranger/gui/color.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ Contains abbreviations to curses color/attribute constants. diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index cc72d6a8..b36048e2 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ Colorschemes define colors for specific contexts. @@ -44,7 +32,7 @@ from curses import color_pair import ranger from ranger.gui.color import get_color from ranger.gui.context import Context -from ranger.core.helper import allow_access_to_confdir +from ranger.core.main import allow_access_to_confdir from ranger.core.shared import SettingsAware from ranger.ext.cached_function import cached_function from ranger.ext.iter_tools import flatten diff --git a/ranger/gui/context.py b/ranger/gui/context.py index 3e7ca505..cdccecde 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'in_browser', 'in_statusbar', 'in_titlebar', 'in_console', @@ -26,7 +14,7 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo', 'marked', 'tagged', 'tag_marker', 'cut', 'copied', 'help_markup', # COMPAT 'seperator', 'key', 'special', 'border', # COMPAT - 'title', 'text', 'highlight', 'bars', 'quotes', 'tab', + 'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded', 'keybuffer'] class Context(object): diff --git a/ranger/gui/curses_shortcuts.py b/ranger/gui/curses_shortcuts.py index a383ab4c..43b583a6 100644 --- a/ranger/gui/curses_shortcuts.py +++ b/ranger/gui/curses_shortcuts.py @@ -1,18 +1,6 @@ # Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com> # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.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/>. +# This software is distributed under the terms of the GNU GPL version 3. import curses import _curses diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index 5e9562a7..cbd77fe4 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. from ranger.core.shared import FileManagerAware, EnvironmentAware from ranger.gui.curses_shortcuts import CursesShortcuts @@ -49,7 +37,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): win -- the own curses window object parent -- the parent (DisplayableContainer) object or None x, y, wid, hei -- absolute coordinates and boundaries - settings, fm, env -- inherited shared variables + settings, fm -- inherited shared variables """ def __init__(self, win, env=None, fm=None, settings=None): @@ -154,7 +142,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): """Resize the widget""" do_move = True try: - maxy, maxx = self.env.termsize + maxy, maxx = self.fm.ui.termsize except TypeError: pass else: diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py index cb697d8d..ed370e54 100644 --- a/ranger/gui/mouse_event.py +++ b/ranger/gui/mouse_event.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import curses diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 69b8463b..6f021b68 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import os import sys @@ -20,7 +8,7 @@ import _curses from .displayable import DisplayableContainer from .mouse_event import MouseEvent -from ranger.ext.keybinding_parser import ALT_KEY +from ranger.ext.keybinding_parser import KeyBuffer, KeyMaps, ALT_KEY TERMINALS_WITH_TITLE = ("xterm", "xterm-256color", "rxvt", "rxvt-256color", "rxvt-unicode", "rxvt-unicode-256color", @@ -53,23 +41,25 @@ class UI(DisplayableContainer): is_set_up = False load_mode = False is_on = False + termsize = None + def __init__(self, env=None, fm=None): self._draw_title = os.environ["TERM"] in TERMINALS_WITH_TITLE - os.environ['ESCDELAY'] = '25' # don't know a cleaner way + self.keybuffer = KeyBuffer() + self.keymaps = KeyMaps(self.keybuffer) - if env is not None: - self.env = env if fm is not None: self.fm = fm + def setup_curses(self): + os.environ['ESCDELAY'] = '25' # don't know a cleaner way try: self.win = curses.initscr() except _curses.error as e: if e.args[0] == "setupterm: could not find terminal": os.environ['TERM'] = 'linux' self.win = curses.initscr() - self.env.keymaps.use_keymap('browser') - + self.keymaps.use_keymap('browser') DisplayableContainer.__init__(self, None) def initialize(self): @@ -147,14 +137,14 @@ class UI(DisplayableContainer): self.hint() if key < 0: - self.env.keybuffer.clear() + self.keybuffer.clear() elif not DisplayableContainer.press(self, key): - self.env.keymaps.use_keymap('browser') + self.keymaps.use_keymap('browser') self.press(key) def press(self, key): - keybuffer = self.env.keybuffer + keybuffer = self.keybuffer self.status.clear_message() keybuffer.add(key) @@ -271,8 +261,8 @@ class UI(DisplayableContainer): def update_size(self): """resize all widgets""" - self.env.termsize = self.win.getmaxyx() - y, x = self.env.termsize + self.termsize = self.win.getmaxyx() + y, x = self.termsize self.browser.resize(1, 0, y - 2, x) self.taskview.resize(1, 0, y - 2, x) @@ -286,9 +276,9 @@ class UI(DisplayableContainer): self.win.touchwin() DisplayableContainer.draw(self) if self._draw_title and self.settings.update_title: - cwd = self.fm.env.cwd.path - if cwd.startswith(self.env.home_path): - cwd = '~' + cwd[len(self.env.home_path):] + cwd = self.fm.thisdir.path + if cwd.startswith(self.fm.home_path): + cwd = '~' + cwd[len(self.fm.home_path):] if self.settings.shorten_title: split = cwd.rsplit(os.sep, self.settings.shorten_title) if os.sep in split[0]: diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index e9c08439..1cb943d2 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -1,20 +1,9 @@ # -*- encoding: utf8 -*- # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """The BrowserColumn widget displays the contents of a directory or file.""" +import curses import stat from time import time @@ -32,7 +21,7 @@ class BrowserColumn(Pager): ellipsis = { False: '~', True: '…' } old_dir = None - old_cf = None + old_thisfile = None def __init__(self, win, level): """ @@ -86,7 +75,7 @@ class BrowserColumn(Pager): if clicked_file.is_directory: self.fm.enter_dir(clicked_file.path) elif self.level == 0: - self.fm.env.cwd.move_to_obj(clicked_file) + self.fm.thisdir.move_to_obj(clicked_file) self.fm.execute_file(clicked_file) except: pass @@ -97,6 +86,22 @@ class BrowserColumn(Pager): return True + def execute_curses_batch(self, line, commands): + """ + Executes a list of "commands" which can be easily cached. + + "commands" is a list of lists. Each element contains + a text and an attribute. First, the attribute will be + set with attrset, then the text is printed. + + Example: + execute_curses_batch(0, [["hello ", 0], ["world", curses.A_BOLD]]) + """ + self.win.move(line, 0) + for entry in commands: + text, attr = entry + self.addstr(text, attr) + def has_preview(self): if self.target is None: return False @@ -113,7 +118,7 @@ class BrowserColumn(Pager): def poke(self): Widget.poke(self) - self.target = self.env.at_level(self.level) + self.target = self.fm.thistab.at_level(self.level) def draw(self): """Call either _draw_file() or _draw_directory()""" @@ -126,9 +131,9 @@ class BrowserColumn(Pager): if self.target and self.target.is_directory \ and (self.level <= 0 or self.settings.preview_directories): - if self.target.pointed_obj != self.old_cf: + if self.target.pointed_obj != self.old_thisfile: self.need_redraw = True - self.old_cf = self.target.pointed_obj + self.old_thisfile = self.target.pointed_obj if self.target.load_content_if_outdated() \ or self.target.sort_if_outdated() \ @@ -200,31 +205,45 @@ class BrowserColumn(Pager): self._set_scroll_begin() - copied = [f.path for f in self.env.copy] + copied = [f.path for f in self.fm.copy_buffer] ellipsis = self.ellipsis[self.settings.unicode_ellipsis] selected_i = self.target.pointer for line in range(self.hei): i = line + self.scroll_begin + if line > self.hei: + break try: drawn = self.target.files[i] except IndexError: break + tagged = self.fm.tags and drawn.realpath in self.fm.tags + if tagged: + tagged_marker = self.fm.tags.marker(drawn.realpath) + else: + tagged_marker = " " + + key = (self.wid, selected_i == i, drawn.marked, self.main_column, + drawn.path in copied, tagged_marker, drawn.infostring) + + if key in drawn.display_data: + self.execute_curses_batch(line, drawn.display_data[key]) + self.color_reset() + continue + + display_data = [] + drawn.display_data[key] = display_data + if self.display_infostring and drawn.infostring \ and self.settings.display_size_in_main_column: infostring = str(drawn.infostring) + " " else: infostring = "" - bad_info_color = None this_color = base_color + list(drawn.mimetype_tuple) text = drawn.basename - tagged = self.fm.tags and drawn.realpath in self.fm.tags - - if tagged: - tagged_marker = self.fm.tags.marker(drawn.realpath) space = self.wid - len(infostring) if self.main_column: @@ -232,9 +251,6 @@ class BrowserColumn(Pager): elif self.settings.display_tags_in_all_columns: space -= 1 -# if len(text) > space: -# text = text[:space-1] + self.ellipsis - if i == selected_i: this_color.append('selected') @@ -245,8 +261,6 @@ class BrowserColumn(Pager): if tagged: this_color.append('tagged') - if self.main_column or self.settings.display_tags_in_all_columns: - text = tagged_marker + text if drawn.is_directory: this_color.append('directory') @@ -265,40 +279,45 @@ class BrowserColumn(Pager): this_color.append('device') if drawn.path in copied: - this_color.append('cut' if self.env.cut else 'copied') + this_color.append('cut' if self.fm.do_cut else 'copied') if drawn.is_link: this_color.append('link') this_color.append(drawn.exists and 'good' or 'bad') - wtext = WideString(text) - if len(wtext) > space: - wtext = wtext[:space - 1] + ellipsis - if self.main_column or self.settings.display_tags_in_all_columns: - if tagged: - self.addstr(line, 0, str(wtext)) - elif self.wid > 1: - self.addstr(line, 1, str(wtext)) - else: - self.addstr(line, 0, str(wtext)) - - if infostring: - x = self.wid - 1 - len(infostring) - if infostring is BAD_INFO: - bad_info_color = (x, len(infostring)) - if x > 0: - self.addstr(line, x, infostring) - - self.color_at(line, 0, self.wid, tuple(this_color)) - if bad_info_color: - start, wid = bad_info_color - self.color_at(line, start, wid, tuple(this_color), 'badinfo') + attr = self.settings.colorscheme.get_attr(*this_color) if (self.main_column or self.settings.display_tags_in_all_columns) \ and tagged and self.wid > 2: this_color.append('tag_marker') - self.color_at(line, 0, len(tagged_marker), tuple(this_color)) + tag_attr = self.settings.colorscheme.get_attr(*this_color) + display_data.append([tagged_marker, tag_attr]) + else: + text = " " + text + + wtext = WideString(text) + if len(wtext) > space: + wtext = wtext[:max(0, space - 1)] + ellipsis + text = str(wtext) + + display_data.append([text, attr]) + + padding = self.wid - len(wtext) + if tagged and (self.main_column or \ + self.settings.display_tags_in_all_columns): + padding -= 1 + if infostring: + if len(text) + 1 + len(infostring) > self.wid: + pass + else: + padding -= len(infostring) + padding = max(0, padding) + infostring = (" " * padding) + infostring + display_data.append([infostring, attr]) + else: + display_data.append([" " * max(0, padding), attr]) + self.execute_curses_batch(line, display_data) self.color_reset() def _get_scroll_begin(self): diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index ea04c1e0..3bfaf3fd 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """The BrowserView manages a set of BrowserColumns.""" import curses @@ -31,6 +19,7 @@ class BrowserView(Widget, DisplayableContainer): need_clear = False old_collapse = False draw_hints = False + draw_info = False def __init__(self, win, ratios, preview = True): DisplayableContainer.__init__(self, win) @@ -47,7 +36,7 @@ class BrowserView(Widget, DisplayableContainer): self.settings.signal_bind('setopt.' + option, self._request_clear_if_has_borders, weak=True) - self.fm.env.signal_bind('move', self.request_clear) + self.fm.signal_bind('move', self.request_clear) self.settings.signal_bind('setopt.column_ratios', self.request_clear) def change_ratios(self, ratios): @@ -98,9 +87,9 @@ class BrowserView(Widget, DisplayableContainer): self.win.erase() self.need_redraw = True self.need_clear = False - for path in self.fm.tabs.values(): - if path is not None: - directory = self.env.get_directory(path) + for tab in self.fm.tabs.values(): + directory = tab.thisdir + if directory: directory.load_content_if_outdated() directory.use() DisplayableContainer.draw(self) @@ -110,6 +99,8 @@ class BrowserView(Widget, DisplayableContainer): self._draw_bookmarks() elif self.draw_hints: self._draw_hints() + elif self.draw_info: + self._draw_info(self.draw_info) def finalize(self): if self.pager.visible: @@ -197,10 +188,23 @@ class BrowserView(Widget, DisplayableContainer): self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE) + def _draw_info(self, lines): + self.need_clear = True + hei = min(self.hei - 1, len(lines)) + ystart = self.hei - hei + i = ystart + whitespace = " " * self.wid + for line in lines: + if i >= self.hei: + break + self.addstr(i, 0, whitespace) + self.addnstr(i, 0, line, self.wid) + i += 1 + def _draw_hints(self): self.need_clear = True hints = [] - for k, v in self.fm.env.keybuffer.pointer.items(): + for k, v in self.fm.ui.keybuffer.pointer.items(): k = key_to_string(k) if isinstance(v, dict): text = '...' diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 2376ba5a..ba6dca86 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The Console widget implements a vim-like console for entering @@ -70,7 +58,10 @@ class Console(Widget): pass else: for entry in self.history_backup: - f.write(entry + '\n') + try: + f.write(entry + '\n') + except UnicodeEncodeError: + pass f.close() def draw(self): @@ -131,6 +122,7 @@ class Console(Widget): except: pass self.last_cursor_mode = None + self.fm.hide_console_info() self.add_to_history() self.tab_deque = None self.clear() @@ -145,7 +137,7 @@ class Console(Widget): self.line = '' def press(self, key): - self.env.keymaps.use_keymap('console') + self.fm.ui.keymaps.use_keymap('console') if not self.fm.ui.press(key): self.type_key(key) diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 38451781..cf156715 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -1,18 +1,6 @@ # Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com> # Copyright (C) 2010 David Barnett <davidbarnett2@gmail.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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The pager displays text and allows you to scroll inside it. @@ -109,7 +97,7 @@ class Pager(Widget): offset=-self.hei + 1) def press(self, key): - self.env.keymaps.use_keymap('pager') + self.fm.ui.keymaps.use_keymap('pager') self.fm.ui.press(key) def set_source(self, source, strip=False): diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 1ffb9fa3..bf5ee641 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The statusbar displays information about the current file and directory. @@ -21,6 +9,7 @@ print for the current file. The right side shows directory information such as the space used by all the files in this directory. """ +import os from pwd import getpwuid from grp import getgrgid from os import getuid, readlink @@ -38,7 +27,7 @@ class StatusBar(Widget): hint = None msg = None - old_cf = None + old_thisfile = None old_ctime = None old_du = None old_hint = None @@ -81,10 +70,10 @@ class StatusBar(Widget): self.msg = None self.need_redraw = True - if self.env.cf: - self.env.cf.load_if_outdated() + if self.fm.thisfile: + self.fm.thisfile.load_if_outdated() try: - ctime = self.env.cf.stat.st_ctime + ctime = self.fm.thisfile.stat.st_ctime except: ctime = -1 else: @@ -93,12 +82,12 @@ class StatusBar(Widget): if not self.result: self.need_redraw = True - if self.old_du and not self.env.cwd.disk_usage: - self.old_du = self.env.cwd.disk_usage + if self.old_du and not self.fm.thisdir.disk_usage: + self.old_du = self.fm.thisdir.disk_usage self.need_redraw = True - if self.old_cf != self.env.cf: - self.old_cf = self.env.cf + if self.old_thisfile != self.fm.thisfile: + self.old_thisfile = self.fm.thisfile self.need_redraw = True if self.old_ctime != ctime: @@ -151,7 +140,7 @@ class StatusBar(Widget): and self.column.target.is_directory: target = self.column.target.pointed_obj else: - target = self.env.at_level(0).pointed_obj + target = self.fm.thistab.at_level(0).pointed_obj try: stat = target.stat except: @@ -227,9 +216,9 @@ class StatusBar(Widget): max_pos = len(target) - self.column.hei base = 'scroll' - if self.env.cwd.filter: + if self.fm.thisdir.filter: right.add(" f=", base, 'filter') - right.add(repr(self.env.cwd.filter), base, 'filter') + right.add(repr(self.fm.thisdir.filter), base, 'filter') right.add(", ", "space") if target.marked_items: @@ -243,7 +232,7 @@ class StatusBar(Widget): else: right.add(human_readable(target.disk_usage, separator='') + " sum") try: - free = self.env.get_free_space(target.mount_path) + free = get_free_space(target.mount_path) except OSError: pass else: @@ -277,6 +266,10 @@ class StatusBar(Widget): self.addstr(str(part)) self.color_reset() +def get_free_space(path): + stat = os.statvfs(path) + return stat.f_bavail * stat.f_bsize + class Message(object): elapse = None text = None diff --git a/ranger/gui/widgets/taskview.py b/ranger/gui/widgets/taskview.py index a3f8e314..53da5826 100644 --- a/ranger/gui/widgets/taskview.py +++ b/ranger/gui/widgets/taskview.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The TaskView allows you to modify what the loader is doing. @@ -63,8 +51,14 @@ class TaskView(Widget, Accumulator): clr.append('selected') descr = obj.get_description() - self.addstr(y, 0, descr, self.wid) - self.color_at(y, 0, self.wid, tuple(clr)) + if obj.percent >= 0 and obj.percent <= 100: + self.addstr(y, 0, "%3d%% - %s" % (obj.percent, descr), self.wid) + wid = int(self.wid / 100.0 * obj.percent) + self.color_at(y, 0, self.wid, tuple(clr)) + self.color_at(y, 0, wid, tuple(clr), 'loaded') + else: + self.addstr(y, 0, descr, self.wid) + self.color_at(y, 0, self.wid, tuple(clr)) else: if self.hei > 1: @@ -92,7 +86,7 @@ class TaskView(Widget, Accumulator): self.fm.loader.move(_from=i, to=to) def press(self, key): - self.env.keymaps.use_keymap('taskview') + self.fm.ui.keymaps.use_keymap('taskview') self.fm.ui.press(key) def get_list(self): diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index 2b5e836b..d37a2fd3 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -1,17 +1,5 @@ # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. """ The titlebar is the widget at the top, giving you broad overview. @@ -25,7 +13,7 @@ from . import Widget from ranger.gui.bar import Bar class TitleBar(Widget): - old_cf = None + old_thisfile = None old_keybuffer = None old_wid = None result = None @@ -42,12 +30,12 @@ class TitleBar(Widget): def draw(self): if self.need_redraw or \ - self.env.cf != self.old_cf or\ - str(self.env.keybuffer) != str(self.old_keybuffer) or\ + self.fm.thisfile != self.old_thisfile or\ + str(self.fm.ui.keybuffer) != str(self.old_keybuffer) or\ self.wid != self.old_wid: self.need_redraw = False self.old_wid = self.wid - self.old_cf = self.env.cf + self.old_thisfile = self.fm.thisfile self._calc_bar() self._print_result(self.result) if self.wid > 2: @@ -103,20 +91,20 @@ class TitleBar(Widget): def _get_left_part(self, bar): # TODO: Properly escape non-printable chars without breaking unicode - if self.env.username == 'root': + if self.fm.username == 'root': clr = 'bad' else: clr = 'good' - bar.add(self.env.username, 'hostname', clr, fixed=True) + bar.add(self.fm.username, 'hostname', clr, fixed=True) bar.add('@', 'hostname', clr, fixed=True) - bar.add(self.env.hostname, 'hostname', clr, fixed=True) + bar.add(self.fm.hostname, 'hostname', clr, fixed=True) bar.add(':', 'hostname', clr, fixed=True) - pathway = self.env.pathway + pathway = self.fm.thistab.pathway if self.settings.tilde_in_titlebar and \ - self.fm.env.cwd.path.startswith(self.env.home_path): - pathway = pathway[self.env.home_path.count('/')+1:] + self.fm.thisdir.path.startswith(self.fm.home_path): + pathway = pathway[self.fm.home_path.count('/')+1:] bar.add('~/', 'directory', fixed=True) for path in pathway: @@ -128,12 +116,12 @@ class TitleBar(Widget): bar.add(path.basename, clr, directory=path) bar.add('/', clr, fixed=True, directory=path) - if self.env.cf is not None: - bar.add(self.env.cf.basename, 'file') + if self.fm.thisfile is not None: + bar.add(self.fm.thisfile.basename, 'file') def _get_right_part(self, bar): # TODO: fix that pressed keys are cut off when chaining CTRL keys - kb = str(self.env.keybuffer) + kb = str(self.fm.ui.keybuffer) self.old_keybuffer = kb bar.addright(kb, 'keybuffer', fixed=True) bar.addright(' ', 'space', fixed=True) @@ -148,7 +136,7 @@ class TitleBar(Widget): def _get_tab_text(self, tabname): result = ' ' + str(tabname) if self.settings.dirname_in_tabs: - dirname = basename(self.fm.tabs[tabname]) + dirname = basename(self.fm.tabs[tabname].path) if not dirname: result += ":/" elif len(dirname) > 15: diff --git a/scripts/ranger b/scripts/ranger new file mode 120000 index 00000000..21b7d3ee --- /dev/null +++ b/scripts/ranger @@ -0,0 +1 @@ +../ranger.py \ No newline at end of file diff --git a/scripts/rifle b/scripts/rifle new file mode 120000 index 00000000..1dbaa2d8 --- /dev/null +++ b/scripts/rifle @@ -0,0 +1 @@ +../ranger/ext/rifle.py \ No newline at end of file diff --git a/setup.py b/setup.py index ec782794..d8de3130 100755 --- a/setup.py +++ b/setup.py @@ -1,18 +1,6 @@ #!/usr/bin/env python # Copyright (C) 2009, 2010, 2011 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/>. +# This software is distributed under the terms of the GNU GPL version 3. import distutils.core import ranger @@ -27,15 +15,16 @@ if __name__ == '__main__': author_email=ranger.__email__, license=ranger.__license__, url='http://savannah.nongnu.org/projects/ranger', - scripts=['ranger/data/ranger'], + scripts=['scripts/ranger', 'scripts/rifle'], data_files=[('share/man/man1', ['doc/ranger.1'])], - package_data={'ranger': ['data/*', 'defaults/rc.conf']}, + package_data={'ranger': ['data/*', 'config/rc.conf', + 'config/rifle.conf']}, packages=('ranger', 'ranger.api', 'ranger.colorschemes', 'ranger.container', 'ranger.core', - 'ranger.defaults', + 'ranger.config', 'ranger.ext', 'ranger.fsobject', 'ranger.gui', |