summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--ranger/colorschemes/default.py35
-rw-r--r--ranger/gui/context.py6
-rw-r--r--ranger/gui/widgets/__init__.py17
-rw-r--r--ranger/gui/widgets/browsercolumn.py202
4 files changed, 180 insertions, 80 deletions
diff --git a/ranger/colorschemes/default.py b/ranger/colorschemes/default.py
index 58c79962..6f6040ba 100644
--- a/ranger/colorschemes/default.py
+++ b/ranger/colorschemes/default.py
@@ -94,6 +94,13 @@ class Default(ColorScheme):
                     fg = red
             if context.loaded:
                 bg = self.progress_bar_color
+            if context.vcsinfo:
+                fg = blue
+                attr &= ~bold
+            if context.vcscommit:
+                fg = yellow
+                attr &= ~bold
+
 
         if context.text:
             if context.highlight:
@@ -112,4 +119,32 @@ class Default(ColorScheme):
                 else:
                     bg = self.progress_bar_color
 
+
+        if context.vcsfile and not context.selected:
+            attr &= ~bold
+            if context.vcsconflict:
+                fg = magenta
+            elif context.vcschanged:
+                fg = red
+            elif context.vcsunknown:
+                fg = red
+            elif context.vcsstaged:
+                fg = green
+            elif context.vcssync:
+                fg = green
+            elif context.vcsignored:
+                fg = default
+
+        elif context.vcsremote and not context.selected:
+            attr &= ~bold
+            if context.vcssync:
+                fg = green
+            elif context.vcsbehind:
+                fg = red
+            elif context.vcsahead:
+                fg = blue
+            elif context.vcsdiverged:
+                fg = magenta
+
+
         return fg, bg, attr
diff --git a/ranger/gui/context.py b/ranger/gui/context.py
index c9e8104e..2ea23571 100644
--- a/ranger/gui/context.py
+++ b/ranger/gui/context.py
@@ -15,7 +15,11 @@ CONTEXT_KEYS = ['reset', 'error', 'badinfo',
         'help_markup', # COMPAT
         'seperator', 'key', 'special', 'border', # COMPAT
         'title', 'text', 'highlight', 'bars', 'quotes', 'tab', 'loaded',
-        'keybuffer']
+        'keybuffer',
+                'infostring',
+                'vcsfile', 'vcsremote', 'vcsinfo', 'vcscommit',
+                'vcsconflict', 'vcschanged', 'vcsunknown', 'vcsignored',
+                'vcsstaged', 'vcssync', 'vcsbehind', 'vcsahead', 'vcsdiverged']
 
 class Context(object):
     def __init__(self, keys):
diff --git a/ranger/gui/widgets/__init__.py b/ranger/gui/widgets/__init__.py
index 2a930b6c..3d0899bb 100644
--- a/ranger/gui/widgets/__init__.py
+++ b/ranger/gui/widgets/__init__.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 from ranger.gui.displayable import Displayable
 
 class Widget(Displayable):
@@ -5,3 +7,18 @@ class Widget(Displayable):
     The Widget class defines no methods and only exists for
     classification of widgets.
     """
+    vcsfilestatus_symb = {'conflict':  ('X', ["vcsconflict"]),
+                  'untracked': ('+', ["vcschanged"]),
+                  'deleted':   ('-', ["vcschanged"]),
+                  'changed':   ('*', ["vcschanged"]),
+                  'staged':    ('*', ["vcsstaged"]),
+                  'ignored':   ('·', ["vcsignored"]),
+                  'sync':      ('√', ["vcssync"]),
+                  'none':      (' ', []),
+                  'unknown':   ('?', ["vcsunknown"])}
+
+    vcsremotestatus_symb = {'none':     (' ',  []),
+                'sync':     ('=', ["vcssync"]),
+                'behind':   ('<', ["vcsbehind"]),
+                'ahead':    ('>', ["vcsahead"]),
+                'diverged': ('Y', ["vcsdiverged"])}
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index a6653070..38a7fdcb 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf8 -*-
+# -*- coding: utf-8 -*-
 # Copyright (C) 2009, 2010, 2011  Roman Zimbelmann <romanz@lavabit.com>
 # This software is distributed under the terms of the GNU GPL version 3.
 
@@ -12,9 +12,12 @@ from .pager import Pager
 from ranger.fsobject import BAD_INFO
 from ranger.ext.widestring import WideString
 
+from ranger.gui.color import *
+
 class BrowserColumn(Pager):
     main_column = False
     display_infostring = False
+    display_vcsstate   = True
     scroll_begin = 0
     target = None
     last_redraw_time = -1
@@ -91,7 +94,7 @@ class BrowserColumn(Pager):
         """
         Executes a list of "commands" which can be easily cached.
 
