diff options
author | hut <hut@lavabit.com> | 2010-04-07 15:50:08 +0200 |
---|---|---|
committer | hut <hut@lavabit.com> | 2010-04-07 15:50:08 +0200 |
commit | 8d89c6f3a7cf6b0c8abc2ff68ecc7201ac08e872 (patch) | |
tree | 610dd3acfdac10ebe1d766a779897207ea6dfb33 /ranger | |
parent | 55435343b142c424619e3072475ca8b3366d109c (diff) | |
parent | f45f9734d2ff9fd6b68ff6c879d5b07b0e5c7d02 (diff) | |
download | ranger-8d89c6f3a7cf6b0c8abc2ff68ecc7201ac08e872.tar.gz |
Merge branch 'devel' into newkey
Conflicts: ranger/core/actions.py ranger/defaults/keys.py ranger/ext/direction.py ranger/gui/ui.py ranger/gui/widgets/browserview.py ranger/gui/widgets/pager.py
Diffstat (limited to 'ranger')
32 files changed, 1341 insertions, 809 deletions
diff --git a/ranger/__init__.py b/ranger/__init__.py index e2a4983d..f46a1e76 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -34,7 +34,7 @@ USAGE = '%prog [options] [path/filename]' DEFAULT_CONFDIR = '~/.ranger' RANGERDIR = os.path.dirname(__file__) LOGFILE = '/tmp/errorlog' -arg = OpenStruct(cd_after_exit=False, +arg = OpenStruct( debug=False, clean=False, confdir=DEFAULT_CONFDIR, mode=0, flags='', targets=[]) @@ -64,6 +64,3 @@ def relpath_conf(*paths): def relpath(*paths): """returns the path relative to rangers library directory""" return os.path.join(RANGERDIR, *paths) - - -from ranger.__main__ import main diff --git a/ranger/__main__.py b/ranger/__main__.py index bd01bf4a..674ad8f6 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -29,38 +29,23 @@ def parse_arguments(): 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. ") - - parser.add_option('-r', '--confdir', dest='confdir', type='string', - default=DEFAULT_CONFDIR, + parser.add_option('-r', '--confdir', type='string', + metavar='dir', default=DEFAULT_CONFDIR, help="the configuration directory. (%default)") - - parser.add_option('-m', '--mode', type='int', dest='mode', default=0, + 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', dest='flags', default='', + parser.add_option('-f', '--flags', type='string', default='', + metavar='string', 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__ - if not arg.clean: try: os.makedirs(arg.confdir) @@ -71,9 +56,7 @@ def parse_arguments(): print("To run ranger without the need for configuration files") print("use the --clean option.") raise SystemExit() - sys.path.append(arg.confdir) - return arg def main(): @@ -100,9 +83,10 @@ def main(): if getdefaultlocale()[1] not in ('utf8', 'UTF-8'): for locale in ('en_US.utf8', 'en_US.UTF-8'): try: setlocale(LC_ALL, locale) - except: pass #sometimes there is none available though... - else: - setlocale(LC_ALL, '') + except: pass + else: break + else: setlocale(LC_ALL, '') + else: setlocale(LC_ALL, '') arg = parse_arguments() ranger.arg = arg @@ -132,7 +116,6 @@ def main(): try: my_ui = UI() my_fm = FM(ui=my_ui) - my_fm.stderr_to_out = arg.cd_after_exit # Run the file manager my_fm.initialize() @@ -142,9 +125,6 @@ def main(): # Finish, clean up if 'my_ui' in vars(): my_ui.destroy() - if arg.cd_after_exit: - try: sys.__stderr__.write(my_fm.env.cwd.path) - except: pass if __name__ == '__main__': top_dir = os.path.dirname(sys.path[0]) diff --git a/ranger/api/options.py b/ranger/api/options.py index 7ead8c90..4748823d 100644 --- a/ranger/api/options.py +++ b/ranger/api/options.py @@ -16,6 +16,7 @@ import re from re import compile as regexp from ranger import colorschemes as allschemes +from ranger.gui import color class AttrToString(object): """ diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py index d1a7e820..24f8ab91 100644 --- a/ranger/colorschemes/default.py +++ b/ranger/colorschemes/default.py @@ -21,7 +21,7 @@ class Default(ColorScheme): fg, bg, attr = default_colors if context.reset: - pass + return default_colors elif context.in_browser: if context.selected: @@ -40,11 +40,17 @@ class Default(ColorScheme): if context.container: fg = red if context.directory: + attr |= bold fg = blue elif context.executable and not \ - any((context.media, context.container)): + any((context.media, context.container, + context.fifo, context.socket)): attr |= bold fg = green + if context.socket: + fg = magenta + if context.fifo: + fg = yellow if context.link: fg = context.good and cyan or magenta if context.tag_marker and not context.selected: @@ -66,6 +72,9 @@ class Default(ColorScheme): fg = context.bad and red or green elif context.directory: fg = blue + elif context.tab: + if context.good: + bg = green elif context.link: fg = cyan diff --git a/ranger/core/actions.py b/ranger/core/actions.py index e55d65b1..6910871b 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -18,15 +18,288 @@ import shutil from inspect import cleandoc import ranger -from ranger.shared import EnvironmentAware, SettingsAware +from ranger.ext.direction import Direction +from ranger.shared import FileManagerAware, EnvironmentAware, SettingsAware from ranger import fsobject from ranger.gui.widgets import console_mode as cmode from ranger.fsobject import File -class Actions(EnvironmentAware, SettingsAware): +class Actions(FileManagerAware, EnvironmentAware, SettingsAware): search_method = 'ctime' search_forward = False + # -------------------------- + # -- Backwards Compatibility + # -------------------------- + # All methods defined here are just for backwards compatibility, + # allowing old configuration files to work with newer versions. + # You can delete them and they should change nothing if you use + # an up-to-date configuration file. + + def dummy(self, *args, **keywords): + """For backwards compatibility only.""" + + handle_mouse = resize = dummy + + def move_left(self, narg=1): + """Enter the parent directory""" + self.move(left=1, narg=narg) + + def move_right(self, narg=None): + """Enter the current directory or execute the current file""" + self.move(right=1, narg=narg) + + def move_pointer(self, relative=0, absolute=None, narg=None): + """Move the pointer down by <relative> or to <absolute>""" + dct = dict(down=relative, narg=narg) + if absolute is not None: + dct['to'] = absolute + self.move(**dct) + + def move_pointer_by_pages(self, relative): + """Move the pointer down by <relative> pages""" + self.move(down=relative, pages=True) + + def move_pointer_by_percentage(self, relative=0, absolute=None, narg=None): + """Move the pointer down to <absolute>%""" + self.move(to=absolute, percentage=True, narg=narg) + + # -------------------------- + # -- Basic Commands + # -------------------------- + + def exit(self): + """Exit the program""" + raise SystemExit() + + def reset(self): + """Reset the filemanager, clearing the directory buffer""" + old_path = self.env.cwd.path + self.env.garbage_collect(-1) + self.enter_dir(old_path) + + def reload_cwd(self): + try: + cwd = self.env.cwd + except: + pass + cwd.unload() + cwd.load_content() + + def notify(self, text, duration=4, bad=False): + if isinstance(text, Exception): + if ranger.arg.debug: + raise + bad = True + text = str(text) + self.log.appendleft(text) + if hasattr(self.ui, 'notify'): + self.ui.notify(text, duration=duration, bad=bad) + + def redraw_window(self): + """Redraw the window""" + self.ui.redraw_window() + + def open_console(self, mode=':', string=''): + """Open the console if the current UI supports that""" + if hasattr(self.ui, 'open_console'): + self.ui.open_console(mode, string) + + def execute_file(self, files, **kw): + """Execute a file. + app is the name of a method in Applications, without the "app_" + flags is a string consisting of runner.ALLOWED_FLAGS + mode is a positive integer. + Both flags and mode specify how the program is run.""" + + if isinstance(files, set): + files = list(files) + elif type(files) not in (list, tuple): + files = [files] + + return self.run(files=list(files), **kw) + + # -------------------------- + # -- Moving Around + # -------------------------- + + def move(self, narg=None, **kw): + """ + A universal movement method. + + Accepts these parameters: + (int) down, (int) up, (int) left, (int) right, (int) to, + (bool) absolute, (bool) relative, (bool) pages, + (bool) percentage + + to=X is translated to down=X, absolute=True + + Example: + self.move(down=4, pages=True) # moves down by 4 pages. + self.move(to=2, pages=True) # moves to page 2. + self.move(to=1, percentage=True) # moves to 80% + """ + direction = Direction(kw) + if 'left' in direction: + steps = direction.left() + if narg is not None: + steps *= narg + try: + directory = os.path.join(*(['..'] * steps)) + except: + return + self.env.enter_dir(directory) + + elif '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: + self.open_console(cmode.OPEN_QUICK) + + elif direction.vertical(): + newpos = direction.move( + direction=direction.down(), + override=narg, + maximum=len(self.env.cwd), + current=self.env.cwd.pointer, + pagesize=self.ui.browser.hei) + self.env.cwd.move(absolute=newpos) + + def history_go(self, relative): + """Move back and forth in the history""" + self.env.history_go(relative) + + def scroll(self, relative): + """Scroll down by <relative> lines""" + if hasattr(self.ui, 'scroll'): + self.ui.scroll(relative) + self.env.cf = self.env.cwd.pointed_obj + + def enter_dir(self, path, remember=False, history=True): + """Enter the directory at the given path""" + if remember: + cwd = self.env.cwd + result = self.env.enter_dir(path, history=history) + self.bookmarks.remember(cwd) + return result + return self.env.enter_dir(path, history=history) + + def cd(self, path, remember=True): + """enter the directory at the given path, remember=True""" + self.enter_dir(path, remember=remember) + + def traverse(self): + cf = self.env.cf + cwd = self.env.cwd + 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 + if cwd.pointer < len(cwd) - 1: + break + if cwd.path == '/': + break + self.move(down=1) + self.traverse() + else: + self.move(down=1) + self.traverse() + + # -------------------------- + # -- Shortcuts / Wrappers + # -------------------------- + + def execute_command(self, cmd, **kw): + return self.run(cmd, **kw) + + def edit_file(self, file=None): + """Calls execute_file with the current file and app='editor'""" + if file is None: + file = self.env.cf + elif isinstance(file, str): + file = File(os.path.expanduser(file)) + if file is None: + return + self.execute_file(file, app = 'editor') + + def toggle_boolean_option(self, string): + """Toggle a boolean option named <string>""" + if isinstance(self.env.settings[string], bool): + self.env.settings[string] ^= True + + def set_option(self, optname, value): + """Set the value of an option named <optname>""" + self.env.settings[optname] = value + + def sort(self, func=None, reverse=None): + if reverse is not None: + self.env.settings['sort_reverse'] = bool(reverse) + + if func is not None: + self.env.settings['sort'] = str(func) + + def set_filter(self, fltr): + try: + self.env.cwd.filter = fltr + except: + pass + + def mark(self, all=False, toggle=False, val=None, movedown=None, narg=1): + """ + A wrapper for the directory.mark_xyz functions. + + Arguments: + all - change all files of the current directory at once? + toggle - toggle the marked-status? + val - mark or unmark? + """ + + if self.env.cwd is None: + return + + cwd = self.env.cwd + + if not cwd.accessible: + return + + if movedown is None: + movedown = not all + + if val is None and toggle is False: + return + + if all: + if toggle: + cwd.toggle_all_marks() + else: + cwd.mark_all(val) + else: + for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))): + item = cwd.files[i] + if item is not None: + if toggle: + cwd.toggle_mark(item) + else: + cwd.mark_item(item, val) + + if movedown: + self.move(down=narg) + + if hasattr(self.ui, 'redraw_main_column'): + self.ui.redraw_main_column() + if hasattr(self.ui, 'status'): + self.ui.status.need_redraw = True + + # -------------------------- + # -- Searching + # -------------------------- + def search(self, order=None, forward=True): original_order = order if self.search_forward: @@ -74,26 +347,11 @@ class Actions(EnvironmentAware, SettingsAware): self.search_method = order self.search_forward = forward - def resize(self): - """Update the size of the UI""" - self.ui.update_size() - - def exit(self): - """Exit the program""" - raise SystemExit() - - def enter_dir(self, path, remember=False): - """Enter the directory at the given path""" - if remember: - cwd = self.env.cwd - result = self.env.enter_dir(path) - self.bookmarks.remember(cwd) - return result - return self.env.enter_dir(path) - - def cd(self, path, remember=True): - """enter the directory at the given path, remember=True""" - self.enter_dir(path, remember) + # -------------------------- + # -- Tags + # -------------------------- + # Tags are saved in ~/.ranger/tagged and simply mark if a + # file is important to you in any context. def tag_toggle(self, movedown=None): try: @@ -107,7 +365,7 @@ class Actions(EnvironmentAware, SettingsAware): if movedown is None: movedown = len(sel) == 1 if movedown: - self.move_pointer(relative=1) + self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() @@ -124,11 +382,16 @@ class Actions(EnvironmentAware, SettingsAware): if movedown is None: movedown = len(sel) == 1 if movedown: - self.move_pointer(relative=1) + self.move(down=1) if hasattr(self.ui, 'redraw_main_column'): self.ui.redraw_main_column() + # -------------------------- + # -- Bookmarks + # -------------------------- + # Using ranger.container.bookmarks. + def enter_bookmark(self, key): """Enter the bookmark with the name <key>""" try: @@ -148,35 +411,10 @@ class Actions(EnvironmentAware, SettingsAware): """Delete the bookmark with the name <key>""" self.bookmarks.delete(key) - def move_left(self, narg=None): - """Enter the parent directory""" - if narg is None: - narg = 1 - try: - directory = os.path.join(*(['..'] * narg)) - except: - return - self.env.enter_dir(directory) - - def move_right(self, mode=0, narg=None): - """Enter the current directory or execute the current file""" - cf = self.env.cf - sel = self.env.get_selection() - - if isinstance(narg, int): - mode = narg - if not self.env.enter_dir(cf): - if sel: - if self.execute_file(sel, mode=mode) is False: - self.open_console(cmode.OPEN_QUICK) - - def history_go(self, relative): - """Move back and forth in the history""" - self.env.history_go(relative) - - def handle_mouse(self): - """Handle mouse-buttons if one was pressed""" - self.ui.handle_mouse() + # -------------------------- + # -- Pager + # -------------------------- + # These commands open the built-in pager and set specific sources. def display_command_help(self, console_widget): if not hasattr(self.ui, 'open_pager'): @@ -239,229 +477,59 @@ class Actions(EnvironmentAware, SettingsAware): pager = self.ui.open_embedded_pager() pager.set_source(f) - def execute_file(self, files, **kw): - """Execute a file. - app is the name of a method in Applications, without the "app_" - flags is a string consisting of runner.ALLOWED_FLAGS - mode is a positive integer. - Both flags and mode specify how the program is run.""" - - if isinstance(files, set): - files = list(files) - elif type(files) not in (list, tuple): - files = [files] - - return self.run(files=list(files), **kw) - - def execute_command(self, cmd, **kw): - return self.run(cmd, **kw) - - def edit_file(self, file=None): - """Calls execute_file with the current file and app='editor'""" - if file is None: - file = self.env.cf - elif isinstance(file, str): - file = File(os.path.expanduser(file)) - if file is None: - return - self.execute_file(file, app = 'editor') - - def open_console(self, mode=':', string=''): - """Open the console if the current UI supports that""" - if hasattr(self.ui, 'open_console'): - self.ui.open_console(mode, string) - - def move_pointer(self, relative = 0, absolute = None, narg=None): - """Move the pointer down by <relative> or to <absolute>""" - self.env.cwd.move(relative=relative, - absolute=absolute, narg=narg) - - def move(self, dir, narg=None): - if narg is not None: - dir = dir * narg - - self.notify(str(dir)) - - if dir.right is not None: - if dir.right >= 0: - if dir.has_explicit_direction: - self.move_right(narg=dir.right) - else: - self.move_right(narg=dir.original_right - 1) - elif dir.right < 0: - self.move_left(narg=dir.left) + # -------------------------- + # -- Tabs + # -------------------------- + # This implementation of tabs is very simple and keeps track of + # directory paths only. + + def tab_open(self, name, path=None): + do_emit_signal = name != self.current_tab + self.current_tab = name + if path or (name in self.tabs): + self.enter_dir(path or self.tabs[name]) else: - if dir.percent: - if dir.absolute: - self.move_pointer_by_percentage( \ - absolute=dir.down, narg=narg) - else: - self.move_pointer_by_percentage( \ - relative=dir.down, narg=narg) - elif dir.pages: - self.move_pointer_by_pages(dir.down) - elif dir.absolute: - if dir.has_explicit_direction: - self.move_pointer(absolute=dir.down) - else: - self.move_pointer(absolute=dir.original_down) - else: - self.move_pointer(relative=dir.down) - - def draw_bookmarks(self): - self.ui.browser.draw_bookmarks = True - - def hide_bookmarks(self): - self.ui.browser.draw_bookmarks = False - - def move_pointer_by_pages(self, relative): - """Move the pointer down by <relative> pages""" - self.env.cwd.move(relative=int(relative * self.env.termsize[0])) - - def move_pointer_by_percentage(self, relative=0, absolute=None, narg=None): - """Move the pointer down by <relative>% or to <absolute>%""" - try: - factor = len(self.env.cwd) / 100.0 - except: - return - - if narg is not None: - absolute = narg - - if absolute is not None: - absolute = int(absolute * factor) - - self.env.cwd.move( - relative=int(relative * factor), - absolute=absolute) - - def scroll(self, relative): - """Scroll down by <relative> lines""" - if hasattr(self.ui, 'scroll'): - self.ui.scroll(relative) - self.env.cf = self.env.cwd.pointed_obj - - def redraw_window(self): - """Redraw the window""" - self.ui.redraw_window() - - def reset(self): - """Reset the filemanager, clearing the directory buffer""" - old_path = self.env.cwd.path - self.env.directories = {} - self.enter_dir(old_path) - - def toggle_boolean_option(self, string): - """Toggle a boolean option named <string>""" - if isinstance(self.env.settings[string], bool): - self.env.settings[string] ^= True - - def sort(self, func=None, reverse=None): - if reverse is not None: - self.env.settings['reverse'] = bool(reverse) - - if func is not None: - self.env.settings['sort'] = str(func) - - def force_load_preview(self): - cf = self.env.cf - if hasattr(cf, 'unload') and hasattr(cf, 'load_content'): - cf.unload() - cf.load_content() - - def reload_cwd(self): - try: - cwd = self.env.cwd - except: - pass - cwd.unload() - cwd.load_content() - - def traverse(self): - cf = self.env.cf - cwd = self.env.cwd - if cf is not None and cf.is_directory: - self.enter_dir(cf.path) - elif cwd.pointer >= len(cwd) - 1: - while True: - self.enter_dir('..') - cwd = self.env.cwd - if cwd.pointer < len(cwd) - 1: - break - if cwd.path == '/': - break - self.move_pointer(1) - self.traverse() - else: - self.move_pointer(1) - self.traverse() - - def set_filter(self, fltr): - try: - self.env.cwd.filter = fltr - except: - pass - - def notify(self, text, duration=4, bad=False): - if isinstance(text, Exception): - if ranger.arg.debug: - raise - bad = True - text = str(text) - self.log.appendleft(text) - if hasattr(self.ui, 'notify'): - self.ui.notify(text, duration=duration, bad=bad) - - def hint(self, text): - self.notify(text) - - def mark(self, all=False, toggle=False, val=None, movedown=None, narg=1): - """ - A wrapper for the directory.mark_xyz functions. - - Arguments: - all - change all files of the current directory at once? - toggle - toggle the marked-status? - val - mark or unmark? - """ - - if self.env.cwd is None: - return - - cwd = self.env.cwd - - if not cwd.accessible: - return - - if movedown is None: - movedown = not all - - if val is None and toggle is False: - return - - if all: - if toggle: - cwd.toggle_all_marks() - else: - cwd.mark_all(val) - else: - for i in range(cwd.pointer, min(cwd.pointer + narg, len(cwd))): - item = cwd.files[i] - if item is not None: - if toggle: - cwd.toggle_mark(item) - else: - cwd.mark_item(item, val) - - if movedown: - self.move_pointer(relative=narg) - - if hasattr(self.ui, 'redraw_main_column'): - self.ui.redraw_main_column() - if hasattr(self.ui, 'status'): - self.ui.status.need_redraw = True - - # ------------------------------------ filesystem operations + self._update_current_tab() + if do_emit_signal: + self.signal_emit('tab.change') + + def tab_close(self, name=None): + if name is None: + name = self.current_tab + if name == self.current_tab: + direction = -1 if name == self._get_tab_list()[-1] else 1 + previous = self.current_tab + self.tab_move(direction) + if previous == self.current_tab: + return # can't close last tab + if name in self.tabs: + del self.tabs[name] + + def tab_move(self, offset): + assert isinstance(offset, int) + tablist = self._get_tab_list() + current_index = tablist.index(self.current_tab) + newtab = tablist[(current_index + offset) % len(tablist)] + if newtab != self.current_tab: + self.tab_open(newtab) + + def tab_new(self): + for i in range(10): + i = (i + 1) % 10 + if not i in self.tabs: + self.tab_open(i) + break + + def _get_tab_list(self): + 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 + + # -------------------------- + # -- File System Operations + # -------------------------- def copy(self): """Copy the selected items""" @@ -567,7 +635,6 @@ class Actions(EnvironmentAware, SettingsAware): except OSError as err: self.notify(err) - def rename(self, src, dest): if hasattr(src, 'path'): src = src.path diff --git a/ranger/core/environment.py b/ranger/core/environment.py index 4301d237..0b38c475 100644 --- a/ranger/core/environment.py +++ b/ranger/core/environment.py @@ -21,15 +21,15 @@ from os.path import abspath, normpath, join, expanduser, isdir from ranger.fsobject.directory import Directory, NoDirectoryGiven from ranger.container import KeyBuffer, History +from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.shared import SettingsAware -class Environment(SettingsAware): +class Environment(SettingsAware, SignalDispatcher): """A collection of data which is relevant for more than one class. """ cwd = None # current directory - cf = None # current file copy = None cmd = None cut = None @@ -42,7 +42,9 @@ class Environment(SettingsAware): keybuffer = None def __init__(self, path): + SignalDispatcher.__init__(self) self.path = abspath(expanduser(path)) + self._cf = None self.pathway = () self.directories = {} self.keybuffer = KeyBuffer(None, None) @@ -59,6 +61,22 @@ class Environment(SettingsAware): from ranger.shared import EnvironmentAware EnvironmentAware.env = self + 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 _set_cf(self, value): + if value is not self._cf: + previous = self._cf + self.signal_emit('move', previous=previous, new=value) + + def _get_cf(self): + return self._cf + + cf = property(_get_cf, _set_cf) + def key_append(self, key): """Append a key to the keybuffer""" @@ -100,14 +118,14 @@ class Environment(SettingsAware): except KeyError: return directory - def garbage_collect(self): + def garbage_collect(self, age): """Delete unused directory objects""" - from ranger.fsobject.fsobject import FileSystemObject - for key in tuple(self.directories.keys()): + for key in tuple(self.directories): value = self.directories[key] - if isinstance(value, FileSystemObject): - if value.is_older_than(1200): - del self.directories[key] + if value.is_older_than(age): # and not value in self.pathway: + del self.directories[key] + if value.is_directory: + value.files = None def get_selection(self): if self.cwd: @@ -154,6 +172,8 @@ class Environment(SettingsAware): if path is None: return path = str(path) + previous = self.cwd + # get the absolute path path = normpath(join(self.path, expanduser(path))) @@ -165,9 +185,12 @@ class Environment(SettingsAware): except NoDirectoryGiven: return False + try: + os.chdir(path) + except: + return True self.path = path self.cwd = new_cwd - os.chdir(path) self.cwd.load_content_if_outdated() @@ -186,11 +209,14 @@ class Environment(SettingsAware): self.assign_cursor_positions_for_subdirs() # set the current file. - self.cwd.directories_first = self.settings.directories_first + 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 994447b0..25e66407 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -19,6 +19,9 @@ The File Manager, putting the pieces together from time import time from collections import deque +from curses import KEY_MOUSE, KEY_RESIZE +import os +import sys import ranger from ranger.core.actions import Actions @@ -26,23 +29,27 @@ 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.ext.signal_dispatcher import SignalDispatcher from ranger import __version__ from ranger.fsobject import Loader CTRL_C = 3 TICKS_BEFORE_COLLECTING_GARBAGE = 100 +TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200 -class FM(Actions): +class FM(Actions, SignalDispatcher): 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) + SignalDispatcher.__init__(self) self.ui = ui self.log = deque(maxlen=20) self.bookmarks = bookmarks self.tags = tags + self.tabs = {} + self.current_tab = 1 self.loader = Loader() self._executables = None self.apps = self.settings.apps.CustomApplications() @@ -55,6 +62,10 @@ class FM(Actions): from ranger.shared import FileManagerAware FileManagerAware.fm = self + self.log.append('Ranger {0} started! Process ID is {1}.' \ + .format(__version__, os.getpid())) + self.log.append('Running on Python ' + sys.version.replace('\n','')) + @property def executables(self): if self._executables is None: @@ -88,6 +99,8 @@ class FM(Actions): self.ui = DefaultUI() self.ui.initialize() + self.env.signal_bind('cd', self._update_current_tab) + def block_input(self, sec=0): self.input_blocked = sec != 0 self.input_blocked_until = time() + sec @@ -131,16 +144,21 @@ class FM(Actions): key = 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: - ui.handle_key(key) + if key == KEY_MOUSE: + ui.handle_mouse() + elif key == KEY_RESIZE: + ui.update_size() + else: + if self.input_blocked and \ + time() > self.input_blocked_until: + self.input_blocked = False + if not self.input_blocked: + ui.handle_key(key) gc_tick += 1 if gc_tick > TICKS_BEFORE_COLLECTING_GARBAGE: gc_tick = 0 - env.garbage_collect() + env.garbage_collect(TIME_BEFORE_FILE_BECOMES_GARBAGE) except KeyboardInterrupt: # this only happens in --debug mode. By default, interrupts diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index 347b9ce2..9543badb 100644 --- a/ranger/defaults/apps.py +++ b/ranger/defaults/apps.py @@ -59,6 +59,7 @@ class CustomApplications(Applications): if f.extension is not None: if f.extension in ('pdf'): + c.flags += 'd' return self.either(c, 'evince', 'zathura', 'apvlv') if f.extension in ('html', 'htm', 'xhtml', 'swf'): return self.either(c, 'firefox', 'opera', 'elinks') @@ -114,7 +115,7 @@ class CustomApplications(Applications): @depends_on('mplayer') def app_mplayer(self, c): if c.mode is 1: - return tup('mplayer', *c) + return tup('mplayer', '-fs', *c) elif c.mode is 2: args = "mplayer -fs -sid 0 -vfm ffmpeg -lavdopts " \ @@ -126,7 +127,7 @@ class CustomApplications(Applications): return tup('mplayer', '-mixer', 'software', *c) else: - return tup('mplayer', '-fs', *c) + return tup('mplayer', *c) @depends_on("eog") def app_eye_of_gnome(self, c): @@ -151,15 +152,18 @@ class CustomApplications(Applications): if len(c.files) > 1: return tup('feh', *c) - from collections import deque + try: + from collections import deque - directory = self.fm.env.get_directory(c.file.dirname) - images = [f.path for f in directory.files if f.image] - position = images.index(c.file.path) - deq = deque(images) - deq.rotate(-position) + directory = self.fm.env.get_directory(c.file.dirname) + images = [f.path for f in directory.files if f.image] + position = images.index(c.file.path) + deq = deque(images) + deq.rotate(-position) - return tup('feh', *deq) + return tup('feh', *deq) + except: + return tup('feh', *c) @depends_on("gimp") def app_gimp(self, c): @@ -231,6 +235,6 @@ class CustomApplications(Applications): @depends_on('totem') def app_totem(self, c): if c.mode is 0: - return tup("totem", "--fullscreen", *c) - if c.mode is 1: return tup("totem", *c) + if c.mode is 1: + return tup("totem", "--fullscreen", *c) diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index b1518013..f653168f 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -199,9 +199,10 @@ class find(Command): search = parse(self.line).rest(1) search = re.escape(search) self.fm.env.last_search = re.compile(search, re.IGNORECASE) + self.fm.search_method = 'search' if self.count == 1: - self.fm.move_right() + self.fm.move(right=1) self.fm.block_input(0.5) def quick_open(self): @@ -239,11 +240,25 @@ class quit(Command): """ :quit + Closes the current tab. If there is only one tab, quit the program. + """ + + def execute(self): + if len(self.fm.tabs) <= 1: + self.fm.exit() + self.fm.tab_close() + + +class quit_now(Command): + """ + :quit! + Quits the program immediately. """ + name = 'quit!' def execute(self): - raise SystemExit + self.fm.exit() class delete(Command): @@ -277,8 +292,11 @@ class delete(Command): # user did not confirm deletion return - if self.fm.env.cwd.marked_items \ - or (self.fm.env.cf.is_directory and not self.fm.env.cf.empty()): + cwd = self.fm.env.cwd + cf = self.fm.env.cf + + if cwd.marked_items or (cf.is_directory and not cf.islink \ + and len(os.listdir(cf.path)) > 0): # better ask for a confirmation, when attempting to # delete multiple files or a non-empty directory. return self.fm.open_console(self.mode, delete.WARNING) @@ -495,5 +513,7 @@ def get_command(name, abbrev=True): def command_generator(start): return (cmd + ' ' for cmd in by_name if cmd.startswith(start)) -alias(e=edit) # to make :e unambiguous. +alias(e=edit, q=quit) # for unambiguity +alias(**{'q!':quit_now}) +alias(qall=quit_now) diff --git a/ranger/defaults/keys.py b/ranger/defaults/keys.py index f48c7012..a236ad9f 100644 --- a/ranger/defaults/keys.py +++ b/ranger/defaults/keys.py @@ -53,28 +53,32 @@ def _vimlike_aliases(map): alias(KEY_HOME, 'gg') alias(KEY_END, 'G') + +def _emacs_aliases(map): + alias = map.alias + alias(KEY_LEFT, ctrl('b')) + alias(KEY_RIGHT, ctrl('f')) + alias(KEY_HOME, ctrl('a')) + alias(KEY_END, ctrl('e')) + alias(KEY_DC, ctrl('d')) + alias(DEL, ctrl('h')) + + def initialize_commands(map): """Initialize the commands for the main user interface""" # -------------------------------------------------------- movement _vimlike_aliases(map) - map.alias(KEY_LEFT, KEY_BACKSPACE, DEL) - - map(KEY_DOWN, fm.move_pointer(relative=1)) - map(KEY_UP, fm.move_pointer(relative=-1)) - map(KEY_RIGHT, KEY_ENTER, ctrl('j'), fm.move_right()) - map(KEY_LEFT, KEY_BACKSPACE, DEL, fm.move_left(1)) - map(KEY_HOME, fm.move_pointer(absolute=0)) - map(KEY_END, fm.move_pointer(absolute=-1)) + _basic_movement(map) - map(KEY_HOME, fm.move_pointer(absolute=0)) - map(KEY_END, fm.move_pointer(absolute=-1)) + map.alias(KEY_LEFT, KEY_BACKSPACE, DEL) + map.alias(KEY_RIGHT, KEY_ENTER, ctrl('j')) - map('%', fm.move_pointer_by_percentage(absolute=50)) - map(KEY_NPAGE, fm.move_pointer_by_pages(1)) - map(KEY_PPAGE, fm.move_pointer_by_pages(-1)) - map(ctrl('d'), 'J', fm.move_pointer_by_pages(0.5)) - map(ctrl('u'), 'K', fm.move_pointer_by_pages(-0.5)) + map('%', fm.move(to=50, percentage=True)) + map(KEY_NPAGE, ctrl('f'), fm.move(down=1, pages=True)) + map(KEY_PPAGE, ctrl('b'), fm.move(up=1, pages=True)) + map(ctrl('d'), 'J', fm.move(down=0.5, pages=True)) + map(ctrl('u'), 'K', fm.move(up=0.5, pages=True)) map(']', fm.traverse()) map('[', fm.history_go(-1)) @@ -107,14 +111,16 @@ def initialize_commands(map): map('du', fm.execute_command('du --max-depth=1 -h | less')) # -------------------------------------------------- toggle options - map('b', hint="show_//h//idden //p//review_files //d//irectories_first " \ - "//c//ollapse_preview flush//i//nput") - map('bh', fm.toggle_boolean_option('show_hidden')) - map('bp', fm.toggle_boolean_option('preview_files')) - map('bP', fm.toggle_boolean_option('preview_directories')) - map('bi', fm.toggle_boolean_option('flushinput')) - map('bd', fm.toggle_boolean_option('directories_first')) - map('bc', fm.toggle_boolean_option('collapse_preview')) + map('b', fm.notify('Warning: settings are now changed with z!', bad=True)) + map('z', hint="show_//h//idden //p//review_files //d//irectories_first " \ + "//c//ollapse_preview flush//i//nput ca//s//e_insensitive") + map('zh', fm.toggle_boolean_option('show_hidden')) + map('zp', fm.toggle_boolean_option('preview_files')) + map('zP', fm.toggle_boolean_option('preview_directories')) + map('zi', fm.toggle_boolean_option('flushinput')) + map('zd', fm.toggle_boolean_option('sort_directories_first')) + map('zc', fm.toggle_boolean_option('collapse_preview')) + map('zs', fm.toggle_boolean_option('sort_case_insensitive')) # ------------------------------------------------------------ sort map('o', 'O', hint="//s//ize //b//ase//n//ame //m//time //t//ype //r//everse") @@ -133,7 +139,7 @@ def initialize_commands(map): map('O' + key, fm.sort(func=val, reverse=True)) map('or', 'Or', 'oR', 'OR', lambda arg: \ - arg.fm.sort(reverse=not arg.fm.settings.reverse)) + arg.fm.sort(reverse=not arg.fm.settings.sort_reverse)) # ----------------------------------------------- console shortcuts @map("A") @@ -146,6 +152,8 @@ def initialize_commands(map): map('f', fm.open_console(cmode.COMMAND_QUICK, 'find ')) map('tf', fm.open_console(cmode.COMMAND, 'filter ')) map('d', hint='d//u// (disk usage) d//d// (cut)') + map('@', fm.open_console(cmode.OPEN, '@')) + map('#', fm.open_console(cmode.OPEN, 'p!')) # --------------------------------------------- jump to directories map('gh', fm.cd('~')) @@ -162,24 +170,32 @@ def initialize_commands(map): map('gs', fm.cd('/srv')) map('gR', fm.cd(RANGERDIR)) + # ------------------------------------------------------------ tabs + map('gc', ctrl('W'), fm.tab_close()) + map('gt', TAB, fm.tab_move(1)) + map('gT', KEY_BTAB, fm.tab_move(-1)) + map('gn', ctrl('N'), fm.tab_new()) + for n in range(10): + map('g' + str(n), fm.tab_open(n)) + # ------------------------------------------------------- searching map('/', fm.open_console(cmode.SEARCH)) map('n', fm.search()) map('N', fm.search(forward=False)) - map(TAB, fm.search(order='tag')) + map('ct', fm.search(order='tag')) map('cc', fm.search(order='ctime')) map('cm', fm.search(order='mimetype')) map('cs', fm.search(order='size')) - map('c', hint='//c//time //m//imetype //s//ize') + map('c', hint='//c//time //m//imetype //s//ize //t//agged') # ------------------------------------------------------- bookmarks for key in ALLOWED_BOOKMARK_KEYS: map("`" + key, "'" + key, fm.enter_bookmark(key)) map("m" + key, fm.set_bookmark(key)) map("um" + key, fm.unset_bookmark(key)) - map("`", "'", "m", draw_bookmarks=True) + map("`", "'", "m", "um", draw_bookmarks=True) # ---------------------------------------------------- change views map('i', fm.display_file()) @@ -204,10 +220,18 @@ def initialize_commands(map): # ------------------------------------------------ system functions _system_functions(map) - map('ZZ', fm.exit()) + map('ZZ', 'ZQ', fm.exit()) map(ctrl('R'), fm.reset()) map('R', fm.reload_cwd()) - map(ctrl('C'), fm.exit()) + @map(ctrl('C')) + def ctrl_c(arg): + try: + item = arg.fm.loader.queue[0] + except: + arg.fm.notify("Type Q or :quit<Enter> to exit Ranger") + else: + arg.fm.notify("Aborting: " + item.get_description()) + arg.fm.loader.remove(index=0) map(':', ';', fm.open_console(cmode.COMMAND)) map('>', fm.open_console(cmode.COMMAND_QUICK)) @@ -220,33 +244,24 @@ def initialize_commands(map): def initialize_console_commands(map): """Initialize the commands for the console widget only""" + _basic_movement(map) + _emacs_aliases(map) + # -------------------------------------------------------- movement map(KEY_UP, wdg.history_move(-1)) map(KEY_DOWN, wdg.history_move(1)) - - map(ctrl('b'), KEY_LEFT, wdg.move(relative = -1)) - map(ctrl('f'), KEY_RIGHT, wdg.move(relative = 1)) - map(ctrl('a'), KEY_HOME, wdg.move(absolute = 0)) - map(ctrl('e'), KEY_END, wdg.move(absolute = -1)) + map(KEY_HOME, wdg.move(right=0, absolute=True)) + map(KEY_END, wdg.move(right=-1, absolute=True)) # ----------------------------------------- deleting / pasting text - map(ctrl('d'), KEY_DC, wdg.delete(0)) - map(ctrl('h'), KEY_BACKSPACE, DEL, wdg.delete(-1)) + map(KEY_DC, wdg.delete(0)) + map(KEY_BACKSPACE, DEL, wdg.delete(-1)) map(ctrl('w'), wdg.delete_word()) map(ctrl('k'), wdg.delete_rest(1)) map(ctrl('u'), wdg.delete_rest(-1)) map(ctrl('y'), wdg.paste()) - # ----------------------------------------------------- typing keys - def type_key(arg): - arg.wdg.type_key(arg.keys) - - for i in range(ord(' '), ord('~')+1): - map(i, type_key) - # ------------------------------------------------ system functions - _system_functions(map) - map(KEY_F1, lambda arg: arg.fm.display_command_help(arg.wdg)) map(ctrl('c'), ESC, wdg.close()) map(ctrl('j'), KEY_ENTER, wdg.execute()) @@ -286,42 +301,44 @@ def initialize_embedded_pager_commands(map): map('q', 'i', ESC, lambda arg: arg.fm.ui.close_embedded_pager()) map.rebuild_paths() + def _base_pager_commands(map): _basic_movement(map) _vimlike_aliases(map) _system_functions(map) # -------------------------------------------------------- movement - map(KEY_LEFT, wdg.move_horizontal(relative=-4)) - map(KEY_RIGHT, wdg.move_horizontal(relative=4)) - map(KEY_NPAGE, wdg.move(relative=1, pages=True)) - map(KEY_PPAGE, wdg.move(relative=-1, pages=True)) - map(ctrl('d'), wdg.move(relative=0.5, pages=True)) - map(ctrl('u'), wdg.move(relative=-0.5, pages=True)) - map(' ', wdg.move(relative=0.8, pages=True)) + map(KEY_LEFT, wdg.move(left=4)) + map(KEY_RIGHT, wdg.move(right=4)) + map(KEY_NPAGE, ctrl('f'), wdg.move(down=1, pages=True)) + map(KEY_PPAGE, ctrl('b'), wdg.move(up=1, pages=True)) + map(ctrl('d'), wdg.move(down=0.5, pages=True)) + map(ctrl('u'), wdg.move(up=0.5, pages=True)) + map(' ', wdg.move(down=0.8, pages=True)) # ---------------------------------------------------------- others map('E', fm.edit_file()) map('?', fm.display_help()) # --------------------------------------------- less-like shortcuts - map.alias(KEY_NPAGE, 'd') - map.alias(KEY_PPAGE, 'u') + map.alias(KEY_NPAGE, 'f') + map.alias(KEY_PPAGE, 'b') + map.alias(ctrl('d'), 'd') + map.alias(ctrl('u'), 'u') def _system_functions(map): - # Each commandlist should have this bindings - map(KEY_RESIZE, fm.resize()) - map(KEY_MOUSE, fm.handle_mouse()) map('Q', fm.exit()) map(ctrl('L'), fm.redraw_window()) def _basic_movement(map): - map(KEY_DOWN, wdg.move(relative=1)) - map(KEY_UP, wdg.move(relative=-1)) - map(KEY_HOME, wdg.move(absolute=0)) - map(KEY_END, wdg.move(absolute=-1)) + map(KEY_DOWN, wdg.move(down=1)) + map(KEY_UP, wdg.move(up=1)) + map(KEY_RIGHT, wdg.move(right=1)) + map(KEY_LEFT, wdg.move(left=1)) + map(KEY_HOME, wdg.move(to=0)) + map(KEY_END, wdg.move(to=-1)) @@ -339,7 +356,7 @@ def base_directions(): map('<end>', dir=Direction(down=-1, absolute=True)) map('<pagedown>', dir=Direction(down=1, pages=True)) map('<pageup>', dir=Direction(down=-1, pages=True)) - map('%<any>', dir=Direction(down=1, percent=True, absolute=True)) + map('%<any>', dir=Direction(down=1, percentage=True, absolute=True)) map('<space>', dir=Direction(down=1, pages=True)) map('<CR>', dir=Direction(down=1)) diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index a7090285..6b2a0dc9 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -33,7 +33,8 @@ of the values stay the same. from ranger.api.options import * -# Which files are hidden if show_hidden is False? +# Which files should be hidden? Toggle this by typing `zh' or +# changing the setting `show_hidden' hidden_filter = regexp( r'lost\+found|^\.|~$|\.(:?pyc|pyo|bak|swp)$') show_hidden = False @@ -50,8 +51,19 @@ preview_directories = True max_filesize_for_preview = 300 * 1024 # 300kb collapse_preview = True +# Save the console history on exit? +save_console_history = True + # Draw borders around columns? draw_borders = False +draw_bookmark_borders = True + +# How many columns are there, and what are their relative widths? +column_ratios = (1, 1, 4, 3) + +# Display the file size in the main column or status bar? +display_size_in_main_column = True +display_size_in_status_bar = False # Set a title for the window? update_title = True @@ -80,6 +92,26 @@ show_cursor = False # One of: size, basename, mtime, type sort = 'basename' -reverse = False -directories_first = True +sort_reverse = False +sort_case_insensitive = False +sort_directories_first = True + + +# Apply an overlay function to the colorscheme. It will be called with +# 4 arguments: the context and the 3 values (fg, bg, attr) returned by +# the original use() function of your colorscheme. The return value +# must be a 3-tuple of (fg, bg, attr). +# Note: Here, the colors/attributes aren't directly imported into +# the namespace but have to be accessed with color.xyz. +def colorscheme_overlay(context, fg, bg, attr): + if context.directory and attr & color.bold and \ + not any((context.marked, context.selected)): + attr ^= color.bold # I don't like bold directories! + + if context.main_column and context.selected: + fg, bg = color.red, color.default # To highlight the main column! + + return fg, bg, attr +# The above function was just an example, let's set it back to None +colorscheme_overlay = None diff --git a/ranger/ext/direction.py b/ranger/ext/direction.py index 30eb87ce..417f3add 100644 --- a/ranger/ext/direction.py +++ b/ranger/ext/direction.py @@ -13,109 +13,123 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -class NoDefault(object): - pass - -class Direction(object): - """An object with a down and right method""" - def __init__(self, right=None, down=None, absolute=False, - percent=False, pages=False, **keywords): - self.has_explicit_direction = False - - if 'up' in keywords: - self.down = -keywords['up'] - else: - self.down = down - - if 'left' in keywords: - self.right = -keywords['left'] +""" +Directions provide convenience methods for movement operations. + +Direction objects are handled just like dicts but provide +methods like up() and down() which give you the correct value +for the vertical direction, even if only the "up" or "down" key +has been defined. + +Example application: +d = Direction(down=5) +print(d.up()) # prints -5 +print(bool(d.horizontal())) # False, since no horizontal direction is defined +""" + +class Direction(dict): + __doc__ = __doc__ # for nicer pydoc + + def __init__(self, dictionary=None, **keywords): + if dictionary is not None: + dict.__init__(self, dictionary) else: - self.right = right + dict.__init__(self, keywords) + if 'to' in self: + self['down'] = self['to'] + self['absolute'] = True - if 'relative' in keywords: - self.absolute = not relative - else: - self.absolute = absolute + def copy(self): + return Direction(**self) - if 'default' in keywords: - self.default = keywords['default'] - else: - self.default = NoDefault + def _get_bool(self, first, second, fallback=None): + try: return self[first] + except: + try: return not self[second] + except: return fallback - self.original_down = self.down - self.original_right = self.right + def _get_direction(self, first, second, fallback=0): + try: return self[first] + except: + try: return -self[second] + except: return fallback - self.percent = percent - self.pages = pages - - @property def up(self): - if self.down is None: - return None - return -self.down - - @property - def left(self): - if self.right is None: - return None - return -self.right + return -Direction.down(self) - @property - def relative(self): - return not self.absolute + def down(self): + return Direction._get_direction(self, 'down', 'up') - def down_or_default(self, default): - if self.has_been_modified: - return self.down - return default + def right(self): + return Direction._get_direction(self, 'right', 'left') - def steps_down(self, page_length=10): - if self.pages: - return self.down * page_length - else: - return self.down + def absolute(self): + return Direction._get_bool(self, 'absolute', 'relative') - def steps_right(self, page_length=10): - if self.pages: - return self.right * page_length - else: - return self.right + def left(self): + return -Direction.right(self) - def copy(self): - new = type(self)() - new.__dict__.update(self.__dict__) - return new - - def __mul__(self, other): - copy = self.copy() - if self.absolute: - if self.down is not None: - copy.down = other - if self.right is not None: - copy.right = other - else: - if self.down is not None: - copy.down *= other - if self.right is not None: - copy.right *= other - copy.original_down = self.original_down - copy.original_right = self.original_right - return copy - __rmul__ = __mul__ - - def __str__(self): - s = ['<Direction'] - if self.down is not None: - s.append(" down=" + str(self.down)) - if self.right is not None: - s.append(" right=" + str(self.right)) - if self.absolute: - s.append(" absolute") + def relative(self): + return not Direction.absolute(self) + + def vertical_direction(self): + down = Direction.down(self) + return (down > 0) - (down < 0) + + def horizontal_direction(self): + right = Direction.right(self) + return (right > 0) - (right < 0) + + def vertical(self): + return set(self) & set(['up', 'down']) + + def horizontal(self): + return set(self) & set(['left', 'right']) + + def pages(self): + return 'pages' in self and self['pages'] + + def percentage(self): + return 'percentage' in self and self['percentage'] + + def multiply(self, n): + for key in ('up', 'right', 'down', 'left'): + try: + self[key] *= n + except: + pass + + def set(self, n): + for key in ('up', 'right', 'down', 'left'): + if key in self: + self[key] = n + + def move(self, direction, override=None, minimum=0, maximum=9999, + current=0, pagesize=1, offset=0): + """ + Calculates the new position in a given boundary. + + Example: + d = Direction(pages=True) + d.move(direction=3) # = 3 + d.move(direction=3, current=2) # = 5 + d.move(direction=3, pagesize=5) # = 15 + d.move(direction=3, pagesize=5, maximum=10) # = 10 + d.move(direction=9, override=2) # = 18 + """ + pos = direction + if override is not None: + if self.absolute(): + pos = override + else: + pos *= override + if self.pages(): + pos *= pagesize + elif self.percentage(): + pos *= maximum / 100.0 + if self.absolute(): + if pos < minimum: + pos += maximum else: - s.append(" relative") - if self.pages: - s.append(" pages") - if self.percent: - s.append(" percent") - s.append('>') - return ''.join(s) + pos += current + return int(max(min(pos, maximum + offset), minimum)) diff --git a/ranger/ext/move.py b/ranger/ext/move.py deleted file mode 100644 index 948adae6..00000000 --- a/ranger/ext/move.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -def move_between(current, minimum, maximum, relative=0, absolute=None): - i = current - if isinstance(absolute, int): - i = absolute - if isinstance(relative, int): - i += relative - i = max(minimum, min(maximum - 1, i)) - return i diff --git a/ranger/ext/signal_dispatcher.py b/ranger/ext/signal_dispatcher.py new file mode 100644 index 00000000..c1630c0c --- /dev/null +++ b/ranger/ext/signal_dispatcher.py @@ -0,0 +1,103 @@ +# Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import weakref +from types import MethodType + +class Signal(dict): + stopped = False + def __init__(self, **keywords): + dict.__init__(self, keywords) + self.__dict__ = self + + def stop(self): + self.stopped = True + + +class SignalHandler(object): + active = True + def __init__(self, signal_name, function, priority, pass_signal): + self.priority = max(0, min(1, priority)) + self.signal_name = signal_name + self.function = function + self.pass_signal = pass_signal + + +class SignalDispatcher(object): + def __init__(self): + self._signals = dict() + + signal_clear = __init__ + + def signal_bind(self, signal_name, function, priority=0.5, weak=False): + assert isinstance(signal_name, str) + try: + handlers = self._signals[signal_name] + except: + handlers = self._signals[signal_name] = [] + nargs = function.__code__.co_argcount + + try: + instance = function.__self__ + except: + if weak: + function = weakref.proxy(function) + else: + nargs -= 1 + if weak: + function = (function.__func__, weakref.proxy(function.__self__)) + handler = SignalHandler(signal_name, function, priority, nargs > 0) + handlers.append(handler) + handlers.sort(key=lambda handler: -handler.priority) + return handler + + def signal_unbind(self, signal_handler): + try: + handlers = self._signals[signal_handler.signal_name] + except: + pass + else: + try: + handlers.remove(signal_handler) + except: + pass + + def signal_emit(self, signal_name, **kw): + assert isinstance(signal_name, str) + try: + handlers = self._signals[signal_name] + except: + return + if not handlers: + return + + signal = Signal(origin=self, name=signal_name, **kw) + + # propagate + for handler in tuple(handlers): + if handler.active: + try: + if isinstance(handler.function, tuple): + fnc = MethodType(*handler.function) + else: + fnc = handler.function + if handler.pass_signal: + fnc(signal) + else: + fnc() + if signal.stopped: + return + except ReferenceError: + handlers.remove(handler) diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index 8bb8a78a..79e32bff 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -17,7 +17,6 @@ import os from collections import deque from time import time -from ranger import log from ranger.fsobject import BAD_INFO, File, FileSystemObject from ranger.shared import SettingsAware from ranger.ext.accumulator import Accumulator @@ -27,9 +26,13 @@ def sort_by_basename(path): """returns path.basename (for sorting)""" return path.basename +def sort_by_basename_icase(path): + """returns case-insensitive path.basename (for sorting)""" + return path.basename_lower + def sort_by_directory(path): """returns 0 if path is a directory, otherwise 1 (for sorting)""" - return 1 - int( isinstance( path, Directory ) ) + return 1 - path.is_directory class NoDirectoryGiven(Exception): pass @@ -54,12 +57,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): last_update_time = -1 load_content_mtime = -1 - old_show_hidden = None - old_directories_first = None - old_reverse = None - old_sort = None - old_filter = None - old_hidden_filter = None + order_outdated = False + content_outdated = False sort_dict = { 'basename': sort_by_basename, @@ -79,13 +78,20 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): self.marked_items = list() - # to find out if something has changed: - self.old_show_hidden = self.settings.show_hidden - self.old_directories_first = self.settings.directories_first - self.old_sort = self.settings.sort - self.old_filter = self.filter - self.old_hidden_filter = self.settings.hidden_filter - self.old_reverse = self.settings.reverse + for opt in ('sort_directories_first', 'sort', 'sort_reverse', + 'sort_case_insensitive'): + self.settings.signal_bind('setopt.' + opt, + self.request_resort, weak=True) + + for opt in ('filter', 'hidden_filter', 'show_hidden'): + self.settings.signal_bind('setopt.' + opt, + self.request_reload, weak=True) + + def request_resort(self): + self.order_outdated = True + + def request_reload(self): + self.content_outdated = True def get_list(self): return self.files @@ -203,7 +209,6 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): else: self.mark_item(item, False) - self.old_directories_first = None self.sort() if len(self.files) > 0: @@ -236,8 +241,12 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): if not self.loading: self.load_once() + if not self.accessible: + self.content_loaded = True + return + if schedule is None: - schedule = self.size > 30 + schedule = True # was: self.size > 30 if self.load_generator is None: self.load_generator = self.load_bit_by_bit() @@ -265,12 +274,17 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): sort_func = self.sort_dict[self.settings.sort] except: sort_func = sort_by_basename + + if self.settings.sort_case_insensitive and \ + sort_func == sort_by_basename: + sort_func = sort_by_basename_icase + self.files.sort(key = sort_func) - if self.settings.reverse: + if self.settings.sort_reverse: self.files.reverse() - if self.settings.directories_first: + if self.settings.sort_directories_first: self.files.sort(key = sort_by_directory) if self.pointer is not None: @@ -278,15 +292,10 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): else: self.correct_pointer() - self.old_directories_first = self.settings.directories_first - self.old_sort = self.settings.sort - self.old_reverse = self.settings.reverse - def sort_if_outdated(self): """Sort the containing files if they are outdated""" - if self.old_directories_first != self.settings.directories_first \ - or self.old_sort != self.settings.sort \ - or self.old_reverse != self.settings.reverse: + if self.order_outdated: + self.order_outdated = False self.sort() return True return False @@ -361,12 +370,8 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): if self.load_content_once(*a, **k): return True - if self.old_show_hidden != self.settings.show_hidden or \ - self.old_filter != self.filter or \ - self.old_hidden_filter != self.settings.hidden_filter: - self.old_filter = self.filter - self.old_hidden_filter = self.settings.hidden_filter - self.old_show_hidden = self.settings.show_hidden + if self.content_outdated: + self.content_outdated = False self.load_content(*a, **k) return True @@ -374,6 +379,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): real_mtime = os.stat(self.path).st_mtime except OSError: real_mtime = None + return False if self.stat: cached_mtime = self.load_content_mtime else: diff --git a/ranger/fsobject/file.py b/ranger/fsobject/file.py index 86621095..aa44016e 100644 --- a/ranger/fsobject/file.py +++ b/ranger/fsobject/file.py @@ -13,6 +13,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +control_characters = set(chr(n) for n in set(range(0, 9)) | set(range(14,32))) +N_FIRST_BYTES = 20 + from .fsobject import FileSystemObject as SuperClass class File(SuperClass): is_file = True + + @property + def firstbytes(self): + try: + return self._firstbytes + except: + try: + f = open(self.path, 'r') + self._firstbytes = f.read(N_FIRST_BYTES) + f.close() + return self._firstbytes + except: + pass + + def is_binary(self): + if self.firstbytes and control_characters & set(self.firstbytes): + return True + return False + diff --git a/ranger/fsobject/fsobject.py b/ranger/fsobject/fsobject.py index 4278c3e8..1ab3addd 100644 --- a/ranger/fsobject/fsobject.py +++ b/ranger/fsobject/fsobject.py @@ -17,6 +17,7 @@ CONTAINER_EXTENSIONS = 'rar zip tar gz bz bz2 tgz 7z iso cab'.split() DOCUMENT_EXTENSIONS = 'pdf doc ppt odt'.split() DOCUMENT_BASENAMES = 'README TODO LICENSE COPYING INSTALL'.split() +import time from . import T_FILE, T_DIRECTORY, T_UNKNOWN, T_NONEXISTANT, BAD_INFO from ranger.shared import MimeTypeAware, FileManagerAware from ranger.ext.shell_escape import shell_escape @@ -81,6 +82,9 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): self.set_mimetype() self.use() + def __repr__(self): + return "<{0} {1}>".format(self.__class__.__name__, self.path) + @property def shell_escaped_basename(self): if self._shell_escaped_basename is None: @@ -109,12 +113,12 @@ class FileSystemObject(MimeTypeAware, FileManagerAware): def use(self): """mark the filesystem-object as used at the current time""" - import time self.last_used = time.time() def is_older_than(self, seconds): """returns whether this object wasn't use()d in the last n seconds""" - import time + if seconds < 0: + return True return self.last_used + seconds < time.time() def set_mimetype(self): diff --git a/ranger/fsobject/loader.py b/ranger/fsobject/loader.py index b47dd9c9..4f4424e4 100644 --- a/ranger/fsobject/loader.py +++ b/ranger/fsobject/loader.py @@ -77,7 +77,7 @@ class Loader(FileManagerAware): def remove(self, item=None, index=None): if item is not None and index is None: - for test, i in zip(self.queue, range(len(self.queue))): + for i, test in enumerate(self.queue): if test == item: index = i break diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index 199a5523..dffacffb 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -41,11 +41,20 @@ If your colorscheme-file contains more than one colorscheme, specify it with: colorscheme = colorschemes.filename.classname """ +import os from curses import color_pair +from inspect import isclass, ismodule + +import ranger from ranger.gui.color import get_color from ranger.gui.context import Context +from ranger.shared.settings import SettingsAware + +# ColorScheme is not SettingsAware but it will gain access +# to the settings during the initialization. We can't import +# SettingsAware here because of circular imports. -class ColorScheme(object): +class ColorScheme(SettingsAware): """ This is the class that colorschemes must inherit from. @@ -73,6 +82,14 @@ class ColorScheme(object): # add custom error messages for broken colorschemes color = self.use(context) + if self.settings.colorscheme_overlay: + result = self.settings.colorscheme_overlay(context, *color) + assert isinstance(result, (tuple, list)), \ + "Your colorscheme overlay doesn't return a tuple!" + assert all(isinstance(val, int) for val in result), \ + "Your colorscheme overlay doesn't return a tuple"\ + " containing 3 integers!" + color = result self.cache[keys] = color return color @@ -102,3 +119,55 @@ class ColorScheme(object): attr |= 2097152 fg = 4 return fg, -1, attr + +def _colorscheme_name_to_class(signal): + # Find the colorscheme. First look for it at ~/.ranger/colorschemes, + # then at RANGERDIR/colorschemes. If the file contains a class + # named Scheme, it is used. Otherwise, an arbitrary other class + # is picked. + if isinstance(signal.value, ColorScheme): return + + scheme_name = signal.value + usecustom = not ranger.arg.clean + + def exists(colorscheme): + return os.path.exists(colorscheme + '.py') + + def is_scheme(x): + return isclass(x) and issubclass(x, ColorScheme) + + # create ~/.ranger/colorschemes/__init__.py if it doesn't exist + if usecustom: + if os.path.exists(ranger.relpath_conf('colorschemes')): + initpy = ranger.relpath_conf('colorschemes', '__init__.py') + if not os.path.exists(initpy): + open(initpy, 'a').close() + + if usecustom and \ + exists(ranger.relpath_conf('colorschemes', scheme_name)): + scheme_supermodule = 'colorschemes' + elif exists(ranger.relpath('colorschemes', scheme_name)): + scheme_supermodule = 'ranger.colorschemes' + else: + scheme_supermodule = None # found no matching file. + + if scheme_supermodule is None: + # XXX: dont print while curses is running + print("ERROR: colorscheme not found, fall back to builtin scheme") + if ranger.arg.debug: + raise Exception("Cannot locate colorscheme!") + signal.value = ColorScheme() + else: + scheme_module = getattr(__import__(scheme_supermodule, + globals(), locals(), [scheme_name], 0), scheme_name) + assert ismodule(scheme_module) + if hasattr(scheme_module, 'Scheme') \ + and is_scheme(scheme_module.Scheme): + signal.value = scheme_module.Scheme() + else: + for name, var in scheme_module.__dict__.items(): + if var != ColorScheme and is_scheme(var): + signal.value = var() + break + else: + raise Exception("The module contains no valid colorscheme!") diff --git a/ranger/gui/context.py b/ranger/gui/context.py index d31124ca..4ea50714 100644 --- a/ranger/gui/context.py +++ b/ranger/gui/context.py @@ -17,7 +17,7 @@ CONTEXT_KEYS = ['reset', 'error', 'in_browser', 'in_statusbar', 'in_titlebar', 'in_console', 'in_pager', 'in_taskview', 'directory', 'file', 'hostname', - 'executable', 'media', 'link', + 'executable', 'media', 'link', 'fifo', 'socket', 'video', 'audio', 'image', 'media', 'document', 'container', 'selected', 'empty', 'main_column', 'message', 'background', 'good', 'bad', @@ -26,7 +26,7 @@ CONTEXT_KEYS = ['reset', 'error', 'marked', 'tagged', 'tag_marker', 'help_markup', 'seperator', 'key', 'special', 'border', - 'title', 'text', 'highlight', 'bars', 'quotes', + 'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'keybuffer'] class Context(object): diff --git a/ranger/gui/defaultui.py b/ranger/gui/defaultui.py index e6a365de..08e0b204 100644 --- a/ranger/gui/defaultui.py +++ b/ranger/gui/defaultui.py @@ -15,8 +15,6 @@ from ranger.gui.ui import UI -RATIO = ( 3, 3, 12, 9 ) - class DefaultUI(UI): def setup(self): """Build up the UI by initializing widgets.""" @@ -32,9 +30,10 @@ class DefaultUI(UI): self.add_child(self.titlebar) # Create the browser view - self.browser = BrowserView(self.win, RATIO) + self.browser = BrowserView(self.win, self.settings.column_ratios) + self.settings.signal_bind('setopt.column_ratios', + self.browser.change_ratios) self.add_child(self.browser) - self.main_column = self.browser.main_column # Create the process manager self.taskview = TaskView(self.win) diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py index ceefb3f1..a7a0945d 100644 --- a/ranger/gui/displayable.py +++ b/ranger/gui/displayable.py @@ -72,6 +72,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): self.hei = 0 self.paryx = (0, 0) self.parent = None + self.fresh = True self._old_visible = self.visible @@ -154,7 +155,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): def resize(self, y, x, hei=None, wid=None): """Resize the widget""" - do_move = False + do_move = self.fresh try: maxy, maxx = self.env.termsize except TypeError: @@ -212,6 +213,7 @@ class Displayable(EnvironmentAware, FileManagerAware, CursesShortcuts): except: pass + self.fresh = False self.paryx = self.win.getparyx() self.y, self.x = self.paryx if self.parent: @@ -291,7 +293,7 @@ class DisplayableContainer(Displayable): return True for displayable in self.container: - if event in displayable: + if displayable.visible and event in displayable: if displayable.click(event): return True @@ -314,7 +316,7 @@ class DisplayableContainer(Displayable): def remove_child(self, obj): """Remove the object from the container.""" try: - container.remove(obj) + self.container.remove(obj) except ValueError: pass else: diff --git a/ranger/gui/mouse_event.py b/ranger/gui/mouse_event.py index d588fd8e..f3955825 100644 --- a/ranger/gui/mouse_event.py +++ b/ranger/gui/mouse_event.py @@ -41,6 +41,15 @@ class MouseEvent(object): except: return False + def mouse_wheel_direction(self): + if self.bstate & curses.BUTTON4_PRESSED: + return -1 + elif self.bstate & curses.BUTTON2_PRESSED \ + or self.bstate > curses.ALL_MOUSE_EVENTS: + return 1 + else: + return 0 + def ctrl(self): return self.bstate & curses.BUTTON_CTRL diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py index 2d86736c..c7c2090a 100644 --- a/ranger/gui/ui.py +++ b/ranger/gui/ui.py @@ -84,8 +84,6 @@ class UI(DisplayableContainer): def suspend(self): """Turn off curses""" - # from ranger import log - # log("suspending ui!") self.win.keypad(0) curses.nocbreak() curses.echo() @@ -111,8 +109,8 @@ class UI(DisplayableContainer): def destroy(self): """Destroy all widgets and turn off curses""" - DisplayableContainer.destroy(self) self.suspend() + DisplayableContainer.destroy(self) def handle_mouse(self): """Handles mouse input""" @@ -121,10 +119,6 @@ class UI(DisplayableContainer): except _curses.error: return - # from ranger import log - # log('{0:0>28b} ({0})'.format(event.bstate)) - # log('y: {0} x: {1}'.format(event.y, event.x)) - DisplayableContainer.click(self, event) def handle_key(self, key): @@ -153,7 +147,7 @@ class UI(DisplayableContainer): if cmd.function: try: - cmd.function(CommandArgs.from_widget(self)) + cmd.function(CommandArgs.from_widget(self.fm)) except Exception as error: self.fm.notify(error) if kbuf.done: @@ -197,7 +191,7 @@ class UI(DisplayableContainer): self.env.termsize = self.win.getmaxyx() def draw(self): - """Erase the window, then draw all objects in the container""" + """Draw all objects in the container""" self.win.touchwin() DisplayableContainer.draw(self) if self._draw_title and self.settings.update_title: diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index 238a4803..0d46ee06 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -15,6 +15,7 @@ """The BrowserColumn widget displays the contents of a directory or file.""" import re +import stat from time import time from . import Widget @@ -41,6 +42,15 @@ PREVIEW_BLACKLIST = re.compile(r""" $ """, re.VERBOSE | re.IGNORECASE) +PREVIEW_WHITELIST = re.compile(r""" + \.( + txt | py | c + ) + # ignore filetype-independent suffixes: + (\.part|\.bak|~)? + $ +""", re.VERBOSE | re.IGNORECASE) + class BrowserColumn(Pager): main_column = False display_infostring = False @@ -65,6 +75,12 @@ class BrowserColumn(Pager): Widget.__init__(self, win) self.level = level + self.settings.signal_bind('setopt.display_size_in_main_column', + self.request_redraw, weak=True) + + def request_redraw(self): + self.need_redraw = True + def resize(self, y, x, hei, wid): Widget.resize(self, y, x, hei, wid) @@ -86,7 +102,7 @@ class BrowserColumn(Pager): self.fm.enter_dir(self.target.path) if index < len(self.target): - self.fm.move_pointer(absolute = index) + self.fm.move(to=index) elif event.pressed(3): try: clicked_file = self.target.files[index] @@ -96,7 +112,7 @@ class BrowserColumn(Pager): else: if self.level > 0: - self.fm.move_right() + self.fm.move(right=0) return True @@ -124,6 +140,9 @@ class BrowserColumn(Pager): self.need_redraw = True self.old_dir = self.target + if self.target: # don't garbage collect this directory please + self.target.use() + 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: @@ -149,10 +168,24 @@ class BrowserColumn(Pager): self.last_redraw_time = time() def _preview_this_file(self, target): + if not (target \ + and self.settings.preview_files \ + and target.is_file \ + and target.accessible \ + and target.stat \ + and not target.stat.st_mode & stat.S_IFIFO): + return False + maxsize = self.settings.max_filesize_for_preview - return self.settings.preview_files \ - and not PREVIEW_BLACKLIST.search(target.basename) \ - and (maxsize is None or maxsize >= target.size) + if maxsize is not None and target.size > maxsize: + return False + if PREVIEW_WHITELIST.search(target.basename): + return True + if PREVIEW_BLACKLIST.search(target.basename): + return False + if target.is_binary(): + return False + return True def _draw_file(self): """Draw a preview of the file, if the settings allow it""" @@ -183,8 +216,6 @@ class BrowserColumn(Pager): base_color = ['in_browser'] - self.target.use() - self.win.move(0, 0) if not self.target.content_loaded: @@ -215,18 +246,18 @@ class BrowserColumn(Pager): i = line + self.scroll_begin try: - drawed = self.target.files[i] + drawn = self.target.files[i] except IndexError: break - this_color = base_color + list(drawed.mimetype_tuple) - text = drawed.basename - tagged = self.fm.tags and drawed.realpath in self.fm.tags + this_color = base_color + list(drawn.mimetype_tuple) + text = drawn.basename + tagged = self.fm.tags and drawn.realpath in self.fm.tags if i == selected_i: this_color.append('selected') - if drawed.marked: + if drawn.marked: this_color.append('marked') if self.main_column: text = " " + text @@ -236,19 +267,25 @@ class BrowserColumn(Pager): if self.main_column: text = self.tagged_marker + text - if drawed.is_directory: + if drawn.is_directory: this_color.append('directory') else: this_color.append('file') - if drawed.stat is not None and drawed.stat.st_mode & stat.S_IXUSR: - this_color.append('executable') + if drawn.stat: + mode = drawn.stat.st_mode + if mode & stat.S_IXUSR: + this_color.append('executable') + if stat.S_ISFIFO(mode): + this_color.append('fifo') + if stat.S_ISSOCK(mode): + this_color.append('socket') - if drawed.islink: + if drawn.islink: this_color.append('link') - this_color.append(drawed.exists and 'good' or 'bad') + this_color.append(drawn.exists and 'good' or 'bad') - string = drawed.basename + string = drawn.basename try: if self.main_column: if tagged: @@ -258,8 +295,9 @@ class BrowserColumn(Pager): else: self.win.addnstr(line, 0, text, self.wid) - if self.display_infostring and drawed.infostring: - info = drawed.infostring + if self.display_infostring and drawn.infostring \ + and self.settings.display_size_in_main_column: + info = drawn.infostring x = self.wid - 1 - len(info) if x > self.x: self.win.addstr(line, x, str(info) + ' ') @@ -320,19 +358,11 @@ class BrowserColumn(Pager): self.scroll_begin = self._get_scroll_begin() self.target.scroll_begin = self.scroll_begin - # TODO: does not work if options.scroll_offset is high, - # relative > 1 and you scroll from scroll_begin = 1 to 0 def scroll(self, relative): """scroll by n lines""" self.need_redraw = True - self._set_scroll_begin() - old_value = self.target.scroll_begin - self.target.scroll_begin += relative - self._set_scroll_begin() - - if self.target.scroll_begin == old_value: - self.target.move(relative = relative) - self.target.scroll_begin += relative + self.target.move(relative=relative) + self.target.scroll_begin += 3 * relative def __str__(self): return self.__class__.__name__ + ' at level ' + str(self.level) diff --git a/ranger/gui/widgets/browserview.py b/ranger/gui/widgets/browserview.py index 8d6dc611..1995b714 100644 --- a/ranger/gui/widgets/browserview.py +++ b/ranger/gui/widgets/browserview.py @@ -15,6 +15,7 @@ """The BrowserView manages a set of BrowserColumns.""" import curses +from ranger.ext.signal_dispatcher import Signal from . import Widget from .browsercolumn import BrowserColumn from .pager import Pager @@ -30,13 +31,31 @@ class BrowserView(Widget, DisplayableContainer): def __init__(self, win, ratios, preview = True): DisplayableContainer.__init__(self, win) - self.ratios = ratios self.preview = preview - self.old_cf = self.env.cf - self.old_prevfile = None - self.old_prevdir = None + self.columns = [] + + self.pager = Pager(self.win, embedded=True) + self.pager.visible = False + self.add_child(self.pager) + + self.change_ratios(ratios, resize=False) + + for option in ('preview_directories', 'preview_files'): + self.settings.signal_bind('setopt.' + option, + self._request_clear_if_has_borders, weak=True) + + self.fm.env.signal_bind('move', self.request_clear) + self.settings.signal_bind('setopt.column_ratios', self.request_clear) + + def change_ratios(self, ratios, resize=True): + if isinstance(ratios, Signal): + ratios = ratios.value + + for column in self.columns: + column.destroy() + self.remove_child(column) + self.columns = [] - # normalize ratios: ratio_sum = float(sum(ratios)) self.ratios = tuple(x / ratio_sum for x in ratios) @@ -46,42 +65,38 @@ class BrowserView(Widget, DisplayableContainer): (self.ratios[-1] * 0.1)) offset = 1 - len(ratios) - if preview: offset += 1 + if self.preview: offset += 1 for level in range(len(ratios)): fl = BrowserColumn(self.win, level + offset) self.add_child(fl) + self.columns.append(fl) try: - self.main_column = self.container[preview and -2 or -1] + self.main_column = self.columns[self.preview and -2 or -1] except IndexError: self.main_column = None else: self.main_column.display_infostring = True self.main_column.main_column = True - self.pager = Pager(self.win, embedded=True) - self.pager.visible = False - self.add_child(self.pager) + self.resize(self.y, self.x, self.hei, self.wid) + + def _request_clear_if_has_borders(self): + if self.settings.draw_borders: + self.request_clear() + + def request_clear(self): + self.need_clear = True def draw(self): if self.draw_bookmarks: self._draw_bookmarks() else: - if self.old_cf != self.env.cf: - self.need_clear = True - if self.settings.draw_borders: - if self.old_prevdir != self.settings.preview_directories: - self.need_clear = True - if self.old_prevfile != self.settings.preview_files: - self.need_clear = True if self.need_clear: self.win.erase() self.need_redraw = True self.need_clear = False - self.old_cf = self.env.cf - self.old_prevfile = self.settings.preview_files - self.old_prevdir = self.settings.preview_directories DisplayableContainer.draw(self) if self.settings.draw_borders: self._draw_borders() @@ -102,13 +117,14 @@ class BrowserView(Widget, DisplayableContainer): pass def _draw_bookmarks(self): + self.color_reset() self.need_clear = True sorted_bookmarks = sorted(item for item in self.fm.bookmarks \ if '/.' not in item[1].path) def generator(): - return zip(range(self.hei), sorted_bookmarks) + return zip(range(self.hei-1), sorted_bookmarks) try: maxlen = max(len(item[1].path) for i, item in generator()) @@ -116,10 +132,19 @@ class BrowserView(Widget, DisplayableContainer): return maxlen = min(maxlen + 5, self.wid) + whitespace = " " * maxlen for line, items in generator(): key, mark = items string = " " + key + ": " + mark.path - self.addnstr(line, 0, string.ljust(maxlen), self.wid) + self.addstr(line, 0, whitespace) + self.addnstr(line, 0, string, self.wid) + + if self.settings.draw_bookmark_borders: + self.win.hline(line+1, 0, curses.ACS_HLINE, maxlen) + + if maxlen < self.wid: + self.win.vline(0, maxlen, curses.ACS_VLINE, line+1) + self.win.addch(line+1, maxlen, curses.ACS_LRCORNER) def _draw_borders(self): win = self.win @@ -128,17 +153,13 @@ class BrowserView(Widget, DisplayableContainer): left_start = 0 right_end = self.wid - 1 - rows = [row for row in self.container \ - if isinstance(row, BrowserColumn)] - rows.sort(key=lambda row: row.x) - - for child in rows: + for child in self.columns: if not child.has_preview(): left_start = child.x + child.wid else: break if not self.pager.visible: - for child in reversed(rows): + for child in reversed(self.columns): if not child.has_preview(): right_end = child.x - 1 else: @@ -151,7 +172,7 @@ class BrowserView(Widget, DisplayableContainer): right_end - left_start) win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2) - for child in rows: + for child in self.columns: if not child.has_preview(): continue if child.main_column and self.pager.visible: @@ -203,7 +224,7 @@ class BrowserView(Widget, DisplayableContainer): max(1, self.wid - left - pad)) try: - self.container[i].resize(pad, left, hei - pad * 2, \ + self.columns[i].resize(pad, left, hei - pad * 2, \ max(1, wid - 1)) except KeyError: pass @@ -211,11 +232,10 @@ class BrowserView(Widget, DisplayableContainer): left += wid def click(self, event): - n = event.ctrl() and 1 or 3 - if event.pressed(4): - self.main_column.scroll(relative = -n) - elif event.pressed(2) or event.key_invalid(): - self.main_column.scroll(relative = n) + n = event.ctrl() and 5 or 1 + direction = event.mouse_wheel_direction() + if direction: + self.main_column.scroll(direction) else: DisplayableContainer.click(self, event) @@ -225,8 +245,8 @@ class BrowserView(Widget, DisplayableContainer): self.need_clear = True self.pager.open() try: - self.container[-2].visible = False - self.container[-3].visible = False + self.columns[-1].visible = False + self.columns[-2].visible = False except IndexError: pass @@ -236,15 +256,15 @@ class BrowserView(Widget, DisplayableContainer): self.need_clear = True self.pager.close() try: - self.container[-2].visible = True - self.container[-3].visible = True + self.columns[-1].visible = True + self.columns[-2].visible = True except IndexError: pass def poke(self): DisplayableContainer.poke(self) if self.settings.collapse_preview and self.preview: - has_preview = self.container[-2].has_preview() + has_preview = self.columns[-2].has_preview() if self.preview_available != has_preview: self.preview_available = has_preview self.resize(self.y, self.x, self.hei, self.wid) diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py index 4dea98c7..aaa85d8e 100644 --- a/ranger/gui/widgets/console.py +++ b/ranger/gui/widgets/console.py @@ -25,8 +25,10 @@ from collections import deque from . import Widget from ranger.defaults import commands from ranger.gui.widgets.console_mode import is_valid_mode, mode_to_class -from ranger import log +from ranger import log, relpath_conf from ranger.ext.shell_escape import shell_quote +from ranger.ext.direction import Direction +import ranger DEFAULT_HISTORY = 0 SEARCH_HISTORY = 1 @@ -52,17 +54,38 @@ class Console(Widget): histories = None override = None allow_close = False + historypaths = [] def __init__(self, win): from ranger.container import History Widget.__init__(self, win) self.keymap = self.settings.keys.console_keys self.clear() - self.histories = [None] * 4 - self.histories[DEFAULT_HISTORY] = History() - self.histories[SEARCH_HISTORY] = History() - self.histories[QUICKOPEN_HISTORY] = History() - self.histories[OPEN_HISTORY] = History() + self.histories = [] + # load histories from files + if not ranger.arg.clean: + self.historypaths = [relpath_conf(x) for x in \ + ('history', 'history_search', 'history_qopen', 'history_open')] + for i, path in enumerate(self.historypaths): + hist = History(self.settings.max_history_size) + self.histories.append(hist) + if ranger.arg.clean: continue + try: f = open(path, 'r') + except: continue + for line in f: + hist.add(line[:-1]) + f.close() + + def destroy(self): + # save histories from files + if ranger.arg.clean or not self.settings.save_console_history: + return + for i, path in enumerate(self.historypaths): + try: f = open(path, 'w') + except: continue + for entry in self.histories[i]: + f.write(entry + '\n') + f.close() def init(self): """override this. Called directly after class change""" @@ -73,12 +96,16 @@ class Console(Widget): self.win.erase() self.addstr(0, 0, self.prompt) - self.addstr(self.line) + overflow = -self.wid + len(self.prompt) + len(self.line) + 1 + if overflow > 0: + self.addstr(self.line[overflow:]) + else: + self.addstr(self.line) def finalize(self): try: self.fm.ui.win.move(self.y, - self.x + self.pos + len(self.prompt)) + self.x + min(self.wid-1, self.pos + len(self.prompt))) except: pass @@ -135,9 +162,15 @@ class Console(Widget): except KeyError: # An unclean hack to allow unicode input. # This whole part should be replaced. - self.type_key(chr(keytuple[0])) - self.env.key_clear() - return + try: + chrkey = chr(keytuple[0]) + except: + pass + else: + self.type_key(chrkey) + finally: + self.env.key_clear() + return if cmd == self.commandlist.dummy_object: return @@ -181,14 +214,16 @@ class Console(Widget): self.history.fast_forward() self.history.modify(self.line) - def move(self, relative = 0, absolute = None): - if absolute is not None: - if absolute < 0: - self.pos = len(self.line) + 1 + absolute - else: - self.pos = absolute - - self.pos = min(max(0, self.pos + relative), len(self.line)) + def move(self, **keywords): + from ranger import log + log(keywords) + direction = Direction(keywords) + if direction.horizontal(): + self.pos = direction.move( + direction=direction.right(), + minimum=0, + maximum=len(self.line) + 1, + current=self.pos) def delete_rest(self, direction): self.tab_deque = None @@ -227,7 +262,7 @@ class Console(Widget): pos = self.pos + mod self.line = self.line[0:pos] + self.line[pos+1:] - self.move(relative = mod) + self.move(right=mod) self.on_line_change() def execute(self): @@ -388,6 +423,8 @@ class OpenConsole(ConsoleWithTab): def execute(self): command, flags = self._parse() + if not command and 'p' in flags: + command = 'cat %f' if command: if _CustomTemplate.delimiter in command: command = self._substitute_metachars(command) diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index c5ed8af1..e915f790 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -18,8 +18,7 @@ The pager displays text and allows you to scroll inside it. """ import re from . import Widget -from ranger.ext.move import move_between -from ranger import log +from ranger.ext.direction import Direction BAR_REGEXP = re.compile(r'\|\d+\?\|') QUOTES_REGEXP = re.compile(r'"[^"]+?"') @@ -46,6 +45,10 @@ class Pager(Widget): else: self.keymap = self.settings.keys.pager_keys + def move_horizontal(self, *a, **k): + """For compatibility""" + self.fm.notify("Your keys.py is out of date. Can't scroll!", bad=True) + def open(self): self.scroll_begin = 0 self.markup = None @@ -112,50 +115,26 @@ class Pager(Widget): if TITLE_REGEXP.match(line): self.color_at(i, 0, -1, 'title', *baseclr) - - def move(self, relative=0, absolute=None, pages=None, narg=None): - i = self.scroll_begin - if isinstance(absolute, int): - if isinstance(narg, int): - absolute = narg - if absolute < 0: - i = absolute + len(self.lines) - else: - i = absolute - - if relative != 0: - if isinstance(pages, int): - relative *= pages * self.hei - if isinstance(narg, int): - relative *= narg - i = int(i + relative) - - length = len(self.lines) - self.hei - if i >= length: - self._get_line(i+self.hei) - - length = len(self.lines) - self.hei - if i >= length: - i = length - - if i < 0: - i = 0 - - self.scroll_begin = i - - def move_horizontal(self, relative=0, absolute=None, narg=None): - if narg is not None: - if absolute is None: - relative = relative < 0 and -narg or narg - else: - absolute = narg - - self.startx = move_between( - current=self.startx, - minimum=0, - maximum=999, - relative=relative, - absolute=absolute) + def move(self, narg=None, **kw): + direction = Direction(kw) + if direction.horizontal(): + self.startx = direction.move( + direction=direction.right(), + override=narg, + maximum=self._get_max_width(), + current=self.startx, + pagesize=self.wid, + offset=-self.wid) + if direction.vertical(): + if self.source_is_stream: + self._get_line(self.scroll_begin + self.hei * 2) + self.scroll_begin = direction.move( + direction=direction.down(), + override=narg, + maximum=len(self.lines), + current=self.scroll_begin, + pagesize=self.hei, + offset=-self.hei) def press(self, key): try: @@ -201,13 +180,13 @@ class Pager(Widget): def click(self, event): n = event.ctrl() and 1 or 3 - if event.pressed(4): - self.move(relative = -n) - elif event.pressed(2) or event.key_invalid(): - self.move(relative = n) + direction = event.mouse_wheel_direction() + if direction: + self.move(relative=direction) return True def _get_line(self, n, attempt_to_read=True): + assert isinstance(n, int), n try: return self.lines[n] except (KeyError, IndexError): @@ -234,3 +213,6 @@ class Pager(Widget): except IndexError: raise StopIteration i += 1 + + def _get_max_width(self): + return max(len(line) for line in self.lines) diff --git a/ranger/gui/widgets/statusbar.py b/ranger/gui/widgets/statusbar.py index 6f52f8ef..75fbbe89 100644 --- a/ranger/gui/widgets/statusbar.py +++ b/ranger/gui/widgets/statusbar.py @@ -47,6 +47,11 @@ class StatusBar(Widget): def __init__(self, win, column=None): Widget.__init__(self, win) self.column = column + self.settings.signal_bind('setopt.display_size_in_status_bar', + self.request_redraw, weak=True) + + def request_redraw(self): + self.need_redraw = True def notify(self, text, duration=4, bad=False): self.msg = Message(text, duration, bad) @@ -157,12 +162,16 @@ class StatusBar(Widget): left.add(self._get_owner(target), 'owner') left.add_space() left.add(self._get_group(target), 'group') - left.add_space() if target.islink: how = target.exists and 'good' or 'bad' - left.add('-> ' + target.readlink, 'link', how) + left.add(' -> ' + target.readlink, 'link', how) else: + if self.settings.display_size_in_status_bar and target.infostring: + left.add(target.infostring) + + left.add_space() + left.add(strftime(self.timeformat, localtime(target.stat.st_mtime)), 'mtime') diff --git a/ranger/gui/widgets/titlebar.py b/ranger/gui/widgets/titlebar.py index e1be8e97..62740e2d 100644 --- a/ranger/gui/widgets/titlebar.py +++ b/ranger/gui/widgets/titlebar.py @@ -30,18 +30,65 @@ class TitleBar(Widget): old_wid = None result = None throbber = ' ' + need_redraw = False + tab_width = 0 + + def __init__(self, *args, **keywords): + Widget.__init__(self, *args, **keywords) + self.fm.signal_bind('tab.change', self.request_redraw, weak=True) + + def request_redraw(self): + self.need_redraw = True def draw(self): - if self.env.cf != self.old_cf or\ + if self.need_redraw or \ + self.env.cf != self.old_cf or\ str(self.env.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._calc_bar() self._print_result(self.result) if self.wid > 2: self.color('in_titlebar', 'throbber') - self.win.addnstr(self.y, self.wid - 2, self.throbber, 1) + self.win.addnstr(self.y, self.wid - 2 - self.tab_width, + self.throbber, 1) + + def click(self, event): + """Handle a MouseEvent""" + direction = event.mouse_wheel_direction() + if direction: + self.fm.tab_move(direction) + self.need_redraw = True + return True + + if not event.pressed(1) or not self.result: + return False + + pos = self.wid - 1 + for tabname in reversed(self.fm._get_tab_list()): + pos -= len(str(tabname)) + 1 + if event.x > pos: + self.fm.tab_open(tabname) + self.need_redraw = True + return True + + pos = 0 + for i, part in enumerate(self.result): + pos += len(part.string) + if event.x < pos: + if i < 2: + self.fm.enter_dir("~") + elif i == 2: + self.fm.enter_dir("/") + else: + try: + self.fm.env.enter_dir(self.env.pathway[(i-3)/2]) + except: + pass + return True + return False def _calc_bar(self): bar = Bar('in_titlebar') @@ -80,6 +127,12 @@ class TitleBar(Widget): self.old_keybuffer = kb bar.addright(kb, 'keybuffer', fixedsize=True) bar.addright(' ', 'space', fixedsize=True) + self.tab_width = 0 + if len(self.fm.tabs) > 1: + for tabname in self.fm._get_tab_list(): + self.tab_width += len(str(tabname)) + 1 + clr = 'good' if tabname == self.fm.current_tab else 'bad' + bar.addright(' '+str(tabname), 'tab', clr, fixedsize=True) def _print_result(self, result): import _curses diff --git a/ranger/help/movement.py b/ranger/help/movement.py index a0407838..4ea2b0c3 100644 --- a/ranger/help/movement.py +++ b/ranger/help/movement.py @@ -21,7 +21,8 @@ 1.3. Searching 1.4. Cycling 1.5. Bookmarks -1.6. Mouse usage +1.6. Tabs +1.7. Mouse usage ============================================================================== @@ -72,7 +73,7 @@ These keys work like in vim: ^R clear the cache and reload the view ^L redraw the window : open the console |3?| - b toggle options + z toggle options i inspect the content of the file E edit the file @@ -102,7 +103,7 @@ visible files. Pressing "n" will move you to the next occurance, "N" to the previous one. You can search for more than just strings: - TAB search tagged files + ct search tagged files cc cycle through all files by their ctime (last modification) cm cycle by mime type, connecting similar files cs cycle by size, large items first @@ -134,7 +135,21 @@ Note: The ' key is equivalent to `. ============================================================================== -1.6. Mouse usage +1.6. Tabs + +Tabs are used to work in different directories in the same Ranger instance. +In Ranger, tabs are very simple though and only store the directory path. + + gt Go to the next tab. (also TAB) + gT Go to the previous tab. (also Shift+TAB) + gn, ^N Create a new tab + g<N> Open a tab. N has to be a number from 0 to 9. + If the tab doesn't exist yet, it will be created. + gc, ^W Close the current tab. The last tab cannot be closed. + + +============================================================================== +1.7. Mouse usage The mouse can be used to quickly enter directories which you point at, or to scroll around with the mouse wheel. The implementation of the mouse diff --git a/ranger/shared/settings.py b/ranger/shared/settings.py index cdddd623..a4a58e6e 100644 --- a/ranger/shared/settings.py +++ b/ranger/shared/settings.py @@ -13,22 +13,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os -import types -from inspect import isclass, ismodule import ranger +from ranger.ext.signal_dispatcher import SignalDispatcher from ranger.ext.openstruct import OpenStruct -from ranger.gui.colorscheme import ColorScheme ALLOWED_SETTINGS = { 'show_hidden': bool, 'show_cursor': bool, 'autosave_bookmarks': bool, + 'save_console_history': bool, 'collapse_preview': bool, + 'column_ratios': (tuple, list, set), + 'display_size_in_main_column': bool, + 'display_size_in_status_bar': bool, 'draw_borders': bool, + 'draw_bookmark_borders': bool, 'sort': str, - 'reverse': bool, - 'directories_first': bool, + 'sort_reverse': bool, + 'sort_case_insensitive': bool, + 'sort_directories_first': bool, 'update_title': bool, 'shorten_title': int, # Note: False is an instance of int 'max_filesize_for_preview': (int, type(None)), @@ -38,26 +41,88 @@ ALLOWED_SETTINGS = { 'preview_directories': bool, 'flushinput': bool, 'colorscheme': str, + 'colorscheme_overlay': (type(None), type(lambda:0)), 'hidden_filter': lambda x: isinstance(x, str) or hasattr(x, 'match'), } + +COMPAT_MAP = { + 'sort_reverse': 'reverse', + 'sort_directories_first': 'directories_first', +} + + +class SettingObject(SignalDispatcher): + def __init__(self): + SignalDispatcher.__init__(self) + self.__dict__['_settings'] = dict() + self.__dict__['_setting_sources'] = list() + + def __setattr__(self, name, value): + if name[0] == '_': + self.__dict__[name] = value + else: + assert name in self._settings, "No such setting: {0}!".format(name) + assert self._check_type(name, value) + kws = dict(setting=name, value=value, + previous=self._settings[name]) + self.signal_bind('setopt.'+name, + self._raw_set_with_signal, priority=0.2) + self.signal_emit('setopt', **kws) + self.signal_emit('setopt.'+name, **kws) + + def __getattr__(self, name): + assert name in ALLOWED_SETTINGS or name in self._settings, \ + "No such setting: {0}!".format(name) + try: + return self._settings[name] + except: + for struct in self._setting_sources: + try: value = getattr(struct, name) + except: pass + else: break + else: + raise Exception("The option `{0}' was not defined" \ + " in the defaults!".format(name)) + assert self._check_type(name, value) + self._raw_set(name, value) + self.__setattr__(name, value) + return self._settings[name] + + def _check_type(self, name, value): + from inspect import isfunction + typ = ALLOWED_SETTINGS[name] + if isfunction(typ): + assert typ(value), \ + "The option `" + name + "' has an incorrect type!" + else: + assert isinstance(value, typ), \ + "The option `" + name + "' has an incorrect type!"\ + " Got " + str(type(value)) + ", expected " + str(typ) + "!" + return True + + __getitem__ = __getattr__ + __setitem__ = __setattr__ + + def _raw_set(self, name, value): + self._settings[name] = value + + def _raw_set_with_signal(self, signal): + self._settings[signal.setting] = signal.value + + # -- globalize the settings -- class SettingsAware(object): settings = OpenStruct() @staticmethod def _setup(): - settings = OpenStruct() + settings = SettingObject() - from ranger.defaults import options - 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)) + from ranger.gui.colorscheme import _colorscheme_name_to_class + settings.signal_bind('setopt.colorscheme', + _colorscheme_name_to_class, priority=1) - import sys if not ranger.arg.clean: # overwrite single default options with custom options try: @@ -65,83 +130,34 @@ class SettingsAware(object): except ImportError: pass else: - for setting in ALLOWED_SETTINGS: + settings._setting_sources.append(my_options) + + # For backward compatibility: + for new, old in COMPAT_MAP.items(): try: - settings[setting] = getattr(my_options, setting) + setattr(my_options, new, getattr(my_options, old)) + print("Warning: the option `{0}'"\ + " was renamed to `{1}'\nPlease update"\ + " your configuration file soon." \ + .format(old, new)) except AttributeError: pass - assert check_option_types(settings) - - # Find the colorscheme. First look for it at ~/.ranger/colorschemes, - # then at RANGERDIR/colorschemes. If the file contains a class - # named Scheme, it is used. Otherwise, an arbitrary other class - # is picked. - - scheme_name = settings.colorscheme - - def exists(colorscheme): - return os.path.exists(colorscheme + '.py') - - def is_scheme(x): - return isclass(x) and issubclass(x, ColorScheme) - - # create ~/.ranger/colorschemes/__init__.py if it doesn't exist - if os.path.exists(ranger.relpath_conf('colorschemes')): - initpy = ranger.relpath_conf('colorschemes', '__init__.py') - if not os.path.exists(initpy): - open(initpy, 'a').close() - - if exists(ranger.relpath_conf('colorschemes', scheme_name)): - scheme_supermodule = 'colorschemes' - elif exists(ranger.relpath('colorschemes', scheme_name)): - scheme_supermodule = 'ranger.colorschemes' - else: - scheme_supermodule = None # found no matching file. - - if scheme_supermodule is None: - print("ERROR: colorscheme not found, fall back to builtin scheme") - if ranger.arg.debug: - raise Exception("Cannot locate colorscheme!") - settings.colorscheme = ColorScheme() - else: - scheme_module = getattr(__import__(scheme_supermodule, - globals(), locals(), [scheme_name], 0), scheme_name) - assert ismodule(scheme_module) - if hasattr(scheme_module, 'Scheme') \ - and is_scheme(scheme_module.Scheme): - settings.colorscheme = scheme_module.Scheme() - else: - for name, var in scheme_module.__dict__.items(): - if var != ColorScheme and is_scheme(var): - settings.colorscheme = var() - break - else: - raise Exception("The module contains no " \ - "valid colorscheme!") + from ranger.defaults import options as default_options + settings._setting_sources.append(default_options) + assert all(hasattr(default_options, setting) \ + for setting in ALLOWED_SETTINGS), \ + "Ensure that all options are defined in the defaults!" try: import apps except ImportError: from ranger.defaults import apps - settings.apps = apps + settings._raw_set('apps', apps) try: import keys except ImportError: from ranger.defaults import keys - settings.keys = keys + settings._raw_set('keys', keys) SettingsAware.settings = settings - -def check_option_types(opt): - import inspect - for name, typ in ALLOWED_SETTINGS.items(): - optvalue = getattr(opt, name) - if inspect.isfunction(typ): - assert typ(optvalue), \ - "The option `" + name + "' has an incorrect type!" - else: - assert isinstance(optvalue, typ), \ - "The option `" + name + "' has an incorrect type!"\ - " Got " + str(type(optvalue)) + ", expected " + str(typ) + "!" - return True |