Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions alot/defaults/alot.rc.spec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ exclude_tags = force_list(default=list())
# display background colors set by ANSI character escapes
interpret_ansi_background = boolean(default=True)

# Enable colouring message based on quote-level
parse_quotes = boolean(default=True)

# Set what quote symbol should be used (regex symbol is possible)
quote_symbol = string(default='>')

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use the existing quote_prefix?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see it before!

However, I notice that it has a space added to it. That would mean that a line like:

>> Level 2 quote level

wouldn't get interpreted correctly, because the regex r'^ *({} *){{{}}}'.format(symbol, quote_level) wouldn't match. It would still be possible to change it but the default wouldn't work really well.

I'm not sure in what context quote_prefix is used. A first solution would be to remove the space and accommodate for this wherever it is used. A second could be to rename my quote_symbol to something more explicit, like quotation_regex_detection.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this new option is necessary. The two most commonly used characters for quoting text are > and |, and I've only ever seen the bracket being used (wikipedia reference).

It would make the PR leaner to assume only those two characters highlight quotes in messages, plus it would avoid some weird unit tests verifying that the code behaves as expected when using [Aa] for quote_symbol.


# confirm exit
bug_on_exit = boolean(default=False)

Expand Down
7 changes: 7 additions & 0 deletions alot/defaults/theme.spec
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@
width = widthtuple(default=None)
alignment = align(default=None)
[thread]
quote_level_1 = attrtriple(default=None)
quote_level_2 = attrtriple(default=None)
quote_level_3 = attrtriple(default=None)
quote_level_4 = attrtriple(default=None)
quote_level_5 = attrtriple(default=None)
quote_level_6 = attrtriple(default=None)
quote_level_7 = attrtriple(default=None)
arrow_heads = attrtriple
arrow_bars = attrtriple
attachment = attrtriple
Expand Down
131 changes: 83 additions & 48 deletions alot/widgets/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# For further details see the COPYING file

import urwid
import re


class ANSIText(urwid.WidgetWrap):
Expand Down Expand Up @@ -33,8 +34,11 @@ def keypress(self, size, key):

ECODES = {
'1': {'bold': True},
'3': {'italics': True},
'4': {'underline': True},
'5': {'blink': True},
'7': {'standout': True},
'9': {'strikethrough': True},
'30': {'fg': 'black'},
'31': {'fg': 'dark red'},
'32': {'fg': 'dark green'},
Expand All @@ -53,67 +57,98 @@ def keypress(self, size, key):
'47': {'bg': 'light gray'},
}

URWID_MODS = [
'bold',
'underline',
'standout',
'blink',
'italics',
'strikethrough',
]


def parse_escapes_to_urwid(text, default_attr=None, default_attr_focus=None,
parse_background=True):
"""This function converts a text with ANSI escape for terminal
attributes and returns a list containing each part of text and its
corresponding Urwid Attributes object, it also returns a dictionary which
maps all attributes applied here to focused attribute.

This will only translate (a subset of) CSI sequences:
we interpret only SGR parameters that urwid supports (excluding true color)
See https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
"""

text = text.split("\033[")
urwid_text = [text[0]]
b1 = r'\033\[' # Control Sequence Introducer
b2 = r'[0-9:;<=>?]*' # parameter bytes
b3 = r'[ !\"#$%&\'()*+,-./]*' # intermediate bytes
b4 = r'[A-Z[\]^_`a-z{|}~]' # final byte"
esc_pattern = b1 \
+ r'(?P<pb>' + b2 + ')' \
+ r'(?P<ib>' + b3 + ')' \
+ r'(?P<fb>' + b4 + ')'

# these two will be returned
urwid_text = [] # we will accumulate text (with attributes) here
# mapping from included attributes to focused attr
urwid_focus = {None: default_attr_focus}