-        "commands" is a list of lists.  Each element contains
+        "commands" is a list of lists.    Each element contains
         a text and an attribute.  First, the attribute will be
         set with attrset, then the text is printed.
 
@@ -136,7 +139,7 @@ class BrowserColumn(Pager):
             self.need_redraw = True
             self.old_dir = self.target
 
-        if self.target:  # don't garbage collect this directory please
+        if self.target:     # don't garbage collect this directory please
             self.target.use()
 
         if self.target and self.target.is_directory \
@@ -224,7 +227,6 @@ class BrowserColumn(Pager):
         self._set_scroll_begin()
 
         copied = [f.path for f in self.fm.copy_buffer]
-        ellipsis = self.ellipsis[self.settings.unicode_ellipsis]
 
         selected_i = self.target.pointer
         for line in range(self.hei):
@@ -245,100 +247,142 @@ class BrowserColumn(Pager):
 
             key = (self.wid, selected_i == i, drawn.marked, self.main_column,
                     drawn.path in copied, tagged_marker, drawn.infostring,
-                    self.fm.do_cut)
+                    drawn.vcsfilestatus, drawn.vcsremotestatus, self.fm.do_cut)
 
             if key in drawn.display_data:
                 self.execute_curses_batch(line, drawn.display_data[key])
                 self.color_reset()
                 continue
 
+            text = drawn.basename
+            if drawn.marked and (self.main_column or self.settings.display_tags_in_all_columns):
+                text = " " + text
+
+            # Computing predisplay data. predisplay contains a list of lists [string, colorlst]
+            # where string is a piece of string to display, and colorlst a list of contexts
+            # that we later pass to the colorscheme, to compute the curses attribute.
+            predisplay_left = []
+            predisplay_right = []
+
+            predisplay_left     = predisplay_left + self._draw_tagged_display(tagged, tagged_marker)
+            predisplay_right = predisplay_right + self._draw_vcsstring_display(drawn)
+            space = self.wid - self._total_len(predisplay_left) - self._total_len(predisplay_right)
+
+            # If not enough space
+            if space <= 2:
+                predisplay_right = []
+                predisplay_left     = []
+
+            predisplay_left = predisplay_left + self._draw_text_display(text, space)
+            space = self.wid - self._total_len(predisplay_left)  - self._total_len(predisplay_right)
+
+            predisplay_right = self._draw_infostring_display(drawn, space) + predisplay_right
+            space = self.wid - self._total_len(predisplay_left)  - self._total_len(predisplay_right)
+
+            if space > 0:
+                predisplay_left.append([' ' * space, []])
+            elif space < 0:
+                raise Exception("Error: there is not enough space to write the text. I have computed spaces wrong.")
+
+            # Computing display data. Now we compute the display_data list ready to display in
+            # curses. It is a list of lists [string, attr]
+
+            this_color = base_color + list(drawn.mimetype_tuple) + self._draw_directory_color(i, drawn, copied)
             display_data = []
             drawn.display_data[key] = display_data
 
-            if self.display_infostring and drawn.infostring \
-                    and self.settings.display_size_in_main_column:
-                infostring = str(drawn.infostring) + " "
-            else:
-                infostring = ""
+            predisplay = predisplay_left + predisplay_right
+            for txt, color in predisplay:
+                attr = self.settings.colorscheme.get_attr(*(this_color + color))
+                display_data.append([txt, attr])
 
-            this_color = base_color + list(drawn.mimetype_tuple)
-            text = drawn.basename
+            self.execute_curses_batch(line, display_data)
+            self.color_reset()
 
-            space = self.wid - len(infostring)
-            if self.main_column:
-                space -= 2
-            elif self.settings.display_tags_in_all_columns:
-                space -= 1
+    def _total_len(self, predisplay):
+        return sum([len(WideString(s)) for s, L in predisplay])
 
