From 0128bee71f734138d3f49f56092688bb0623c6a2 Mon Sep 17 00:00:00 2001 From: hut Date: Thu, 11 Mar 2010 20:27:25 +0100 Subject: working on the configuration system / main method --- INSTALL | 2 +- TODO | 1 + ranger/__init__.py | 37 +++---- ranger/__main__.py | 106 +++++++++++++------- ranger/api/apps.py | 4 +- ranger/colorschemes/__init__.py | 19 ++-- ranger/container/bookmarks.py | 8 ++ ranger/container/tags.py | 4 + ranger/core/__init__.py | 0 ranger/core/fm.py | 135 +++++++++++++++++++++++++ ranger/core/runner.py | 191 ++++++++++++++++++++++++++++++++++++ ranger/defaults/apps.py | 3 +- ranger/defaults/options.py | 41 +++++--- ranger/ext/openstruct.py | 7 +- ranger/fm.py | 130 ------------------------ ranger/gui/widgets/browsercolumn.py | 2 +- ranger/gui/widgets/console.py | 2 +- ranger/help/starting.py | 4 +- ranger/runner.py | 191 ------------------------------------ ranger/shared/settings.py | 84 ++++++++-------- 20 files changed, 520 insertions(+), 451 deletions(-) create mode 100644 ranger/core/__init__.py create mode 100644 ranger/core/fm.py create mode 100644 ranger/core/runner.py delete mode 100644 ranger/fm.py delete mode 100644 ranger/runner.py diff --git a/INSTALL b/INSTALL index 4635478b..269140c6 100644 --- a/INSTALL +++ b/INSTALL @@ -27,4 +27,4 @@ To install ranger, follow this instructions: alias rng="source ranger ranger" (Unfortunately this feature is shell dependent. It has been - successfully tested with BASH only.) + successfully tested with BASH and ZSH only.) diff --git a/TODO b/TODO index 41b5497e..81af21db 100644 --- a/TODO +++ b/TODO @@ -69,6 +69,7 @@ Bugs (X) #62 10/02/15 curs_set can raise an exception (X) #65 10/02/16 "source ranger ranger some/file.txt" shouldn't cd after exit ( ) #67 10/03/08 terminal title in tty + ( ) #69 10/03/11 tab-completion breaks with Apps subclass Ideas diff --git a/ranger/__init__.py b/ranger/__init__.py index f6913bb5..8bd978d7 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -18,12 +18,6 @@ import os import sys -LOGFILE = '/tmp/errorlog' - -__copyright__ = """ -Copyright (C) 2009, 2010 Roman Zimbelmann -""" - __license__ = 'GPL3' __version__ = '1.0.3' __credits__ = 'Roman Zimbelmann' @@ -31,34 +25,41 @@ __author__ = 'Roman Zimbelmann' __maintainer__ = 'Roman Zimbelmann' __email__ = 'romanz@lavabit.com' -debug = False - -CONFDIR = os.path.expanduser(os.path.join('~', '.config', 'ranger')) -RANGERDIR = os.path.dirname(__file__) - -sys.path.append(CONFDIR) +__copyright__ = """ +Copyright (C) 2009, 2010 Roman Zimbelmann +""" USAGE = '%prog [options] [path/filename]' +DEFAULT_CONFDIR = '~/.ranger' +RANGERDIR = os.path.dirname(__file__) +LOGFILE = '/tmp/errorlog' #for python3-only versions, this could be replaced with: -# #def log(*objects, start='ranger:', sep=' ', end='\n'): # print(start, *objects, end=end, sep=sep, file=open(LOGFILE, 'a')) def log(*objects, **keywords): - """Writes objects to a logfile. - Has the same arguments as print() in python3""" + """ + Writes objects to a logfile (for the purpose of debugging only.) + Has the same arguments as print() in python3. + """ + if LOGFILE is None 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 relpath_conf(*paths): + """returns the path relative to rangers configuration directory""" + if arg.clean: + assert 0, "Should not access relpath_conf in clean mode!" + else: + return os.path.join(arg.confdir, *paths) + def relpath(*paths): """returns the path relative to rangers library directory""" return os.path.join(RANGERDIR, *paths) -def relpath_conf(*paths): - """returns the path relative to rangers configuration directory""" - return os.path.join(CONFDIR, *paths) from ranger.__main__ import main diff --git a/ranger/__main__.py b/ranger/__main__.py index 72b4adf8..dba229bc 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -19,6 +19,65 @@ import os import sys + +def parse_arguments(): + """Parse the program arguments""" + + from optparse import OptionParser, SUPPRESS_HELP + from ranger.ext.openstruct import OpenStruct + from ranger import __version__, USAGE, DEFAULT_CONFDIR + + parser = OptionParser(usage=USAGE, version='ranger ' + __version__) + + # Instead of using this directly, use the embedded + # shell script by running ranger with: + # source /path/to/ranger /path/to/ranger + parser.add_option('--cd-after-exit', + action='store_true', + help=SUPPRESS_HELP) + + 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. " \ + "This will disable certain features. (tagging, bookmarks)") + + parser.add_option('-r', '--confdir', dest='confdir', type='string', + default=DEFAULT_CONFDIR, + help="the configuration directory. (%default)") + + parser.add_option('-m', '--mode', type='int', dest='mode', default=0, + help="if a filename is supplied, run it with this mode") + + parser.add_option('-f', '--flags', type='string', dest='flags', default='', + help="if a filename is supplied, run it with these flags.") + + options, positional = parser.parse_args() + + arg = OpenStruct(options.__dict__, targets=positional) + + arg.confdir = os.path.expanduser(arg.confdir) + + if arg.cd_after_exit: + sys.stderr = sys.__stdout__ + + try: + os.makedirs(arg.confdir) + except OSError as err: + if err.errno != 17: # 17 means it already exists + print("This configuration directory could not be created:") + print(arg.confdir) + print("To run ranger without the need for configuration files") + print("use the --clean option (not implemented yet)") + raise SystemExit() + + if not arg.clean: +# sys.path[0:0] = (arg.confdir, ) + sys.path.append(arg.confdir) + + return arg + def main(): """initialize objects and run the filemanager""" try: @@ -30,12 +89,10 @@ def main(): from signal import signal, SIGINT from locale import setlocale, LC_ALL - from optparse import OptionParser, SUPPRESS_HELP import ranger from ranger.ext import curses_interrupt_handler - from ranger import __version__, USAGE, CONFDIR - from ranger.fm import FM + from ranger.core.fm import FM from ranger.container.environment import Environment from ranger.shared.settings import SettingsAware from ranger.gui.defaultui import DefaultUI as UI @@ -45,50 +102,23 @@ def main(): setlocale(LC_ALL, 'en_US.utf8') except: pass - os.stat_float_times(True) - curses_interrupt_handler.install_interrupt_handler() - - if not os.path.exists(CONFDIR): - os.mkdir(CONFDIR) + curses_interrupt_handler.install_interrupt_handler() - # Parse options - parser = OptionParser(usage=USAGE, version='ranger ' + __version__) - - # Instead of using this directly, use the embedded - # shell script by running ranger with: - # source /path/to/ranger /path/to/ranger - parser.add_option('--cd-after-exit', - action='store_true', - help=SUPPRESS_HELP) - - parser.add_option('-m', type='int', dest='mode', default=0, - help="if a filename is supplied, run it with this mode") - - parser.add_option('-f', type='string', dest='flags', default='', - help="if a filename is supplied, run it with these flags.") - - parser.add_option('-d', '--debug', action='store_true', - help="activate debug mode") - - args, rest = parser.parse_args() - - if args.cd_after_exit: - sys.stderr = sys.__stdout__ - - ranger.debug = args.debug + arg = parse_arguments() + ranger.arg = arg SettingsAware._setup() # Initialize objects - target = ' '.join(rest) - if target: + if arg.targets: + target = arg.target[0] if not os.access(target, os.F_OK): print("File or directory doesn't exist: %s" % target) sys.exit(1) elif os.path.isfile(target): thefile = File(target) - FM().execute_file(thefile, mode=args.mode, flags=args.flags) + FM().execute_file(thefile, mode=arg.mode, flags=arg.flags) sys.exit(0) else: path = target @@ -100,7 +130,7 @@ def main(): try: my_ui = UI() my_fm = FM(ui=my_ui) - my_fm.stderr_to_out = args.cd_after_exit + my_fm.stderr_to_out = arg.cd_after_exit # Run the file manager my_fm.initialize() @@ -110,7 +140,7 @@ def main(): # Finish, clean up if 'my_ui' in vars(): my_ui.destroy() - if args.cd_after_exit: + if arg.cd_after_exit: try: sys.__stderr__.write(my_fm.env.pwd.path) except: pass diff --git a/ranger/api/apps.py b/ranger/api/apps.py index 743fa248..eadc0839 100644 --- a/ranger/api/apps.py +++ b/ranger/api/apps.py @@ -14,7 +14,7 @@ # along with this program. If not, see . """ -This module provides helper functions/classes for ranger.defaults.apps. +This module provides helper functions/classes for ranger.apps. """ import os, sys, re @@ -26,7 +26,7 @@ from ranger.shared import FileManagerAware class Applications(FileManagerAware): """ This class contains definitions on how to run programs and should - be extended in ranger.defaults.apps + 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 diff --git a/ranger/colorschemes/__init__.py b/ranger/colorschemes/__init__.py index ed5413d8..422c6598 100644 --- a/ranger/colorschemes/__init__.py +++ b/ranger/colorschemes/__init__.py @@ -22,22 +22,15 @@ from os.path import expanduser, dirname, exists, join __all__ = get_all_modules(dirname(__file__)) -from ranger.colorschemes import * -from ranger import relpath_conf - -if exists(relpath_conf('colorschemes')): - initpy = relpath_conf('colorschemes', '__init__.py') - if not exists(initpy): - open(initpy, 'w').write("""# Automatically generated: +if not ranger.arg.clean: + if exists(ranger.relpath_conf('colorschemes')): + initpy = ranger.relpath_conf('colorschemes', '__init__.py') + if not exists(initpy): + open(initpy, 'w').write("""# Automatically generated: from ranger.ext.get_all_modules import get_all_modules from os.path import dirname __all__ = get_all_modules(dirname(__file__)) """) - try: - import sys - sys.path[0:0] = [ranger.CONFDIR] - from colorschemes import * - except ImportError: - pass +from ranger.colorschemes import * diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index a0c757ca..d4e12f62 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -150,6 +150,8 @@ class Bookmarks(object): This is done automatically after every modification if autosave is True.""" import os self.update() + if self.path is None: + return if os.access(self.path, os.W_OK): f = open(self.path, 'w') for key, value in self.dct.items(): @@ -163,6 +165,10 @@ class Bookmarks(object): def _load_dict(self): import os dct = {} + + if self.path is None: + return dct + if not os.path.exists(self.path): try: f = open(self.path, 'w') @@ -193,6 +199,8 @@ class Bookmarks(object): def _get_mtime(self): import os + if self.path is None: + return None try: return os.stat(self.path).st_mtime except OSError: diff --git a/ranger/container/tags.py b/ranger/container/tags.py index 70a4aa3d..11ac3a5d 100644 --- a/ranger/container/tags.py +++ b/ranger/container/tags.py @@ -81,3 +81,7 @@ class Tags(object): for line in f: result.add(line.strip()) return result + + def __nonzero__(self): + return True + __bool__ = __nonzero__ diff --git a/ranger/core/__init__.py b/ranger/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ranger/core/fm.py b/ranger/core/fm.py new file mode 100644 index 00000000..83426a04 --- /dev/null +++ b/ranger/core/fm.py @@ -0,0 +1,135 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# 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 . + +from time import time +from collections import deque + +import ranger +from ranger.actions import Actions +from ranger.container import Bookmarks +from ranger.core.runner import Runner +from ranger import relpath_conf +from ranger.ext.get_executables import get_executables +from ranger import __version__ +from ranger.fsobject import Loader + +CTRL_C = 3 +TICKS_BEFORE_COLLECTING_GARBAGE = 100 + +class FM(Actions): + input_blocked = False + input_blocked_until = 0 + stderr_to_out = False + def __init__(self, ui=None, bookmarks=None, tags=None): + """Initialize FM.""" + Actions.__init__(self) + self.ui = ui + self.log = deque(maxlen=20) + self.bookmarks = bookmarks + self.tags = tags + self.loader = Loader() + self._executables = None + self.apps = self.settings.apps.CustomApplications() + + def mylogfunc(text): + self.notify(text, bad=True) + self.run = Runner(ui=self.ui, apps=self.apps, + logfunc=mylogfunc) + + from ranger.shared import FileManagerAware + FileManagerAware.fm = self + + @property + def executables(self): + if self._executables is None: + self._executables = sorted(get_executables()) + return self._executables + + def initialize(self): + """If ui/bookmarks are None, they will be initialized here.""" + from ranger.fsobject.directory import Directory + + if self.bookmarks is None: + if ranger.arg.clean: + bookmarkfile = None + else: + bookmarkfile = relpath_conf('bookmarks') + self.bookmarks = Bookmarks( + bookmarkfile=bookmarkfile, + bookmarktype=Directory, + autosave=self.settings.autosave_bookmarks) + self.bookmarks.load() + + else: + self.bookmarks = bookmarks + + from ranger.container.tags import Tags + if not ranger.arg.clean and self.tags is None: + self.tags = Tags(relpath_conf('tagged')) + + if self.ui is None: + from ranger.gui.defaultui import DefaultUI + self.ui = DefaultUI() + self.ui.initialize() + + def block_input(self, sec=0): + self.input_blocked = sec != 0 + self.input_blocked_until = time() + sec + + def loop(self): + """ + The main loop consists of: + 1. reloading bookmarks if outdated + 2. letting the loader work + 3. drawing and finalizing ui + 4. reading and handling user input + 5. after X loops: collecting unused directory objects + """ + + self.env.enter_dir(self.env.path) + + gc_tick = 0 + + try: + while True: + self.bookmarks.update_if_outdated() + self.loader.work() + if hasattr(self.ui, 'throbber'): + if self.loader.has_work(): + self.ui.throbber(self.loader.status) + else: + self.ui.throbber(remove=True) + + self.ui.redraw() + + self.ui.set_load_mode(self.loader.has_work()) + + key = self.ui.get_next_key() + + if key > 0: + if self.input_blocked and \ + time() > self.input_blocked_until: + self.input_blocked = False + if not self.input_blocked: + self.ui.handle_key(key) + + gc_tick += 1 + if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE: + gc_tick = 0 + self.env.garbage_collect() + + finally: + self.bookmarks.remember(self.env.pwd) + self.bookmarks.save() diff --git a/ranger/core/runner.py b/ranger/core/runner.py new file mode 100644 index 00000000..26424881 --- /dev/null +++ b/ranger/core/runner.py @@ -0,0 +1,191 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann +# +# 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 . + +""" +This module is an abstract layer over subprocess.Popen + +It gives you highlevel control about how processes are run. + +Example: +run = Runner(logfunc=print) +run('sleep 2', wait=True) # waits until the process exists +run(['ls', '--help'], flags='p') # pipes output to pager +run() # prints an error message + +List of allowed flags: +s: silent mode. output will be discarded. +d: detach the process. +p: redirect output to the pager +(An uppercase key ensures that a certain flag will not be used.) +""" + +import os +import sys +from subprocess import Popen, PIPE +from ranger.ext.waitpid_no_intr import waitpid_no_intr + + +ALLOWED_FLAGS = 'sdpSDP' +devnull = open(os.devnull, 'a') + + +class Context(object): + """ + A context object contains data on how to run a process. + + The attributes are: + action -- a string with a command or a list of arguments for + the Popen call. + app -- the name of the app function. ("vim" for app_vim.) + app is used to get an action if the user didn't specify one. + 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 + file -- an arbitrary file from that list (or None) + fm -- the filemanager instance + wait -- boolean, wait for the end or execute programs in parallel? + popen_kws -- keyword arguments which are directly passed to Popen + """ + + def __init__(self, **keywords): + self.__dict__ = keywords + + @property + def filepaths(self): + try: + return [f.path for f in self.files] + except: + return [] + + def __iter__(self): + """Iterate over file paths""" + for item in self.filepaths: + yield item + + def squash_flags(self): + """Remove duplicates and lowercase counterparts of uppercase flags""" + for flag in self.flags: + if ord(flag) <= 90: + bad = flag + flag.lower() + self.flags = ''.join(c for c in self.flags if c not in bad) + + +class Runner(object): + def __init__(self, ui=None, logfunc=None, apps=None): + self.ui = ui + self.logfunc = logfunc + self.apps = apps + + def _log(self, text): + try: + self.logfunc(text) + except TypeError: + pass + return False + + def _activate_ui(self, boolean): + if self.ui is not None: + if boolean: + try: self.ui.initialize() + except: self._log("Failed to initialize UI") + else: + try: self.ui.suspend() + except: self._log("Failed to suspend UI") + + def __call__(self, action=None, try_app_first=False, + app='default', files=None, mode=0, + flags='', wait=True, **popen_kws): + """ + Run the application in the way specified by the options. + + Returns False if nothing can be done, None if there was an error, + otherwise the process object returned by Popen(). + + This function tries to find an action if none is defined. + """ + + # Find an action if none was supplied by + # creating a Context object and passing it to + # an Application object. + + context = Context(app=app, files=files, mode=mode, + 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!") + + # Preconditions + + context.squash_flags() + popen_kws = context.popen_kws # shortcut + + toggle_ui = True + pipe_output = False + + popen_kws['args'] = action + if 'shell' not in popen_kws: + popen_kws['shell'] = isinstance(action, str) + if 'stdout' not in popen_kws: + popen_kws['stdout'] = sys.stdout + if 'stderr' not in popen_kws: + popen_kws['stderr'] = sys.stderr + + # Evaluate the flags to determine keywords + # for Popen() and other variables + + if 'p' in context.flags: + popen_kws['stdout'] = PIPE + popen_kws['stderr'] = PIPE + toggle_ui = False + pipe_output = True + context.wait = False + if 's' in context.flags or 'd' in context.flags: + for key in ('stdout', 'stderr', 'stdin'): + popen_kws[key] = devnull + if 'd' in context.flags: + toggle_ui = False + context.wait = False + + # Finally, run it + + if toggle_ui: + self._activate_ui(False) + try: + process = None + try: + process = Popen(**popen_kws) + except: + self._log("Failed to run: " + str(action)) + else: + if context.wait: + waitpid_no_intr(process.pid) + finally: + if toggle_ui: + self._activate_ui(True) + if pipe_output and process: + return self(action='less', app='pager', try_app_first=True, + stdin=process.stdout) + return process diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index a19df7a9..37099d04 100644 --- a/ranger/defaults/apps.py +++ b/ranger/defaults/apps.py @@ -78,7 +78,7 @@ class CustomApplications(Applications): return self.either(c, 'mplayer', 'totem') if f.image: - return self.app_feh(c) + return self.either(c, 'feh', 'mirage') if f.document: return self.app_editor(c) @@ -130,7 +130,6 @@ class CustomApplications(Applications): @depends_on('mirage') def app_mirage(self, c): c.flags += 'd' - return tup('mirage', *c) @depends_on('feh') diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 139cda88..8711f737 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -21,26 +21,45 @@ intact and the type of the value stays the same. from ranger.api.options import * -one_kb = 1024 - -colorscheme = colorschemes.default +# Which colorscheme to use? There are these by default: +# colorschemes.texas +# colorschemes.jungle +# colorschemes.default +# colorschemes.snow +# Texas uses 88 colors. If they are not supported, it will fall back +# to the default scheme. +colorscheme = colorschemes.texas max_history_size = 20 -max_filesize_for_preview = 300 * one_kb scroll_offset = 2 -preview_files = True -flushinput = True -sort = 'basename' -reverse = False -directories_first = True +# Flush the input after each key hit? (Noticable when ranger lags) +flushinput = True -show_hidden = False +# Preview files on the rightmost column? +# And collapse the last column if there is nothing to preview? +preview_files = True +max_filesize_for_preview = 300 * 1024 # 300kb collapse_preview = True + +# Save bookmarks (used with mX and `X) instantly? +# this helps to synchronize bookmarks between multiple ranger +# instances but leads to slight performance loss. +# When false, bookmarks are saved when ranger is exited. autosave_bookmarks = True + +# Specify a title for the window? Some terminals don't support this: update_title = False +# Makes sense for screen readers: show_cursor = False +# One of: size, basename, mtime, type +sort = 'basename' +reverse = False +directories_first = True + +# Which files are hidden if show_hidden is False? hidden_filter = regexp( - r'lost\+found|^\.|~$|\.(:?pyc|pyo|bak|swp)$') + r'lost\+found|^\.|~$|\.(:?pyc|pyo|bak|swp)$') +show_hidden = False diff --git a/ranger/ext/openstruct.py b/ranger/ext/openstruct.py index 11363127..a94c3031 100644 --- a/ranger/ext/openstruct.py +++ b/ranger/ext/openstruct.py @@ -13,8 +13,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# prepend __ to arguments because one might use "args" +# or "keywords" as a keyword argument. + class OpenStruct(dict): """The fusion of dict and struct""" - def __init__(self, *args, **keywords): - dict.__init__(self, *args, **keywords) + def __init__(self, *__args, **__keywords): + dict.__init__(self, *__args, **__keywords) self.__dict__ = self diff --git a/ranger/fm.py b/ranger/fm.py deleted file mode 100644 index a10e9af6..00000000 --- a/ranger/fm.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# 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 . - -from time import time -from collections import deque - -from ranger.actions import Actions -from ranger.container import Bookmarks -from ranger.runner import Runner -from ranger import relpath_conf -from ranger.ext.get_executables import get_executables -from ranger import __version__ -from ranger.fsobject import Loader - -CTRL_C = 3 -TICKS_BEFORE_COLLECTING_GARBAGE = 100 - -class FM(Actions): - input_blocked = False - input_blocked_until = 0 - stderr_to_out = False - def __init__(self, ui=None, bookmarks=None, tags=None): - """Initialize FM.""" - Actions.__init__(self) - self.ui = ui - self.log = deque(maxlen=20) - self.bookmarks = bookmarks - self.tags = tags - self.loader = Loader() - self._executables = None - self.apps = self.settings.apps.CustomApplications() - - def mylogfunc(text): - self.notify(text, bad=True) - self.run = Runner(ui=self.ui, apps=self.apps, - logfunc=mylogfunc) - - from ranger.shared import FileManagerAware - FileManagerAware.fm = self - - @property - def executables(self): - if self._executables is None: - self._executables = sorted(get_executables()) - return self._executables - - def initialize(self): - """If ui/bookmarks are None, they will be initialized here.""" - from ranger.fsobject.directory import Directory - - if self.bookmarks is None: - self.bookmarks = Bookmarks( - bookmarkfile=relpath_conf('bookmarks'), - bookmarktype=Directory, - autosave=self.settings.autosave_bookmarks) - self.bookmarks.load() - - else: - self.bookmarks = bookmarks - - from ranger.container.tags import Tags - if self.tags is None: - self.tags = Tags(relpath_conf('tagged')) - - if self.ui is None: - from ranger.gui.defaultui import DefaultUI - self.ui = DefaultUI() - self.ui.initialize() - - def block_input(self, sec=0): - self.input_blocked = sec != 0 - self.input_blocked_until = time() + sec - - def loop(self): - """ - The main loop consists of: - 1. reloading bookmarks if outdated - 2. letting the loader work - 3. drawing and finalizing ui - 4. reading and handling user input - 5. after X loops: collecting unused directory objects - """ - - self.env.enter_dir(self.env.path) - - gc_tick = 0 - - try: - while True: - self.bookmarks.update_if_outdated() - self.loader.work() - if hasattr(self.ui, 'throbber'): - if self.loader.has_work(): - self.ui.throbber(self.loader.status) - else: - self.ui.throbber(remove=True) - - self.ui.redraw() - - self.ui.set_load_mode(self.loader.has_work()) - - key = self.ui.get_next_key() - - if key > 0: - if self.input_blocked and \ - time() > self.input_blocked_until: - self.input_blocked = False - if not self.input_blocked: - self.ui.handle_key(key) - - gc_tick += 1 - if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE: - gc_tick = 0 - self.env.garbage_collect() - - finally: - self.bookmarks.remember(self.env.pwd) - self.bookmarks.save() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 2550062f..b98005b5 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -207,7 +207,7 @@ class BrowserColumn(Pager, Widget): this_color = base_color + list(drawed.mimetype_tuple) text = drawed.basename - tagged = drawed.realpath in self.fm.tags + tagged = self.fm.tags and drawed.realpath in self.fm.tags if i == selected_i: this_color.append('selected') diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 87e5a7b5..a331c66d 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -580,7 +580,7 @@ class QuickOpenConsole(ConsoleWithTab): return self.fm.apps.has(arg) def _is_flags(self, arg): - from ranger.runner import ALLOWED_FLAGS + from ranger.core.runner import ALLOWED_FLAGS return all(x in ALLOWED_FLAGS for x in arg) def _is_mode(self, arg): diff --git a/ranger/help/starting.py b/ranger/help/starting.py index 29921ffc..f5517c78 100644 --- a/ranger/help/starting.py +++ b/ranger/help/starting.py @@ -61,7 +61,7 @@ Note: The "open with" console is named QuickOpenConsole in the source code. ============================================================================== 2.3. Programs -Programs have to be defined in ranger/defaults/apps.py. Each function +Programs have to be defined in ranger/apps.py. Each function in the class CustomApplications which starts with "app_" can be used as a program in the "open with" prompt. @@ -83,7 +83,7 @@ start a file in mode 0. "4l" will start the file in mode 4 etc. You can specify a mode in the "open with" console by simply adding the number. Eg: "open with: mplayer 1" or "open with: 1" -For a list of all programs and modes, see ranger/defaults/apps.py +For a list of all programs and modes, see ranger/apps.py ============================================================================== diff --git a/ranger/runner.py b/ranger/runner.py deleted file mode 100644 index 26424881..00000000 --- a/ranger/runner.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (C) 2009, 2010 Roman Zimbelmann -# -# 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 . - -""" -This module is an abstract layer over subprocess.Popen - -It gives you highlevel control about how processes are run. - -Example: -run = Runner(logfunc=print) -run('sleep 2', wait=True) # waits until the process exists -run(['ls', '--help'], flags='p') # pipes output to pager -run() # prints an error message - -List of allowed flags: -s: silent mode. output will be discarded. -d: detach the process. -p: redirect output to the pager -(An uppercase key ensures that a certain flag will not be used.) -""" - -import os -import sys -from subprocess import Popen, PIPE -from ranger.ext.waitpid_no_intr import waitpid_no_intr - - -ALLOWED_FLAGS = 'sdpSDP' -devnull = open(os.devnull, 'a') - - -class Context(object): - """ - A context object contains data on how to run a process. - - The attributes are: - action -- a string with a command or a list of arguments for - the Popen call. - app -- the name of the app function. ("vim" for app_vim.) - app is used to get an action if the user didn't specify one. - 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 - file -- an arbitrary file from that list (or None) - fm -- the filemanager instance - wait -- boolean, wait for the end or execute programs in parallel? - popen_kws -- keyword arguments which are directly passed to Popen - """ - - def __init__(self, **keywords): - self.__dict__ = keywords - - @property - def filepaths(self): - try: - return [f.path for f in self.files] - except: - return [] - - def __iter__(self): - """Iterate over file paths""" - for item in self.filepaths: - yield item - - def squash_flags(self): - """Remove duplicates and lowercase counterparts of uppercase flags""" - for flag in self.flags: - if ord(flag) <= 90: - bad = flag + flag.lower() - self.flags = ''.join(c for c in self.flags if c not in bad) - - -class Runner(object): - def __init__(self, ui=None, logfunc=None, apps=None): - self.ui = ui - self.logfunc = logfunc - self.apps = apps - - def _log(self, text): - try: - self.logfunc(text) - except TypeError: - pass - return False - - def _activate_ui(self, boolean): - if self.ui is not None: - if boolean: - try: self.ui.initialize() - except: self._log("Failed to initialize UI") - else: - try: self.ui.suspend() - except: self._log("Failed to suspend UI") - - def __call__(self, action=None, try_app_first=False, - app='default', files=None, mode=0, - flags='', wait=True, **popen_kws): - """ - Run the application in the way specified by the options. - - Returns False if nothing can be done, None if there was an error, - otherwise the process object returned by Popen(). - - This function tries to find an action if none is defined. - """ - - # Find an action if none was supplied by - # creating a Context object and passing it to - # an Application object. - - context = Context(app=app, files=files, mode=mode, - 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!") - - # Preconditions - - context.squash_flags() - popen_kws = context.popen_kws # shortcut - - toggle_ui = True - pipe_output = False - - popen_kws['args'] = action - if 'shell' not in popen_kws: - popen_kws['shell'] = isinstance(action, str) - if 'stdout' not in popen_kws: - popen_kws['stdout'] = sys.stdout - if 'stderr' not in popen_kws: - popen_kws['stderr'] = sys.stderr - - # Evaluate the flags to determine keywords - # for Popen() and other variables - - if 'p' in context.flags: - popen_kws['stdout'] = PIPE - popen_kws['stderr'] = PIPE - toggle_ui = False - pipe_output = True - context.wait = False - if 's' in context.flags or 'd' in context.flags: - for key in ('stdout', 'stderr', 'stdin'): - popen_kws[key] = devnull - if 'd' in context.flags: - toggle_ui = False - context.wait = False - - # Finally, run it - - if toggle_ui: - self._activate_ui(False) - try: - process = None - try: - process = Popen(**popen_kws) - except: - self._log("Failed to run: " + str(action)) - else: - if context.wait: - waitpid_no_intr(process.pid) - finally: - if toggle_ui: - self._activate_ui(True) - if pipe_output and process: - return self(action='less', app='pager', try_app_first=True, - stdin=process.stdout) - return process diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py index b549bd20..6df5241f 100644 --- a/ranger/shared/settings.py +++ b/ranger/shared/settings.py @@ -14,6 +14,8 @@ # along with this program. If not, see . import types +from inspect import isclass, ismodule +import ranger from ranger.ext.openstruct import OpenStruct from ranger.gui.colorscheme import ColorScheme @@ -41,54 +43,52 @@ class SettingsAware(object): @staticmethod def _setup(): - from inspect import isclass, ismodule - from ranger.gui.colorscheme import ColorScheme + settings = OpenStruct() - # overwrite single default options with custom options from ranger.defaults import options - try: - import options as custom_options - for setting in ALLOWED_SETTINGS: - if hasattr(custom_options, setting): - setattr(options, setting, getattr(custom_options, setting)) - elif not hasattr(options, setting): - raise Exception("This option was not defined: " + setting) - except ImportError: - pass - - assert check_option_types(options) - - try: - import apps - except ImportError: - from ranger.defaults import apps - - try: - import keys - except ImportError: - from ranger.defaults import keys + for setting in ALLOWED_SETTINGS: + try: + settings[setting] = getattr(options, setting) + except AttributeError: + raise Exception("The option `{0}' was not defined" \ + " in the defaults!".format(setting)) + + import sys + if not ranger.arg.clean: + # overwrite single default options with custom options + try: + import rangerrc + except ImportError: + pass + else: + for setting in ALLOWED_SETTINGS: + try: + settings[setting] = getattr(rangerrc, setting) + except AttributeError: + pass + assert check_option_types(settings) # If a module is specified as the colorscheme, replace it with one # valid colorscheme inside that module. - all_content = options.colorscheme.__dict__.items() + all_content = settings.colorscheme.__dict__.items() - if isclass(options.colorscheme) and \ - issubclass(options.colorscheme, ColorScheme): - options.colorscheme = options.colorscheme() + if isclass(settings.colorscheme) and \ + issubclass(settings.colorscheme, ColorScheme): + settings.colorscheme = settings.colorscheme() - elif ismodule(options.colorscheme): + elif ismodule(settings.colorscheme): def is_scheme(x): return isclass(x) and issubclass(x, ColorScheme) - if hasattr(options.colorscheme, 'Scheme') \ - and is_scheme(options.colorscheme.Scheme): - options.colorscheme = options.colorscheme.Scheme() + if hasattr(settings.colorscheme, 'Scheme') \ + and is_scheme(settings.colorscheme.Scheme): + settings.colorscheme = settings.colorscheme.Scheme() else: - for name, var in options.colorscheme.__dict__.items(): + for name, var in settings.colorscheme.__dict__.items(): if var != ColorScheme and is_scheme(var): - options.colorscheme = var() + settings.colorscheme = var() break else: raise Exception("The module contains no " \ @@ -96,12 +96,18 @@ class SettingsAware(object): else: raise Exception("Cannot locate colorscheme!") - for setting in ALLOWED_SETTINGS: - SettingsAware.settings[setting] = getattr(options, setting) - - SettingsAware.settings.keys = keys - SettingsAware.settings.apps = apps + try: + import apps + except ImportError: + from ranger.defaults import apps + settings.apps = apps + try: + import keys + except ImportError: + from ranger.defaults import keys + settings.keys = keys + SettingsAware.settings = settings def check_option_types(opt): import inspect -- cgit 1.4.1-2-gfad0