diff options
-rw-r--r-- | alot/commands/thread.py | 17 | ||||
-rw-r--r-- | alot/defaults/alot.rc.spec | 6 | ||||
-rw-r--r-- | alot/widgets/thread.py | 100 |
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 |