-            if i == selected_i:
-                this_color.append('selected')
+    def _draw_text_display(self, text, space):
+        wtext = WideString(text)
+        if len(wtext) > space:
+            wtext = wtext[:max(0, space - 1)] + self.ellipsis[self.settings.unicode_ellipsis]
 
-            if drawn.marked:
-                this_color.append('marked')
-                if self.main_column or self.settings.display_tags_in_all_columns:
-                    text = " " + text
+        return [[str(wtext), []]]
 
+    def _draw_tagged_display(self, tagged, tagged_marker):
+        tagged_display = []
+        if (self.main_column or self.settings.display_tags_in_all_columns) and self.wid > 2:
             if tagged:
-                this_color.append('tagged')
-
-            if drawn.is_directory:
-                this_color.append('directory')
+                tagged_display.append([tagged_marker, ['tag_marker']])
             else:
-                this_color.append('file')
-
-            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 drawn.is_device:
-                    this_color.append('device')
-
-            if drawn.path in copied:
-                this_color.append('cut' if self.fm.do_cut else 'copied')
-
-            if drawn.is_link:
-                this_color.append('link')
-                this_color.append(drawn.exists and 'good' or 'bad')
-
-            attr = self.settings.colorscheme.get_attr(*this_color)
-
-            if (self.main_column or self.settings.display_tags_in_all_columns) \
-                    and tagged and self.wid > 2:
-                this_color.append('tag_marker')
-                tag_attr = self.settings.colorscheme.get_attr(*this_color)
-                display_data.append([tagged_marker, tag_attr])
+                tagged_display.append([" ", ['tag_marker']])
+        return tagged_display
+
+    def _draw_infostring_display(self, drawn, space):
+        infostring_display = []
+        if self.display_infostring and drawn.infostring \
+                and self.settings.display_size_in_main_column:
+            infostring = str(drawn.infostring) + " "
+            if len(infostring) <= space:
+                infostring_display.append([infostring, ['infostring']])
+        return infostring_display
+
+    def _draw_vcsstring_display(self, drawn):
+        vcsstring_display = []
+        if self.settings.vcs_aware and (drawn.vcsfilestatus or drawn.vcsremotestatus):
+            if drawn.vcsfilestatus:
+                vcsstr, vcscol = self.vcsfilestatus_symb[drawn.vcsfilestatus]
             else:
-                text = " " + text
-                space += 1
-
-            wtext = WideString(text)
-            if len(wtext) > space:
-                wtext = wtext[:max(0, space - 1)] + ellipsis
-            text = str(wtext)
-
-            display_data.append([text, attr])
-
-            padding = self.wid - len(wtext)
-            if tagged and (self.main_column or \
-                    self.settings.display_tags_in_all_columns):
-                padding -= 1
-            if infostring:
-                if len(wtext) + 1 + len(infostring) > self.wid:
-                    pass
-                else:
-                    padding -= len(infostring)
-                    padding = max(0, padding)
-                    infostring = (" " * padding) + infostring
-                    display_data.append([infostring, attr])
+                vcsstr = " "
+                vcscol = []
+            vcsstring_display.append([vcsstr, ['vcsfile'] + vcscol])
+
+            if drawn.vcsremotestatus:
+                vcsstr, vcscol = self.vcsremotestatus_symb[drawn.vcsremotestatus]
             else:
-                display_data.append([" " * max(0, padding), attr])
 
-            self.execute_curses_batch(line, display_data)
-            self.color_reset()
+                vcsstr = " "
+                vcscol = []
+            vcsstring_display.append([vcsstr, ['vcsremote'] + vcscol])
+        elif self.target.has_vcschild:
+            vcsstring_display.append(["  ", []])
+        return vcsstring_display
+
+    def _draw_directory_color(self, i, drawn, copied):
+        this_color = []
+        if i == self.target.pointer:
+            this_color.append('selected')
+
+        if drawn.marked:
+            this_color.append('marked')
+
+        if self.fm.tags and drawn.realpath in self.fm.tags:
+            this_color.append('tagged')
+
+        if drawn.is_directory:
+            this_color.append('directory')
+        else:
+            this_color.append('file')
+
+        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 drawn.is_device:
+                this_color.append('device')
+
+        if drawn.path in copied:
+            this_color.append('cut' if self.fm.do_cut else 'copied')
+
+        if drawn.is_link:
+            this_color.append('link')
+            this_color.append(drawn.exists and 'good' or 'bad')
+
+        return this_color
 
     def _get_scroll_begin(self):
         """Determines scroll_begin (the position of the first displayed file)"""
