diff options
author | Anton Khirnov <anton@khirnov.net> | 2021-01-11 18:35:12 +0100 |
---|---|---|
committer | Anton Khirnov <anton@khirnov.net> | 2021-01-11 18:35:12 +0100 |
commit | 81b31e2ad3944961521e26bfcfaba020392150c6 (patch) | |
tree | 52d2e137d60a8180b2bb40de02495b250e76dcf9 | |
parent | c79196b8b1c5eb99a2f240aaf0069853d798bdd0 (diff) |
widgets/thread: refactor message body folding
Currently we split the message body into a list of widgets whose
appearance changes depending on the foldlevel. This is limiting, since
the individual widgets cannot be hidden completely, and the tree-like
structure of the quote levels is not preserved in this representation.
After this commit, we parse the message into a tree of nested folds,
defined by a start and end line. Hiding a given fold also hides any
nested content.
This should also be more flexible wrt possible future improvements, such
as folding individual folds (like in vim) instead of entire levels.
-rw-r--r-- | alot/widgets/thread.py | 162 |
1 files changed, 106 insertions, 56 deletions
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py index 266fc0df..fe78cb01 100644 --- a/alot/widgets/thread.py +++ b/alot/widgets/thread.py @@ -135,80 +135,90 @@ class _AltMixedPart(_MIMEPartWidget): def __init__(self, children): super().__init__(urwid.Pile(children), children) -class _TextBlock(urwid.WidgetWrap): - - _level = None +class _Fold: + level = None + start = None + end = None + + children = None + + def __init__(self, level, start, end): + self.children = [] + + self.level = level + self.start = start + self.end = end + + def fold(self, lines, level, context): + ret = [] + + if level == self.level: + # this level is the lowest folded one + # display only the context + ctx_start, ctx_end = context + length = self.end - self.start + if length <= ctx_start + ctx_end + 1: + ret.extend(lines[self.start:self.end]) + else: + ret.extend(lines[self.start:self.start + ctx_start]) + attr = ret[-1][0] + ret.append((attr, '%s [...%d lines folded...]\n' % + ('> ' * level, length - ctx_start - ctx_end))) + ret.extend(lines[self.end - ctx_end:self.end]) + elif level > self.level: + # this level is not folded, display as-is + # but we still need to fold the children individually - _switch_wgt = None - _text_wgt = None - _folded_wgt = None + # start by adding lines before the first child, or all + # lines when there are no children + end = self.children[0].start if self.children else self.end + ret.extend(lines[self.start:end]) - def __init__(self, level, lines, attr, fold_context): - self._level = level - self._text_wgt = urwid.Text((attr, '\n'.join(lines))) + for i, c in enumerate(self.children): + # fold the child recursively + ret.extend(c.fold(lines, level, context)) - 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))) + # add lines inbetween this child's end and next child's start + # or our end if this is the last child + next_start = self.children[i + 1].start if (i + 1 < len(self.children)) \ + else self.end + ret.extend(lines[c.end:next_start]) - self._switch_wgt = urwid.WidgetPlaceholder(self._text_wgt) + return ret - super().__init__(self._switch_wgt) + def __repr__(self): + return 'Fold(%d, %d, %d, %s)' % \ + (self.level, self.start, self.end, self.children) - 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 + _lines = None + _fold = 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') - - blocks = self._split_quotes(text) - max_level = max((b['level'] for b in blocks)) - - block_wgts = [] - for b in blocks: - level = b['level'] - lines = b['data'] - if level == 0 or len(attrs_quote) < 1: - attr = attr_text - else: - attr = attrs_quote[(level - 1) % len(attrs_quote)] - - wgt = _TextBlock(level, lines, attr, fold_context) - block_wgts.append(wgt) + self._fold_context = settings.get('thread_fold_context') - self._block_wgts = block_wgts - self._max_level = max_level + self._lines, self._fold, self._max_level = \ + self._parse_quotes(text, attr_text, attrs_quote) - super().__init__(urwid.Pile(block_wgts)) + super().__init__(urwid.Text('')) - def _split_quotes(self, text): + def _parse_quotes(self, text, attr_text, attrs_quote): """ - Split the text into blocks of quote levels. + Parse quotes in email body, generate folds and theming from them. """ - lines = text.splitlines() - # first pass: assign a level to each line based on the number of quote # characters at the beginning - blocks = [] - for line in lines: + lines = text.splitlines(keepends = True) + blocks = [] + for i, line in enumerate(lines): level = 0 m = re.match(self._QUOTE_REGEX, line) if m is not None: @@ -222,9 +232,9 @@ class _TextPart(_MIMEPartWidget): if len(blocks) == 0 or blocks[-1]['level'] != level: # start a new block - blocks.append({ 'level' : level, 'data' : [], 'data_empty' : True}) + blocks.append({ 'level' : level, 'start' : i, 'end' : i, 'data_empty' : True}) - blocks[-1]['data'].append(line) + blocks[-1]['end'] += 1 if len(remainder) > 0: blocks[-1]['data_empty'] = False @@ -248,18 +258,58 @@ class _TextPart(_MIMEPartWidget): if merge_level is not None: b['level'] = merge_level - return blocks + fold_stack = [] + cur_fold = _Fold(0, 0, None) + max_level = 0 + for b in blocks: + max_level = max(max_level, b['level']) + + if b['level'] > cur_fold.level: + fold_stack.append(cur_fold) + cur_fold = _Fold(b['level'], b['start'], b['end']) + elif b['level'] == cur_fold.level: + cur_fold.end = b['end'] + else: + child = cur_fold + + cur_fold = fold_stack.pop() + cur_fold.end = b['end'] + cur_fold.children.append(child) + + while fold_stack: + fold_stack[-1].children.append(cur_fold) + cur_fold = fold_stack.pop() + + assert(cur_fold.level == 0) + cur_fold.end = len(lines) + + def color_lines(fold, lines, colored_lines): + if fold.level == 0 or len(attrs_quote) < 1: + attr = attr_text + else: + attr = attrs_quote[(fold.level - 1) % len(attrs_quote)] + + colored_lines[fold.start:fold.end] = [(attr, line) for line in lines[fold.start:fold.end]] + + for c in fold.children: + color_lines(c, lines, colored_lines) + + colored_lines = lines[:] + color_lines(cur_fold, lines, colored_lines) + + return colored_lines, cur_fold, max_level @property def foldlevel(self): return self._fold_level @foldlevel.setter def foldlevel(self, val): - val = max(0, min(self._max_level, val)) + val = max(1, min(self._max_level + 1, val)) logging.info('settting foldlevel to %d', val) - for b in self._block_wgts: - b.set_foldlevel(val) + text = self._fold.fold(self._lines, val, + self._fold_context) + self._w = urwid.Text(text) self._fold_level = val class _EmptyMessageWidget(_MIMEPartWidget): |