summaryrefslogtreecommitdiff
path: root/alot/widgets
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 /alot/widgets
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.
Diffstat (limited to 'alot/widgets')
-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):