From 152b319e7c91ee0660e745ab2332750ce4dd58fc Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 7 Sep 2023 20:04:25 -0400 Subject: [PATCH 1/3] Support wrapping column text over multiple lines --- alot/utils/configobj.py | 11 +++++++- alot/widgets/search.py | 51 ++++++++++++++++++++++++++--------- tests/utils/test_configobj.py | 16 +++++++++-- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/alot/utils/configobj.py b/alot/utils/configobj.py index f797ca737..e6fb0378d 100644 --- a/alot/utils/configobj.py +++ b/alot/utils/configobj.py @@ -68,6 +68,13 @@ def width_tuple(value): to use at least width min, and cut of at width max. Here, min and max are positive integers or 0 to disable the boundary. + ('wrap', minw, maxw, minl, maxl): behave like 'fit' but wrap text content + that exceeds width max to the next line. + Here, minl represents the minimum number + of content lines to display, regardless + of whether the content is wrapped, and + maxl represents the maximum number of + content lines to display. ('weight',n): have it relative weight of n compared to other columns. Here, n is an int. """ @@ -75,11 +82,13 @@ def width_tuple(value): res = 'fit', 0, 0 elif not isinstance(value, (list, tuple)): raise VdtTypeError(value) - elif value[0] not in ['fit', 'weight']: + elif value[0] not in ['fit', 'wrap', 'weight']: raise VdtTypeError(value) try: if value[0] == 'fit': res = 'fit', int(value[1]), int(value[2]) + elif value[0] == 'wrap': + res = 'wrap', int(value[1]), int(value[2]), int(value[3]), int(value[4]) else: res = 'weight', int(value[1]) except IndexError: diff --git a/alot/widgets/search.py b/alot/widgets/search.py index b82e7f882..69cddf36e 100644 --- a/alot/widgets/search.py +++ b/alot/widgets/search.py @@ -5,6 +5,7 @@ Widgets specific to search mode """ import urwid +import textwrap from ..settings.const import settings from ..helper import shorten_author_string @@ -137,22 +138,39 @@ def build_text_part(name, thread, struct): # extract min and max allowed width from theme minw = 0 maxw = None + min_lines = 1 + max_lines = 1 width_tuple = struct['width'] if width_tuple is not None: if width_tuple[0] == 'fit': minw, maxw = width_tuple[1:] - - content = prepare_string(name, thread, maxw) - - # pad content if not long enough - if minw: - alignment = struct['alignment'] - if alignment == 'left': - content = content.ljust(minw) - elif alignment == 'center': - content = content.center(minw) - else: - content = content.rjust(minw) + elif width_tuple[0] == 'wrap': + minw, maxw, min_lines, max_lines = width_tuple[1:] + + content = prepare_string(name, thread, max_lines * maxw) + alignment = struct['alignment'] + if width_tuple[0] == 'wrap': + lines = textwrap.wrap( + content, + width=maxw, + max_lines=max_lines, + expand_tabs=False, + placeholder='', + ) + + # ensure minimum number of lines + if len(lines) < min_lines: + lines = lines + [''] * (min_lines - len(lines)) + + # pad content line by line if not long enough + if minw: + lines = [pad_content(l, alignment, minw) for l in lines] + + content = '\n'.join(lines) + else: + # pad content if not long enough + if minw: + content = pad_content(content, alignment, minw) # define width and part_w text = urwid.Text(content, wrap='clip') @@ -162,6 +180,15 @@ def build_text_part(name, thread, struct): return width, part_w +def pad_content(content, alignment, min_width): + if alignment == 'left': + return content.ljust(min_width) + elif alignment == 'center': + return content.center(min_width) + else: + return content.rjust(min_width) + + def prepare_date_string(thread): newest = thread.get_newest_date() if newest is not None: diff --git a/tests/utils/test_configobj.py b/tests/utils/test_configobj.py index d6a9fb44c..efd2ae13b 100644 --- a/tests/utils/test_configobj.py +++ b/tests/utils/test_configobj.py @@ -42,10 +42,22 @@ def test_validates_width_tuple_for_weight_needs_an_argument(self): with self.assertRaises(VdtTypeError): checks.width_tuple(['weight']) - def test_arg_for_width_must_be_a_number(self): + def test_arg_for_weight_must_be_a_number(self): with self.assertRaises(VdtValueError): checks.width_tuple(['weight', 'not-a-number']) - def test_width_with_a_number(self): + def test_weight_with_a_number(self): weight_result = checks.width_tuple(['weight', 123]) self.assertEqual(('weight', 123), weight_result) + + def test_validates_width_tuple_for_wrap_requires_four_args(self): + with self.assertRaises(VdtTypeError): + checks.width_tuple(['wrap', 123, 456, 789]) + + def test_validates_width_tuple_for_wrap_must_be_numbers(self): + with self.assertRaises(VdtValueError): + checks.width_tuple(['wrap', 12, 34, 56, 'not-a-number']) + + def test_wrap_with_four_numbers(self): + fit_result = checks.width_tuple(['wrap', 12, 34, 56, 78]) + self.assertEqual(('wrap', 12, 34, 56, 78), fit_result) From a74fda0c54b27d117d5f31c9a945ef4b5b9dc053 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Fri, 24 Nov 2023 23:00:38 -0500 Subject: [PATCH 2/3] Fix codeclimate ambiguous var name issue --- alot/widgets/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alot/widgets/search.py b/alot/widgets/search.py index 69cddf36e..8a295bb44 100644 --- a/alot/widgets/search.py +++ b/alot/widgets/search.py @@ -164,7 +164,7 @@ def build_text_part(name, thread, struct): # pad content line by line if not long enough if minw: - lines = [pad_content(l, alignment, minw) for l in lines] + lines = [pad_content(line, alignment, minw) for line in lines] content = '\n'.join(lines) else: From 4d841944482ae9f0d653a1a3dbd666cec650c77a Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Mon, 27 Nov 2023 14:42:05 -0500 Subject: [PATCH 3/3] Document pad_content function --- alot/widgets/search.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/alot/widgets/search.py b/alot/widgets/search.py index 8a295bb44..80cbc9012 100644 --- a/alot/widgets/search.py +++ b/alot/widgets/search.py @@ -181,6 +181,9 @@ def build_text_part(name, thread, struct): def pad_content(content, alignment, min_width): + """ + pad 'content' to 'min_width' justified according to 'alignment'. + """ if alignment == 'left': return content.ljust(min_width) elif alignment == 'center':