summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile8
-rw-r--r--doc/ranger.118
-rw-r--r--doc/ranger.pod5
-rw-r--r--doc/rifle.12
-rw-r--r--ranger/__init__.py11
-rwxr-xr-xranger/config/commands.py12
-rw-r--r--ranger/config/rc.conf6
-rw-r--r--ranger/container/bookmarks.py18
-rw-r--r--ranger/container/history.py32
-rw-r--r--ranger/core/actions.py2
-rwxr-xr-xranger/ext/rifle.py8
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/ranger/__init__.py0
-rw-r--r--tests/ranger/container/__init__.py0
-rw-r--r--tests/ranger/container/test_bookmarks.py52
-rw-r--r--tests/ranger/container/test_container.py100
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"