an>.tree_pointer = self.tree_pointer[key] except TypeError: print(self.tree_pointer) self.failure = True return None except KeyError: if DIRKEY in self.tree_pointer: self.eval_command = False self.eval_quantifier = True self.tree_pointer = self.tree_pointer[DIRKEY] self.dir_tree_pointer = self.direction_keys._tree elif ANYKEY in self.tree_pointer: self.moo.append(key) self.tree_pointer = self.tree_pointer[ANYKEY] self._try_to_finish() else: self.failure = True return None else: self._try_to_finish() if self.eval_quantifier and self._do_eval_quantifier(key): return # evaluate direction keys {j,k,gg,pagedown,...} if not self.eval_command: try: self.dir_tree_pointer = self.dir_tree_pointer[key] except KeyError: self.failure = True else: if not isinstance(self.dir_tree_pointer, dict): match = self.dir_tree_pointer direction = match.actions['dir'] * self.direction_quant self.directions.append(direction) self.direction_quant = None self._try_to_finish() def _do_eval_quantifier(self, key): if self.eval_command: tree = self.tree_pointer else: tree = self.dir_tree_pointer if is_ascii_digit(key) and ANYKEY not in tree: attr = self.eval_command and 'quant' or 'direction_quant' if getattr(self, attr) is None: setattr(self, attr, 0) setattr(self, attr, getattr(self, attr) * 10 + key - 48) else: self.eval_quantifier = False return False return True def _try_to_finish(self): if not isinstance(self.tree_pointer, dict): self.command = self.tree_pointer self.done = True def clear(self): self.failure = False self.done = False self.quant = None self.moo = [] self.quant2 = None self.command = None self.direction_quant = None self.directions = [] self.all_keys = [] self.tree_pointer = self.keymap._tree self.dir_tree_pointer = self.direction_keys._tree self.eval_quantifier = True self.eval_command = True def __str__(self): """returns a concatenation of all characters""" return "".join(to_string(c) for c in self.all_keys) def simulate_press(self, string): for char in string: self.add(ord(char)) if self.done: return self.command if self.failure: break class Keymap(object): """Contains a tree with all the keybindings""" def __init__(self): self._tree = dict() def add(self, *args, **keywords): if keywords: return self.add_binding(*args, **keywords) firstarg = args[0] if isfunction(firstarg): keywords[FUNC] = firstarg return self.add_binding(*args[1:], **keywords) def decorator_function(func): keywords = {FUNC:func} self.add(*args, **keywords) return func return decorator_function def _split(self, key): assert isinstance(key, (tuple, int, str)) if isinstance(key, tuple): for char in key: yield char elif isinstance(key, str): for char in key: if char == '.': yield ANYKEY elif char == '}': yield DIRKEY else: yield ord(char) elif isinstance(key, int): yield key else: raise TypeError(key) def add_binding(self, *keys, **actions): assert keys bind = binding(keys, actions) for key in keys: assert key chars = tuple(self._split(key)) tree = self.traverse_tree(chars[:-1]) if isinstance(tree, dict): tree[chars[-1]] = bind def traverse_tree(self, generator): tree = self._tree for char in generator: try: tree = tree[char] except KeyError: tree[char] = dict() tree = tree[char] except TypeError: raise TypeError("Attempting to override existing entry") return tree def __getitem__(self, key): tree = self._tree for char in self._split(key): try: tree = tree[char] except TypeError: raise KeyError("trying to enter leaf") except KeyError: raise KeyError(str(char) + " not in tree " + str(tree)) try: return tree except KeyError: raise KeyError(str(char) + " not in tree " + str(tree)) class binding(object): """The keybinding object""" def __init__(self, keys, actions): assert hasattr(keys, '__iter__') assert isinstance(actions, dict) self.keys = set(keys) self.actions = actions try: self.function = self.actions[FUNC] except KeyError: self.function = None self.has_direction = False else: argnames = getargspec(self.function)[0] try: self.has_direction = actions['with_direction'] except KeyError: self.has_direction = DIRECTION in argnames def add_keys(self, keys): assert isinstance(keys, set) self.keys |= keys def has(self, action): return action in self.actions def action(self, key): return self.actions[key] def n(value): """ return n or value """ def fnc(arg=None): if arg is None or arg.n is None: return value return arg.n return fnc def nd(arg): """ n * direction """ if arg.n is None: n = 1 else: n = arg.n if arg.direction is None: dir = Direction(down=1) else: dir = arg.direction return n * dir.down class Test(TestCase): """The test cases""" def _mkpress(self, keybuffer, keymap): def press(keys): keybuffer.clear() match = keybuffer.simulate_press(keys) self.assertFalse(keybuffer.failure, "parsing keys '"+keys+"' did fail!") self.assertTrue(keybuffer.done, "parsing keys '"+keys+"' did not complete!") arg = CommandArgs(None, None, keybuffer) self.assert_(match.function, match.__dict__) return match.function(arg) return press def test_add(self): c = Keymap() c.add(lambda *_: 'lolz', 'aa', 'b') self.assert_(c['aa'].actions[FUNC](), 'lolz') @c.add('a', 'c') def test(): return 5 self.assert_(c['b'].actions[FUNC](), 'lolz') self.assert_(c['c'].actions[FUNC](), 5) self.assert_(c['a'].actions[FUNC](), 5) def test_quantifier(self): km = Keymap() directions = Keymap() kb = KeyBuffer(km, directions) km.add(n(5), 'p') press = self._mkpress(kb, km) self.assertEqual(3, press('3p')) self.assertEqual(6223, press('6223p')) def test_direction(self): km = Keymap() directions = Keymap() kb = KeyBuffer(km, directions) directions.add('j', dir=Direction(down=1)) directions.add('k', dir=Direction(down=-1)) km.add(nd, 'd}') km.add('dd', func=nd, with_direction=False) press = self._mkpress(kb, km) self.assertEqual( 1, press('dj')) self.assertEqual( 3, press('3ddj')) self.assertEqual( 15, press('3d5j')) self.assertEqual(-15, press('3d5k')) # supporting this kind of key combination would be too confusing: # self.assertEqual( 15, press('3d5d')) self.assertEqual( 3, press('3dd')) self.assertEqual( 33, press('33dd')) self.assertEqual( 1, press('dd')) km.add(nd, 'x}') km.add('xxxx', func=nd, with_direction=False) self.assertEqual(1, press('xxxxj')) self.assertEqual(1, press('xxxxjsomeinvalitchars')) # these combinations should break: kb.clear() self.assertEqual(None, kb.simulate_press('xxxj')) kb.clear() self.assertEqual(None, kb.simulate_press('xxj')) kb.clear() self.assertEqual(None, kb.simulate_press('xxkldfjalksdjklsfsldkj')) kb.clear() self.assertEqual(None, kb.simulate_press('xyj')) kb.clear() self.assertEqual(None, kb.simulate_press('x')) #direction missing kb.clear() def test_any_key(self): km = Keymap() directions = Keymap() kb = KeyBuffer(km, directions) directions.add('j', dir=Direction(down=1)) directions.add('k', dir=Direction(down=-1)) def cat(arg): n = arg.n is None and 1 or arg.n return ''.join(chr(c) for c in arg.moo) * n km.add(cat, 'return.') km.add(cat, 'cat4....') press = self._mkpress(kb, km) self.assertEqual('x', press('returnx')) self.assertEqual('abcd', press('cat4abcd')) self.assertEqual('abcdabcd', press('2cat4abcd')) self.assertEqual('55555', press('5return5')) km.add(lambda _: Ellipsis, '.') self.assertEqual('x', press('returnx')) self.assertEqual('abcd', press('cat4abcd')) self.assertEqual(Ellipsis, press('2cat4abcd')) self.assertEqual(Ellipsis, press('5return5')) self.assertEqual(Ellipsis, press('f')) self.assertEqual(Ellipsis, press('ß')) self.assertEqual(Ellipsis, press('ア')) self.assertEqual(Ellipsis, press('9')) if __name__ == '__main__': main()