summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2021-01-11 18:35:12 +0100
committerAnton Khirnov <anton@khirnov.net>2021-01-11 18:35:12 +0100
commit81b31e2ad3944961521e26bfcfaba020392150c6 (patch)
tree52d2e137d60a8180b2bb40de02495b250e76dcf9
parentc79196b8b1c5eb99a2f240aaf0069853d798bdd0 (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.py162
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):