summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/commands/thread.py17
-rw-r--r--alot/defaults/alot.rc.spec6
-rw-r--r--alot/widgets/thread.py100
3 files changed, 115 insertions, 8 deletions
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
index af5576cb..de948747 100644
--- a/alot/commands/thread.py
+++ b/alot/commands/thread.py
@@ -519,12 +519,16 @@ class EditNewCommand(Command):
MODE, 'indent', help='change message/reply indentation',
arguments=[(['indent'], {'action': cargparse.ValidatedStoreAction,
'validator': cargparse.is_int_or_pm})])
+@registerCommand(
+ MODE, 'fold', help='change message folding level',
+ arguments=[(['fold'], {'action': cargparse.ValidatedStoreAction,
+ 'validator': cargparse.is_int_or_pm})])
class ChangeDisplaymodeCommand(Command):
repeatable = True
def __init__(self, query=None, raw=None, all_headers=None,
- indent=None, **kwargs):
+ indent=None, fold = None, **kwargs):
"""
:param query: notmuch query string used to filter messages to affect
:type query: str
@@ -534,6 +538,8 @@ class ChangeDisplaymodeCommand(Command):
:type all_headers: True, False, 'toggle' or None
:param indent: message/reply indentation
:type indent: '+', '-', or int
+ :param fold: message fold level
+ :type indent: '+', '-', or int
"""
self.query = None
if query:
@@ -541,6 +547,7 @@ class ChangeDisplaymodeCommand(Command):
self.raw = raw
self.all_headers = all_headers
self.indent = indent
+ self.fold = fold
Command.__init__(self, **kwargs)
def apply(self, ui):
@@ -589,6 +596,14 @@ class ChangeDisplaymodeCommand(Command):
if all_headers is not None:
m.display_all_headers = all_headers
+ if self.fold is not None:
+ if self.fold == '+':
+ m.foldlevel += 1
+ elif self.fold == '-':
+ m.foldlevel -= 1
+ else:
+ m.foldlevel = int(self.fold)
+
@registerCommand(MODE, 'pipeto', arguments=[
(['cmd'], {'help': 'shellcommand to pipe to', 'nargs': '+'}),
diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec
index 17d3cc13..943bfe16 100644
--- a/alot/defaults/alot.rc.spec
+++ b/alot/defaults/alot.rc.spec
@@ -203,6 +203,12 @@ prompt_suffix = string(default=':')
# String prepended to line when quoting
quote_prefix = string(default='> ')
+# number of context lines at the start and end of a folded quote
+thread_fold_context = mixed_list(integer, integer, default = list(2, 2))
+
+# initial fold level for quotes
+thread_fold_initial_level = integer(default = 0)
+
# String prepended to subject header on reply
# only if original subject doesn't start with 'Re:' or this prefix
reply_subject_prefix = string(default='Re: ')
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py
index b410ff14..dd179ed9 100644
--- a/alot/widgets/thread.py
+++ b/alot/widgets/thread.py
@@ -75,10 +75,30 @@ class _MIMEPartWidget(urwid.WidgetWrap):
# when not None, a list of child _MIMEPartWidget subclasses
_children = None
+ _fold_level = None
+
def __init__(self, body_wgt, children = None):
self._children = children
+ self._fold_level = 0
super().__init__(body_wgt)
+ @property
+ def foldlevel(self):
+ return self._fold_level
+ @foldlevel.setter
+ def foldlevel(self, val):
+ if self._children is None:
+ return
+
+ val_in = max(0, val)
+ val_out = 0
+
+ for ch in self._children:
+ ch.foldlevel = val_in
+ val_out = max(val_out, ch.foldlevel)
+
+ self._fold_level = val_out
+
class _CryptPartWidget(_MIMEPartWidget):
def __init__(self, mime_tree, alternative_pref):
children = None
@@ -132,17 +152,53 @@ class _AltMixedPart(_MIMEPartWidget):
def __init__(self, children):
super().__init__(urwid.Pile(children), children)
+class _TextBlock(urwid.WidgetWrap):
+
+ _level = None
+
+ _switch_wgt = None
+ _text_wgt = None
+ _folded_wgt = None
+
+ def __init__(self, level, lines, attr, fold_context):
+ self._level = level
+ self._text_wgt = urwid.Text((attr, '\n'.join(lines)))
+
+ context_start, context_end = fold_context
+ if level == 0 or len(lines) <= context_start + context_end + 1:
+ # block is too small for folding
+ self._folded_wgt = self._text_wgt
+ else:
+ folded_lines = lines[:context_start]
+ folded_lines.append('%s [...%d lines folded...]' %
+ ('> ' * level, len(lines) - context_start - context_end))
+ folded_lines.extend(lines[-context_end:])
+ self._folded_wgt = urwid.Text((attr, '\n'.join(folded_lines)))
+
+ self._switch_wgt = urwid.WidgetPlaceholder(self._text_wgt)
+
+ super().__init__(self._switch_wgt)
+
+ def set_foldlevel(self, level):
+ wgt = self._text_wgt if level >= self._level else self._folded_wgt
+ self._switch_wgt.original_widget = wgt
+
class _TextPart(_MIMEPartWidget):
_QUOTE_CHARS = '>|:}#'
_QUOTE_REGEX = '(([ \t]*[{quote_chars}])+)'.format(quote_chars = _QUOTE_CHARS)
+ _max_level = None
+ _block_wgts = None
+
def __init__(self, text):
attr_text = settings.get_theming_attribute('thread', 'body')
attrs_quote = settings.get_quote_theming()
+ fold_context = settings.get('thread_fold_context')
lines = text.splitlines()
- quote_levels = []
+ blocks = []
+ max_level = 0
for line in lines:
level = 0
m = re.match(self._QUOTE_REGEX, line)
@@ -151,18 +207,39 @@ class _TextPart(_MIMEPartWidget):
for c in self._QUOTE_CHARS:
level += g.count(c)
- quote_levels.append(level)
+ max_level = max(max_level, level)
- line_widgets = []
+ if len(blocks) > 0 and blocks[-1][0] == level:
+ blocks[-1][1].append(line)
+ else:
+ blocks.append((level, [line]))
- for level, line in zip(quote_levels, lines):
+ block_wgts = []
+ for level, lines in blocks:
if level == 0 or len(attrs_quote) < 1:
attr = attr_text
else:
attr = attrs_quote[(level - 1) % len(attrs_quote)]
- line_widgets.append(urwid.Text((attr, line)))
- super().__init__(urwid.Pile(line_widgets))
+ wgt = _TextBlock(level, lines, attr, fold_context)
+ block_wgts.append(wgt)
+
+ self._block_wgts = block_wgts
+ self._max_level = max_level
+
+ super().__init__(urwid.Pile(block_wgts))
+
+ @property
+ def foldlevel(self):
+ return self._fold_level
+ @foldlevel.setter
+ def foldlevel(self, val):
+ val = max(0, min(self._max_level, val))
+ logging.info('settting foldlevel to %d', val)
+
+ for b in self._block_wgts:
+ b.set_foldlevel(val)
+ self._fold_level = val
class _EmptyMessageWidget(_MIMEPartWidget):
def __init__(self):
@@ -201,7 +278,7 @@ def _render_mime_tree(mime_tree, alternative_pref):
if mime_tree.is_alternative:
return _handle_alternative(mime_tree, alternative_pref)
- return _AltMixedPart(mime_tree, alternative_pref)
+ return _handle_mixed(mime_tree, alternative_pref)
# no children - this is a leaf node
# skip attachment parts
@@ -339,6 +416,8 @@ class MessageWidget(urwid.WidgetWrap):
self.display_all_headers = False
self.display_source = False
+ self.foldlevel = settings.get('thread_fold_initial_level')
+
def get_message(self):
return self._message
@@ -425,6 +504,13 @@ class MessageWidget(urwid.WidgetWrap):
self._reassemble(val)
self._display_source = val
+ @property
+ def foldlevel(self):
+ return self._body_wgt.foldlevel
+ @foldlevel.setter
+ def foldlevel(self, val):
+ self._body_wgt.foldlevel = val
+
def get_selected_attachment(self):
"""
If an AttachmentWidget is currently focused, return it. Otherwise return