# Escapes are cumulative so we always keep previous values until it's
# changed by another escape.
attr = dict(fg=default_attr._foreground_color, bg=default_attr.background,
attr = dict(fg=default_attr.foreground, bg=default_attr.background,
bold=default_attr.bold, underline=default_attr.underline,
standout=default_attr.underline)
for part in text[1:]:
esc_code, esc_substr = part.split('m', 1)
esc_code = esc_code.split(';')

if not esc_code:
attr.update(fg=default_attr._foreground_color,
bg=default_attr.background, bold=default_attr.bold,
underline=default_attr.underline,
standout=default_attr.underline)
else:
i = 0
while i < len(esc_code):
code = esc_code[i]
if code == 0:
attr.update({'bold': default_attr.bold,
'underline': default_attr.underline,
'standout': default_attr.standout})
if code in ECODES:
attr.update(ECODES[code])
# 256 codes
elif code == '38':
attr.update(fg='h' + esc_code[i+2])
i += 2
elif code == '48':
attr.update(bg='h'+esc_code[i+2])
i += 2
i += 1

# If there is no string in esc_substr we skip it, the above
# attributes will accumulate to the next escapes.
if esc_substr:
# Construct Urwid attributes
urwid_fg = attr['fg']
urwid_bg = default_attr.background
if attr['bold']:
urwid_fg += ',bold'
if attr['underline']:
urwid_fg += ',underline'
if attr['standout']:
urwid_fg += ',standout'
if parse_background:
urwid_bg = attr['bg']
urwid_attr = urwid.AttrSpec(urwid_fg, urwid_bg)
urwid_focus[urwid_attr] = default_attr_focus
urwid_text.append((urwid_attr, esc_substr))

def append_themed_infix(infix):
# add using prev attribute
urwid_fg = attr['fg']
urwid_bg = default_attr.background
for mod in URWID_MODS:
if mod in attr and attr[mod]:
urwid_fg += ',' + mod
if parse_background:
urwid_bg = attr['bg']
urwid_attr = urwid.AttrSpec(urwid_fg, urwid_bg)
urwid_focus[urwid_attr] = default_attr_focus
urwid_text.append((urwid_attr, infix))

def reset_attr():
attr.update(fg=default_attr.foreground,
bg=default_attr.background, bold=default_attr.bold,
underline=default_attr.underline,
standout=default_attr.underline)

def update_attr(m):
# parameter, intermediate, final bytes in the esc seq
pb, _, fb, = m.groups()
if fb == 'm':
# selector bit found. this means theming changes
if not pb: # no bit r zero --> reset
reset_attr()
elif pb.startswith('38;5;'):
# 8-bit colour foreground
col = pb[5:]
attr.update(fg='h' + col)
elif pb.startswith('48;5;') and parse_background:
# 8-bit colour background
col = pb[5:]
attr.update(bg='h' + col)
else:
# Several attributes can be set in the same sequence,
# separated by semicolons. Interpret them acc to ECODES
codes = pb.split(';')
for code in codes:
if code in ECODES:
attr.update(ECODES[code])

# iterate over text
start = 0 # points to start of current infix

for m in re.finditer(esc_pattern, text):
infix = text[start:m.start()] # text beween last and this Esc seq
update_attr(m)
append_themed_infix(infix) # add using prev attribute
start = m.end() # start of next infix is after this esc sec

append_themed_infix(text[start:]) # add final infix
return urwid_text, urwid_focus
65 changes: 65 additions & 0 deletions alot/widgets/colour_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright (C) 2011-2020 Patrick Totzke <patricktotzke@gmail.com>
# Copyright © 2019-2020 Chloé Dequeker <contact@nelyah.eu>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
import logging
Comment thread
Nelyah marked this conversation as resolved.
import re

from urwid import AttrSpec
from ..settings.const import settings


def parse_text_colour(line):
"""Get colour attribute for the line

:param str line: line of text to be parsed
:return: The theme attribute to apply
"""
if settings.get('parse_quotes'):
Comment thread
Nelyah marked this conversation as resolved.
return parse_quotes(line)
else:
return None


def parse_quotes(line):
"""Search for quotes.
Only search up to the 7th quote level.

:param str line: The line of text to be parsed
:return: Theming attribute, None if no quote are available
"""

# The value is arbitrarily set because we need to define
# the corresponding attributes in the theming configuration spec.
max_quote_level = 7
quote_colour = get_quote_colour(line, max_quote_level)

if isinstance(quote_colour, AttrSpec):
return quote_colour
else:
return None


def get_quote_colour(line, max_quote_level):
Comment thread
lucc marked this conversation as resolved.
"""Cycle through quotation levels for the line

:param str line: The line of text to be parsed
:param int max_quote_level: Search for quotes up to that level
:return: quote_colour (either string 'default' or an AttrSpec object)
"""
symbol = settings.get('quote_symbol')
quote_colour = None

for quote_level in range(1, max_quote_level+1):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think running 7 regex matches on a line that has 7 levels of nested quoting is too much.

I made it work (with a dirty patch) on my local branch with only two calls to re.match(), for any amount of nesting, I think it's the better way to go about it.

Plus if we remove the quote_symbol option (c.f. my other comment), it will be easier to parse spaces properly (RFC reference)

quote_regex = r'^ *({} *){{{}}}'.format(symbol, quote_level)
if re.match(quote_regex, line):
logging.debug(
'Requesting attribute quote_level_{}'.format(quote_level))
quote_colour = settings.get_theming_attribute(
'thread', 'quote_level_{}'.format(quote_level))

else:
# If there is no match at some point,
# we simply use the last level match colour

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think cycling back to the colours for the first level of quoting is a better option:

  • 8 → 1
  • 9 → 2
  • 15 → 1
  • 16 → 2

etc.

Basically a modulo.

break
return quote_colour
12 changes: 11 additions & 1 deletion alot/widgets/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..db.attachment import Attachment
from ..db.utils import decode_header, X_SIGNATURE_MESSAGE_HEADER
from ..helper import string_sanitize
from .colour_text import parse_text_colour

ANSI_BACKGROUND = settings.get("interpret_ansi_background")

Expand Down Expand Up @@ -84,13 +85,19 @@ def __init__(self, content, attr=None, attr_focus=None):
for each line in content.
"""
structure = []
attr_parse = attr

# depending on this config setting, we either add individual lines
# or the complete context as focusable objects.
if settings.get('thread_focus_linewise'):
for line in content.splitlines():
structure.append((ANSIText(line, attr, attr_focus,
attr_parse = parse_text_colour(line)

if attr_parse is None:
attr_parse = attr
structure.append((ANSIText(line, attr_parse, attr_focus,
ANSI_BACKGROUND), None))
attr_parse = attr
else:
structure.append((ANSIText(content, attr, attr_focus,
ANSI_BACKGROUND), None))
Expand All @@ -107,6 +114,7 @@ class DictList(SimpleTree):
its sibblings will be the other pairs and first|last_child will always
be None.
"""

def __init__(self, content, key_attr, value_attr, gaps_attr=None):
"""
:param headerslist: list of key/value pairs to display
Expand Down Expand Up @@ -145,6 +153,7 @@ class MessageTree(CollapsibleTree):

Collapsing this message corresponds to showing the summary only.
"""

def __init__(self, message, odd=True):
"""
:param message: Message to display
Expand Down Expand Up @@ -359,6 +368,7 @@ class ThreadTree(Tree):
messages. As MessageTreess are *not* urwid widgets themself this is to be
used in combination with :class:`NestedTree` only.
"""

def __init__(self, thread):
self._thread = thread
self.root = thread.get_toplevel_messages()[0].get_message_id()
Expand Down
20 changes: 20 additions & 0 deletions docs/source/configuration/alotrc_table
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,16 @@
:default: 2


.. _parse-quotes:

.. describe:: parse_quotes

Enable colouring message based on quote-level

:type: boolean
:default: True


.. _periodic-hook-frequency:

.. describe:: periodic_hook_frequency
Expand Down Expand Up @@ -525,6 +535,16 @@
:default: "> "


.. _quote-symbol:

.. describe:: quote_symbol

Set what quote symbol should be used (regex symbol is possible)

:type: string
:default: ">"


.. _reply-account-header-priority:

.. describe:: reply_account_header_priority
Expand Down
Loading