diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | doc/ranger.1 | 18 | ||||
-rw-r--r-- | doc/ranger.pod | 5 | ||||
-rw-r--r-- | doc/rifle.1 | 2 | ||||
-rw-r--r-- | ranger/__init__.py | 11 | ||||
-rwxr-xr-x | ranger/config/commands.py | 12 | ||||
-rw-r--r-- | ranger/config/rc.conf | 6 | ||||
-rw-r--r-- | ranger/container/bookmarks.py | 18 | ||||
-rw-r--r-- | ranger/container/history.py | 32 | ||||
-rw-r--r-- | ranger/core/actions.py | 2 | ||||
-rwxr-xr-x | ranger/ext/rifle.py | 8 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/ranger/__init__.py | 0 | ||||
-rw-r--r-- | tests/ranger/container/__init__.py | 0 | ||||
-rw-r--r-- | tests/ranger/container/test_bookmarks.py | 52 | ||||
-rw-r--r-- | tests/ranger/container/test_container.py | 100 |
17 files changed, 239 insertions, 36 deletions
diff --git a/.gitignore b/.gitignore index 5357ead2..81e1b8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ stuff/* doc/ranger.1.html build +pytestdebug.log diff --git a/Makefile b/Makefile index f6208015..5de061ff 100644 --- a/Makefile +++ b/Makefile @@ -61,10 +61,18 @@ doc: cleandoc find . -name \*.html -exec sed -i 's|'"$(CWD)"'|../..|g' -- {} \; test: + @echo "Running doctests..." @for FILE in $(shell grep -IHm 1 doctest -r ranger | grep $(FILTER) | cut -d: -f1); do \ echo "Testing $$FILE..."; \ RANGER_DOCTEST=1 PYTHONPATH=".:"$$PYTHONPATH ${PYTHON} $$FILE; \ done + @if type -t py.test > /dev/null; then \ + echo "Running py.test tests..."; \ + py.test tests; \ + else \ + echo "WARNING: Couldn't run some tests because py.test is not installed!"; \ + fi + @echo "Finished testing." man: pod2man --stderr --center='ranger manual' --date='$(NAME)-$(VERSION)' \ diff --git a/doc/ranger.1 b/doc/ranger.1 index e810e7c6..ee487e1e 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.7.2" "01/01/2016" "ranger manual" +.TH RANGER 1 "ranger-1.7.2" "02/24/2016" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -299,16 +299,17 @@ bottom right indicates that there are marked files in this directory. .IX Subsection "MACROS" Macros can be used in commands to abbreviate things. .PP -.Vb 5 +.Vb 6 \& %f the highlighted file \& %d the path of the current directory -\& %s the selected files in the current directory. +\& %s the selected files in the current directory \& %t all tagged files in the current directory \& %c the full paths of the currently copied/cut files +\& %p the full paths of selected files .Ve .PP -The macros \f(CW%f\fR, \f(CW%d\fR and \f(CW%s\fR also have upper case variants, \f(CW%F\fR, \f(CW%D\fR and \f(CW%S\fR, -which refer to the next tab. To refer to specific tabs, add a number in +The macros \f(CW%f\fR, \f(CW%d\fR, \f(CW%p\fR, and \f(CW%s\fR also have upper case variants, \f(CW%F\fR, \f(CW%D\fR, \f(CW%P\fR, and +\&\f(CW%S\fR, which refer to the next tab. To refer to specific tabs, add a number in between. (%7s = selection of the seventh tab.) .PP \&\f(CW%c\fR is the only macro which ranges out of the current directory. So you may @@ -1180,6 +1181,13 @@ doesn't work for functions and regular expressions. Valid values are: Assigns a new value to an option, but locally for the directories that are marked with \fItag\fR. This means, that this option only takes effect when visiting that directory. +.Sp +For example, to change the sorting order in your downloads directory, tag it +with the \fIv\fR tag by typing \fI"v\fR, then use this command: +.Sp +.Vb 1 +\& setintag v sort ctime +.Ve .IP "setlocal [path=\fIpath\fR] \fIoption\fR \fIvalue\fR" 2 .IX Item "setlocal [path=path] option value" Assigns a new value to an option, but locally for the directory given by diff --git a/doc/ranger.pod b/doc/ranger.pod index 60deedf3..91ba904a 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -1237,6 +1237,11 @@ Assigns a new value to an option, but locally for the directories that are marked with I<tag>. This means, that this option only takes effect when visiting that directory. +For example, to change the sorting order in your downloads directory, tag it +with the I<v> tag by typing I<"v>, then use this command: + + setintag v sort ctime + =item setlocal [path=I<path>] I<option> I<value> Assigns a new value to an option, but locally for the directory given by diff --git a/doc/rifle.1 b/doc/rifle.1 index 32501ee0..0b67edcc 100644 --- a/doc/rifle.1 +++ b/doc/rifle.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RIFLE 1" -.TH RIFLE 1 "rifle-1.7.2" "10/04/2015" "rifle manual" +.TH RIFLE 1 "rifle-1.7.2" "02/24/2016" "rifle manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/ranger/__init__.py b/ranger/__init__.py index 20c8ab35..b562ebf5 100644 --- a/ranger/__init__.py +++ b/ranger/__init__.py @@ -25,11 +25,20 @@ TIME_BEFORE_FILE_BECOMES_GARBAGE = 1200 MAX_RESTORABLE_TABS = 3 MACRO_DELIMITER = '%' DEFAULT_PAGER = 'less' -LOGFILE = tempfile.gettempdir()+'/ranger_errorlog' CACHEDIR = os.path.expanduser("~/.cache/ranger") USAGE = '%prog [options] [path]' VERSION = 'ranger-master %s\n\nPython %s' % (__version__, sys.version) +try: + ExceptionClass = FileNotFoundError +except NameError: + ExceptionClass = IOError +try: + LOGFILE = tempfile.gettempdir()+'/ranger_errorlog' +except ExceptionClass: + LOGFILE = '/dev/null' +del ExceptionClass + # If the environment variable XDG_CONFIG_HOME is non-empty, CONFDIR is ignored # and the configuration directory will be $XDG_CONFIG_HOME/ranger instead. CONFDIR = '~/.config/ranger' diff --git a/ranger/config/commands.py b/ranger/config/commands.py index eb4ee4c3..f734d64a 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -209,11 +209,7 @@ class shell(Command): flags = '' command = self.rest(1) - if not command and 'p' in flags: - command = 'cat %f' if command: - if '%' in command: - command = self.fm.substitute_macros(command, escape=True) self.fm.execute_command(command, flags=flags) def tab(self, tabnum): @@ -771,6 +767,14 @@ class rename(Command): if self.fm.rename(self.fm.thisfile, new_name): f = File(new_name) + # Update bookmarks that were pointing on the previous name + obsoletebookmarks = [b for b in self.fm.bookmarks + if b[1].path == self.fm.thisfile] + if obsoletebookmarks: + for key, _ in obsoletebookmarks: + self.fm.bookmarks[key] = f + self.fm.bookmarks.update_if_outdated() + self.fm.thisdir.pointed_obj = f self.fm.thisfile = f for t in tagged: diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf index d719da20..3b8ee333 100644 --- a/ranger/config/rc.conf +++ b/ranger/config/rc.conf @@ -330,9 +330,9 @@ map g? cd /usr/share/doc/ranger map E edit map du shell -p du --max-depth=1 -h --apparent-size map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh -map yp shell -f echo -n %%d/%%f | xsel -i; xsel -o | xsel -i -b -map yd shell -f echo -n %%d | xsel -i; xsel -o | xsel -i -b -map yn shell -f echo -n %%f | xsel -i; xsel -o | xsel -i -b +map yp shell -f echo -n %d/%f | xsel -i; xsel -o | xsel -i -b +map yd shell -f echo -n %d | xsel -i; xsel -o | xsel -i -b +map yn shell -f echo -n %f | xsel -i; xsel -o | xsel -i -b # Filesystem Operations map = chmod diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py index cbb95864..02f3e3bc 100644 --- a/ranger/container/bookmarks.py +++ b/ranger/container/bookmarks.py @@ -30,6 +30,7 @@ class Bookmarks(object): """ self.autosave = autosave self.dct = {} + self.original_dict = {} self.path = bookmarkfile self.bookmarktype = bookmarktype @@ -42,14 +43,6 @@ class Bookmarks(object): self._set_dict(new_dict, original=new_dict) - def delete(self, key): - """Delete the bookmark with the given key""" - if key == '`': - key = "'" - if key in self.dct: - del self.dct[key] - if self.autosave: self.save() - def enter(self, key): """Enter the bookmark with the given key. @@ -70,6 +63,15 @@ class Bookmarks(object): self["'"] = value if self.autosave: self.save() + def __delitem__(self, key): + """Delete the bookmark with the given key""" + if key == '`': + key = "'" + if key in self.dct: + del self.dct[key] + if self.autosave: self.save() + + def __iter__(self): return iter(self.dct.items()) diff --git a/ranger/container/history.py b/ranger/container/history.py index 8300edae..69c3da57 100644 --- a/ranger/container/history.py +++ b/ranger/container/history.py @@ -8,6 +8,7 @@ class HistoryEmptyException(Exception): class History(object): def __init__(self, maxlen=None, unique=True): + assert maxlen is not None, "maxlen cannot be None" if isinstance(maxlen, History): self._history = list(maxlen._history) self._index = maxlen._index @@ -52,12 +53,31 @@ class History(object): self.add(item) def rebase(self, other_history): + """ + Replace the past of this history by that of another. + + This is used when creating a new tab to seamlessly blend in the history + of the old tab into the new one. + + Example: if self is [a,b,C], the current item is uppercase, and + other_history is [x,Y,z], then self.merge(other_history) will result in + [x, y, C]. + """ assert isinstance(other_history, History) - index_offset = len(self._history) - self._index - self._history[:self._index] = list(other_history._history) + + if len(self._history) == 0: + self._index = 0 + future_length = 0 + else: + future_length = len(self._history) - self._index - 1 + + self._history[:self._index] = list( + other_history._history[:other_history._index + 1]) if len(self._history) > self.maxlen: self._history = self._history[-self.maxlen:] - self._index = len(self._history) - index_offset + + self._index = len(self._history) - future_length - 1 + assert self._index < len(self._history) def __len__(self): return len(self._history) @@ -112,9 +132,6 @@ class History(object): def __iter__(self): return self._history.__iter__() - def next(self): - return self._history.next() - def forward(self): if self._history: self._index += 1 @@ -129,6 +146,3 @@ class History(object): self._index = len(self._history) - 1 else: self._index = 0 - - def _left(self): # used for unit test - return self._history[0:self._index+1] diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 63916145..e692eb39 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -790,7 +790,7 @@ class Actions(FileManagerAware, SettingsAware): def unset_bookmark(self, key): """Delete the bookmark with the name <key>""" self.bookmarks.update_if_outdated() - self.bookmarks.delete(str(key)) + del self.bookmarks[str(key)] def draw_bookmarks(self): self.ui.browser.draw_bookmarks = True diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py index c43de24f..a8d8bee7 100755 --- a/ranger/ext/rifle.py +++ b/ranger/ext/rifle.py @@ -154,11 +154,12 @@ class Rifle(object): config_file = self.config_file f = open(config_file, 'r') self.rules = [] - lineno = 1 + lineno = 0 for line in f: - if line.startswith('#') or line == '\n': - continue + lineno += 1 line = line.strip() + if line.startswith('#') or line == '': + continue try: if self.delimiter1 not in line: raise Exception("Line without delimiter") @@ -170,7 +171,6 @@ class Rifle(object): except Exception as e: self.hook_logger("Syntax error in %s line %d (%s)" % \ (config_file, lineno, str(e))) - lineno += 1 f.close() def _eval_condition(self, condition, files, label): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/__init__.py diff --git a/tests/ranger/__init__.py b/tests/ranger/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/ranger/__init__.py diff --git a/tests/ranger/container/__init__.py b/tests/ranger/container/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/ranger/container/__init__.py diff --git a/tests/ranger/container/test_bookmarks.py b/tests/ranger/container/test_bookmarks.py new file mode 100644 index 00000000..46c615c6 --- /dev/null +++ b/tests/ranger/container/test_bookmarks.py @@ -0,0 +1,52 @@ +import os +import time +import pytest + +from ranger.container.bookmarks import Bookmarks + +def testbookmarks(tmpdir): + # Bookmarks point to directory location and allow fast access to + # 'favorite' directories. They are persisted to a bookmark file, plain text. + bookmarkfile = tmpdir.join("bookmarkfile") + bmstore = Bookmarks(str(bookmarkfile)) + + # loading an empty bookmark file doesnot crash + bmstore.load() + + # One can add / remove and check existing of bookmark + bmstore["h"] = "world" + assert "h" in bmstore + del bmstore["h"] + + # Only one letter/digit bookmarks are valid, adding something else fails + # silently + bmstore["hello"] = "world" + assert "hello" not in bmstore + + # The default bookmark is ', remember allows to set it + bmstore.remember("the milk") + assert bmstore["'"] == "the milk" + + # We can persist bookmarks to disk and restore them from disk + bmstore.save() + secondstore = Bookmarks(str(bookmarkfile)) + secondstore.load() + assert "'" in secondstore + assert secondstore["'"] == "the milk" + + # We don't uneccesary update when the file on disk does not change + origupdate = secondstore.update + class OutOfDateException(Exception): + pass + def crash(): + raise OutOfDateException("Don't access me") + secondstore.update = crash + secondstore.update_if_outdated() + + # If the modification time change, we try to read the file + newtime = time.time() - 5 + os.utime(str(bookmarkfile), (newtime, newtime)) + with pytest.raises(OutOfDateException): + secondstore.update_if_outdated() + secondstore.update = origupdate + secondstore.update_if_outdated() diff --git a/tests/ranger/container/test_container.py b/tests/ranger/container/test_container.py new file mode 100644 index 00000000..a24c3f8d --- /dev/null +++ b/tests/ranger/container/test_container.py @@ -0,0 +1,100 @@ +from ranger.container import history + + +HISTORY_TEST_ENTRIES = [str(k) for k in range(20)] +OTHER_TEST_ENTRIES = [str(k) for k in range(40,45)] + +def testhistorybasic(): + # A history is a buffer of limited size that stores the last `maxlen` + # item added to it. It has a `current` index that serves as a cursor. + + # A history has a limited size, check that only `maxlen` items are stored + h = history.History(maxlen=10) + for entry in HISTORY_TEST_ENTRIES: + h.add(entry) + + # 10 items are stored + assert len(h) == 10 + assert h.current() == "19" + assert h.top() == "19" + assert h.bottom() == "10" + + # going back in time affects only changes current item + h.back() + assert len(h) == 10 + assert h.current() == "18" + assert h.top() == "19" + assert h.bottom() == "10" + + # __iter__ is actually an interator and we can iterate through the list + it = iter(h) + assert iter(it) == it + assert list(it) == HISTORY_TEST_ENTRIES[10:] + + # search allows to go back in time as long as a pattern matches and we don't + # go over a step limit + assert h.search("45", -9) == "18" + assert h.search("1", -5) == "13" + + # fast forward selects the last item + h.fast_forward() + assert h.current() == "19" + + # back followed by forward is a noop + h.back() + h.forward() + assert h.current() == "19" + + # move can be expressed as multiple calls to back and forward + h.move(-3) + h.forward() + h.forward() + h.forward() + assert h.current() == "19" + + # back, forward, move play well with boundaries + for _ in range(30): + h.back() + + for _ in range(30): + h.forward() + + for _ in range(30): + h.move(-2) + + for _ in range(30): + h.move(2) + assert h.current() == "19" + + # we can create an history from another history + h = history.History(maxlen=10) + for entry in HISTORY_TEST_ENTRIES: + h.add(entry) + # XXX maxlen should not be used to refer to something that isn't a length + otherh = history.History(maxlen=h) + assert(list(h) == list(otherh)) + + # Rebase replaces the past of the history with that of another + otherh = history.History(maxlen=h) + old_current_item = h.current() + for entry in OTHER_TEST_ENTRIES: + otherh.add(entry) + assert list(otherh)[-3:] == ["42", "43", "44"] + h.rebase(otherh) + assert h.current() == old_current_item + assert list(h)[-3:] == ['43', '44', old_current_item] + + # modify, modifies the top of the stack + h.modify("23") + assert h.current() == "23" + + +def testhistoryunique(): + # Check that unique history refuses to store duplicated entries + h = history.History(maxlen=10, unique=True) + for entry in HISTORY_TEST_ENTRIES: + h.add(entry) + assert h.current() == "19" + h.add("17") + assert list(h).count("17") == 1 + assert h.current() == "17" |