diff options
-rw-r--r-- | README | 2 | ||||
-rw-r--r-- | TODO | 11 | ||||
-rw-r--r-- | doc/ranger.1 | 6 | ||||
-rwxr-xr-x | ranger.py | 2 | ||||
-rw-r--r-- | ranger/__main__.py | 48 | ||||
-rw-r--r-- | ranger/api/apps.py | 4 | ||||
-rw-r--r-- | ranger/api/options.py | 1 | ||||
-rw-r--r-- | ranger/core/actions.py | 5 | ||||
-rw-r--r-- | ranger/defaults/apps.py | 22 | ||||
-rw-r--r-- | ranger/defaults/commands.py | 45 | ||||
-rw-r--r-- | ranger/defaults/options.py | 8 | ||||
-rw-r--r-- | ranger/fsobject/directory.py | 3 | ||||
-rw-r--r-- | ranger/gui/colorscheme.py | 4 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 2 | ||||
-rw-r--r-- | ranger/gui/widgets/pager.py | 4 | ||||
-rw-r--r-- | ranger/help/console.py | 6 | ||||
-rw-r--r-- | test/__init__.py | 2 | ||||
-rw-r--r-- | test/tc_loader.py | 185 |
18 files changed, 316 insertions, 44 deletions
diff --git a/README b/README index 4ce3462e..96b7e53e 100644 --- a/README +++ b/README @@ -147,7 +147,7 @@ Tips Change the directory of your parent shell when you exit ranger: ranger() { - command ranger $@ && + command ranger --fail-if-run $@ && cd "$(grep \^\' ~/.ranger/bookmarks | cut -b3-)" } diff --git a/TODO b/TODO index f7ad1936..169fa6d6 100644 --- a/TODO +++ b/TODO @@ -52,7 +52,9 @@ General (X) #79 10/04/08 tab number zero ( ) #80 10/04/08 when closing tabs, avoid gaps? ( ) #81 10/04/15 system crash when previewing /proc/kcore with root permissions - ( ) #83 10/04/19 better ways to mark files. eg by regexp, filetype,.. + (X) #83 10/04/19 better ways to mark files. eg by regexp, filetype,.. + ( ) #86 10/04/21 narg for move_parent + ( ) #60 10/02/05 utf support improvable Bugs @@ -71,14 +73,14 @@ Bugs (X) #49 10/01/19 fix unit tests :'( (X) #52 10/01/23 special characters in tab completion (X) #54 10/01/23 max_dirsize_for_autopreview not working - ( ) #60 10/02/05 utf support improvable (X) #62 10/02/15 curs_set can raise an exception - ( ) #65 10/02/16 "source ranger ranger some/file.txt" shouldn't cd after exit + (X) #65 10/02/16 "source ranger ranger some/file.txt" shouldn't cd after exit (X) #67 10/03/08 terminal title in tty (X) #69 10/03/11 tab-completion breaks with Apps subclass (X) #73 10/03/21 when clicking on the first column, it goes 1x down (X) #74 10/03/21 console doesn't scroll - ( ) #78 10/03/31 broken preview when deleting all files in a directory + (X) #78 10/03/31 broken preview when deleting all files in a directory + (X) #85 10/04/26 no automatic reload of directory after using :filter Ideas @@ -96,4 +98,5 @@ Ideas (X) #76 10/03/28 save history between sessions (X) #77 10/03/28 colorscheme overlay in options.py ( ) #82 10/04/19 :s command for batch renaming + ( ) #84 10/04/25 use pygments for syntax highlighting diff --git a/doc/ranger.1 b/doc/ranger.1 index 68353dd9..b197d774 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -33,6 +33,10 @@ Activate the clean mode: Ranger will not access or create any configuration files nor will it leave any traces on your system. This is useful when your configuration is broken, when you want to avoid clutter, etc. .TP +--fail-if-run +Return the exit code 1 if ranger is used to run a file, for example with +`ranger --fail-if-run filename`. This can be useful for scripts. +.TP -r \fIdir\fR, --confdir=\fIdir\fR Define a different configuration directory. The default is $HOME/.ranger. .TP @@ -172,7 +176,7 @@ of your parent shell after exiting ranger: .nf ranger() { - command ranger $@ && + command ranger --fail-if-run $@ && cd "$(grep \\^\\' ~/.ranger/bookmarks | cut -b3-)" } .\"----------------------------------------- diff --git a/ranger.py b/ranger.py index 06b86531..754f0a8f 100755 --- a/ranger.py +++ b/ranger.py @@ -23,7 +23,7 @@ # after you exit ranger by starting it with: source ranger ranger """": if [ $1 ]; then - $@ && cd "$(grep \^\' ~/.ranger/bookmarks | cut -b3-)" + $@ --fail-if-run && cd "$(grep \^\' ~/.ranger/bookmarks | cut -b3-)" else echo "usage: source path/to/ranger.py path/to/ranger.py" fi diff --git a/ranger/__main__.py b/ranger/__main__.py index cbb87cc2..8bb0bfa1 100644 --- a/ranger/__main__.py +++ b/ranger/__main__.py @@ -29,6 +29,7 @@ from signal import signal, SIGINT from locale import getdefaultlocale, setlocale, LC_ALL from ranger.ext import curses_interrupt_handler +from ranger.core.runner import Runner from ranger.core.fm import FM from ranger.core.environment import Environment from ranger.shared import (EnvironmentAware, FileManagerAware, @@ -44,6 +45,9 @@ def parse_arguments(): help="activate debug mode") parser.add_option('-c', '--clean', action='store_true', help="don't touch/require any config files. ") + parser.add_option('--fail-if-run', action='store_true', + help="experimental: return the exit code 1 if ranger is" \ + "used to run a file (with `ranger filename`)") parser.add_option('-r', '--confdir', type='string', metavar='dir', default=DEFAULT_CONFDIR, help="the configuration directory. (%default)") @@ -60,19 +64,27 @@ def parse_arguments(): return arg -def load_settings(fm, clean): - if not clean: +def allow_access_to_confdir(confdir, allow): + if allow: try: - os.makedirs(ranger.arg.confdir) + os.makedirs(confdir) except OSError as err: if err.errno != 17: # 17 means it already exists print("This configuration directory could not be created:") - print(ranger.arg.confdir) + print(confdir) print("To run ranger without the need for configuration") print("files, use the --clean option.") raise SystemExit() + if not confdir in sys.path: + sys.path[0:0] = [confdir] + else: + if sys.path[0] == confdir: + del sys.path[0] + - sys.path[0:0] = [ranger.arg.confdir] +def load_settings(fm, clean): + if not clean: + allow_access_to_confdir(ranger.arg.confdir, True) # Load commands comcont = ranger.api.commands.CommandContainer() @@ -107,7 +119,7 @@ def load_settings(fm, clean): print("Warning: the syntax for ~/.ranger/keys.py has changed.") print("Your custom keys are not loaded."\ " Please update your configuration.") - del sys.path[0] + allow_access_to_confdir(ranger.arg.confdir, False) else: comcont = ranger.api.commands.CommandContainer() ranger.api.commands.alias = comcont.alias @@ -119,6 +131,19 @@ def load_settings(fm, clean): fm.apps = apps.CustomApplications() +def load_apps(fm, clean): + if not clean: + allow_access_to_confdir(ranger.arg.confdir, True) + try: + import apps + except ImportError: + from ranger.defaults import apps + allow_access_to_confdir(ranger.arg.confdir, False) + else: + from ranger.defaults import apps + fm.apps = apps.CustomApplications() + + def main(): """initialize objects and run the filemanager""" try: @@ -151,11 +176,12 @@ def main(): print("File or directory doesn't exist: %s" % target) sys.exit(1) elif os.path.isfile(target): - thefile = File(target) - fm = FM() - load_settings(fm, ranger.arg.clean) - fm.execute_file(thefile, mode=arg.mode, flags=arg.flags) - sys.exit(0) + def print_function(string): + print(string) + runner = Runner(logfunc=print_function) + load_apps(runner, ranger.arg.clean) + runner(files=[File(target)], mode=arg.mode, flags=arg.flags) + sys.exit(1 if arg.fail_if_run else 0) else: path = target else: diff --git a/ranger/api/apps.py b/ranger/api/apps.py index 38d638fb..90162e0b 100644 --- a/ranger/api/apps.py +++ b/ranger/api/apps.py @@ -13,10 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -""" -This module provides helper functions/classes for ranger.apps. -""" - import os, sys, re from ranger.api import * from ranger.ext.iter_tools import flatten diff --git a/ranger/api/options.py b/ranger/api/options.py index 2a90f78d..ee947b39 100644 --- a/ranger/api/options.py +++ b/ranger/api/options.py @@ -15,6 +15,5 @@ import re from re import compile as regexp -from ranger import colorschemes as allschemes from ranger.api import * from ranger.gui import color diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 408bbf3a..86d37915 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -141,6 +141,11 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): pagesize=self.ui.browser.hei) cwd.move(to=newpos) + def move_parent(self, n): + self.enter_dir('..') + self.move(down=n) + self.move(right=0) + def history_go(self, relative): """Move back and forth in the history""" self.env.history_go(relative) diff --git a/ranger/defaults/apps.py b/ranger/defaults/apps.py index 7e9fe385..6c09298a 100644 --- a/ranger/defaults/apps.py +++ b/ranger/defaults/apps.py @@ -35,10 +35,11 @@ This example modifies the behaviour of "feh" and adds a custom media player: return tup('feh', '-F', *c) def app_default(self, c): - if c.file.video or c.file.audio: + f = c.file #shortcut + if f.video or f.audio: return self.app_kaffeine(c) - if c.file.image and c.mode == 0: + if f.image and c.mode == 0: return self.app_feh_fullscreen_by_default(c) return DefaultApps.app_default(self, c) @@ -48,16 +49,14 @@ This example modifies the behaviour of "feh" and adds a custom media player: from ranger.api.apps import * from ranger.ext.get_executables import get_executables -INTERPRETED_LANGUAGES = re.compile(r''' - ^(text|application)/x-( - haskell|perl|python|ruby|sh - )$''', re.VERBOSE) - class CustomApplications(Applications): def app_default(self, c): """How to determine the default application?""" f = c.file + if f.basename.lower() == 'makefile': + return self.app_make(c) + if f.extension is not None: if f.extension in ('pdf', ): c.flags += 'd' @@ -65,9 +64,9 @@ class CustomApplications(Applications): if f.extension in ('xml', ): return self.app_editor(c) if f.extension in ('html', 'htm', 'xhtml'): - return self.either(c, 'firefox', 'opera') - if f.extension in ('swf', ): return self.either(c, 'firefox', 'opera', 'elinks') + if f.extension in ('swf', ): + return self.either(c, 'firefox', 'opera') if f.extension == 'nes': return self.app_fceux(c) if f.extension in ('swc', 'smc'): @@ -251,3 +250,8 @@ class CustomApplications(Applications): return tup("totem", *c) if c.mode is 1: return tup("totem", "--fullscreen", *c) + +INTERPRETED_LANGUAGES = re.compile(r''' + ^(text|application)/x-( + haskell|perl|python|ruby|sh + )$''', re.VERBOSE) diff --git a/ranger/defaults/commands.py b/ranger/defaults/commands.py index 8117f1f3..bfe038c5 100644 --- a/ranger/defaults/commands.py +++ b/ranger/defaults/commands.py @@ -50,7 +50,7 @@ and write some command definitions, for example: def execute(self): num = self.line.split()[1] self.fm.tab_open(int(num)) - + For a list of all actions, check /ranger/core/actions.py. ''' @@ -145,8 +145,12 @@ class find(Command): deq = deque(cwd.files) deq.rotate(-cwd.pointer) i = 0 + case_insensitive = arg.lower() == arg for fsobj in deq: - filename = fsobj.basename_lower + if case_insensitive: + filename = fsobj.basename_lower + else: + filename = fsobj.basename if arg in filename: self.count += 1 if self.count == 1: @@ -273,6 +277,40 @@ class delete(Command): # no need for a confirmation, just delete self.fm.delete() + +class mark(Command): + """ + :mark <regexp> + + Mark all files matching a regular expression. + """ + do_mark = True + + def execute(self): + import re + cwd = self.fm.env.cwd + line = parse(self.line) + input = line.rest(1) + searchflags = re.UNICODE + if input.lower() == input: # "smartcase" + searchflags |= re.IGNORECASE + pattern = re.compile(input, searchflags) + for fileobj in cwd.files: + if pattern.search(fileobj.basename): + cwd.mark_item(fileobj, val=self.do_mark) + self.fm.ui.status.need_redraw = True + self.fm.ui.need_redraw = True + + +class unmark(mark): + """ + :unmark <regexp> + + Unmark all files matching a regular expression. + """ + do_mark = False + + class mkdir(Command): """ :mkdir <dirname> @@ -397,7 +435,7 @@ class chmod(Command): try: mode = int(mode, 8) - if mode < 0 or mode > 511: + if mode < 0 or mode > 0o777: raise ValueError except ValueError: self.fm.notify("Need an octal number between 0 and 777!", bad=True) @@ -427,6 +465,7 @@ class filter(Command): def execute(self): line = parse(self.line) self.fm.set_filter(line.rest(1)) + self.fm.reload_cwd() class grep(Command): diff --git a/ranger/defaults/options.py b/ranger/defaults/options.py index 9fb9d728..ac074e1c 100644 --- a/ranger/defaults/options.py +++ b/ranger/defaults/options.py @@ -108,6 +108,10 @@ sort_reverse = False sort_case_insensitive = False sort_directories_first = True +# Enable this if key combinations with the Alt Key don't work for you. +# (Especially on xterm) +xterm_alt_key = False + # 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 @@ -127,7 +131,3 @@ def colorscheme_overlay(context, fg, bg, attr): # The above function was just an example, let's set it back to None colorscheme_overlay = None - -# Enable this if key combinations with the Alt Key don't work for you. -# (Especially on xterm) -xterm_alt_key = False diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index c434ee5c..43af772a 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -84,7 +84,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): self.settings.signal_bind('setopt.' + opt, self.request_resort, weak=True) - for opt in ('filter', 'hidden_filter', 'show_hidden'): + for opt in ('hidden_filter', 'show_hidden'): self.settings.signal_bind('setopt.' + opt, self.request_reload, weak=True) @@ -224,6 +224,7 @@ class Directory(FileSystemObject, Accumulator, SettingsAware): self.content_loaded = True self.determine_infostring() self.last_update_time = time() + self.correct_pointer() finally: self.loading = False diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py index 8c81763f..501b8788 100644 --- a/ranger/gui/colorscheme.py +++ b/ranger/gui/colorscheme.py @@ -47,6 +47,7 @@ from curses import color_pair import ranger from ranger.gui.color import get_color from ranger.gui.context import Context +from ranger.__main__ import allow_access_to_confdir from ranger.shared.settings import SettingsAware # ColorScheme is not SettingsAware but it will gain access @@ -150,6 +151,7 @@ def _colorscheme_name_to_class(signal): scheme_supermodule = 'colorschemes' elif exists(ranger.relpath('colorschemes', scheme_name)): scheme_supermodule = 'ranger.colorschemes' + usecustom = False else: scheme_supermodule = None # found no matching file. @@ -160,8 +162,10 @@ def _colorscheme_name_to_class(signal): raise Exception("Cannot locate colorscheme!") signal.value = ColorScheme() else: + if usecustom: allow_access_to_confdir(ranger.arg.confdir, True) scheme_module = getattr(__import__(scheme_supermodule, globals(), locals(), [scheme_name], 0), scheme_name) + if usecustom: allow_access_to_confdir(ranger.arg.confdir, False) if hasattr(scheme_module, 'Scheme') \ and is_scheme(scheme_module.Scheme): signal.value = scheme_module.Scheme() diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py index b193a523..e4ba4b64 100644 --- a/ranger/gui/widgets/browsercolumn.py +++ b/ranger/gui/widgets/browsercolumn.py @@ -304,7 +304,7 @@ class BrowserColumn(Pager): x = self.wid - 1 - len(info) if info is BAD_INFO: bad_info_color = (x, len(str(info))) - if x > self.x: + if x > 0: self.win.addstr(line, x, str(info) + ' ') except: # the drawing of the last string will cause an error diff --git a/ranger/gui/widgets/pager.py b/ranger/gui/widgets/pager.py index 00f3ec78..c0bc98b4 100644 --- a/ranger/gui/widgets/pager.py +++ b/ranger/gui/widgets/pager.py @@ -116,7 +116,7 @@ class Pager(Widget): maximum=self._get_max_width(), current=self.startx, pagesize=self.wid, - offset=-self.wid) + offset=-self.wid + 1) if direction.vertical(): if self.source_is_stream: self._get_line(self.scroll_begin + self.hei * 2) @@ -126,7 +126,7 @@ class Pager(Widget): maximum=len(self.lines), current=self.scroll_begin, pagesize=self.hei, - offset=-self.hei) + offset=-self.hei + 1) def press(self, key): self.env.keymanager.use_context(self.embedded and 'embedded_pager' or 'pager') diff --git a/ranger/help/console.py b/ranger/help/console.py index c62d0244..04372f38 100644 --- a/ranger/help/console.py +++ b/ranger/help/console.py @@ -94,6 +94,12 @@ it conflicts with ":cd". Looks for a string in all marked files or directory. (equivalent to "!grep [some options] -e <string> -r %s | less") +:mark <regexp> + Mark all files matching a regular expression. + +:unmark <regexp> + Unmark all files matching a regular expression. + :mkdir <dirname> Creates a directory with the name <dirname> diff --git a/test/__init__.py b/test/__init__.py index 29796ac1..c2f9bde2 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -28,7 +28,7 @@ class Fake(object): self.__dict__[attrname] = val return val - def __call__(self, *_): + def __call__(self, *_, **__): return Fake() def __clear__(self): diff --git a/test/tc_loader.py b/test/tc_loader.py new file mode 100644 index 00000000..22f866ec --- /dev/null +++ b/test/tc_loader.py @@ -0,0 +1,185 @@ +# 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/>. + +if __name__ == '__main__': from __init__ import init; init() + +import unittest +import os +from os.path import realpath, join, dirname +from time import time + +from test import Fake +from ranger.shared import FileManagerAware, SettingsAware +from ranger.core.loader import Loader +from ranger.fsobject import Directory, File +from ranger.ext.openstruct import OpenStruct + +TESTDIR = realpath(join(dirname(__file__), 'testdir')) +#TESTDIR = "/usr/sbin" + +def raw_load_content(self): + """ + The method which is used in a Directory object to load stuff. + Keep this up to date! + """ + + from os.path import join, isdir, basename + from os import listdir + import ranger.ext.mount_path + + self.loading = True + self.load_if_outdated() + + try: + if self.exists and self.runnable: + self.mount_path = ranger.ext.mount_path.mount_path(self.path) + + filenames = [] + for fname in listdir(self.path): + if not self.settings.show_hidden: + hfilter = self.settings.hidden_filter + if hfilter: + if isinstance(hfilter, str) and hfilter in fname: + continue + if hasattr(hfilter, 'search') and \ + hfilter.search(fname): + continue + if isinstance(self.filter, str) and self.filter \ + and self.filter not in fname: + continue + filenames.append(join(self.path, fname)) + + self.load_content_mtime = os.stat(self.path).st_mtime + + marked_paths = [obj.path for obj in self.marked_items] + + files = [] + for name in filenames: + if isdir(name): + try: + item = self.fm.env.get_directory(name) + except: + item = Directory(name) + else: + item = File(name) + item.load_if_outdated() + files.append(item) + + self.disk_usage = sum(f.size for f in files if f.is_file) + + self.scroll_offset = 0 + self.filenames = filenames + self.files = files + + self._clear_marked_items() + for item in self.files: + if item.path in marked_paths: + self.mark_item(item, True) + else: + self.mark_item(item, False) + + self.sort() + + if len(self.files) > 0: + if self.pointed_obj is not None: + self.sync_index() + else: + self.move(to=0) + else: + self.filenames = None + self.files = None + + self.cycle_list = None + self.content_loaded = True + self.determine_infostring() + self.last_update_time = time() + self.correct_pointer() + + finally: + self.loading = False + + +def benchmark_load(n): + loader = Loader() + fm = OpenStruct(loader=loader) + SettingsAware.settings = Fake() + FileManagerAware.fm = fm + dir = Directory(TESTDIR) + + t1 = time() + for _ in range(n): + dir.load_content(schedule=True) + while loader.has_work(): + loader.work() + t2 = time() + return t2 - t1 + + +def benchmark_raw_load(n): + SettingsAware.settings = Fake() + dir = Directory(TESTDIR) + generator = dir.load_bit_by_bit() + t1 = time() + for _ in range(n): + raw_load_content(dir) + t2 = time() + return t2 - t1 + + +class Test1(unittest.TestCase): + def test_loader(self): + loader = Loader() + fm = OpenStruct(loader=loader) + SettingsAware.settings = Fake() + FileManagerAware.fm = fm + + # initially, the loader has nothing to do + self.assertFalse(loader.has_work()) + + dir = Directory(TESTDIR) + self.assertEqual(None, dir.files) + self.assertFalse(loader.has_work()) + + # Calling load_content() will enqueue the loading operation. + # dir is not loaded yet, but the loader has work + dir.load_content(schedule=True) + self.assertEqual(None, dir.files) + self.assertTrue(loader.has_work()) + + iterations = 0 + while loader.has_work(): + iterations += 1 + loader.work() + #print(iterations) + self.assertNotEqual(None, dir.files) + self.assertFalse(loader.has_work()) + + def test_get_overhead_of_loader(self): + N = 5 + tloader = benchmark_load(N) + traw = benchmark_raw_load(N) + #traw1k = 250.0 + #traw = traw1k * N / 1000.0 + #print("Loader: {0}s".format(tloader)) + #print("Raw: {0}s".format(traw)) + self.assertTrue(tloader > traw) + overhead = tloader * 100 / traw - 100 + self.assertTrue(overhead < 2, "overhead of loader too high: {0}" \ + .format(overhead)) + #print("Overhead: {0:.5}%".format(overhead)) + + +if __name__ == '__main__': + unittest.main() |