~rjarry/aerc-devel

aerc: Allow styling part selector MIME and filename v1 APPLIED

This started as a follow up on
https://lists.sr.ht/~rjarry/aerc-devel/patches/44342 and grew out of
hand pretty quickly.

Patch 3/3 also contains a revert of the aforementioned patch since
colors/style now allow differentiating MIME types from filenames without
any alignment.

Robin Jarry (3):
  config: add default values for empty stylesets
  colorize: support trailing comments after values
  msgviewer: add styles for part selector

 CHANGELOG.md                             |   9 ++
 config/style.go                          | 108 +++++++++++++++++++----
 doc/aerc-stylesets.7.scd                 |  48 ++++++++++
 filters/colorize.c                       |   2 +-
 filters/test.sh                          |   8 +-
 filters/vectors/colorize-patch.expected  |  18 ++--
 filters/vectors/colorize-quotes.expected |   4 +-
 stylesets/blue                           |   9 ++
 stylesets/default                        |  52 ++++++-----
 stylesets/dracula                        |   9 ++
 stylesets/nord                           |   6 ++
 stylesets/pink                           |   9 +-
 stylesets/solarized                      |   3 +
 widgets/msgviewer.go                     |  52 ++++-------
 widgets/msgviewer_test.go                |  68 --------------
 15 files changed, 247 insertions(+), 158 deletions(-)
 delete mode 100644 widgets/msgviewer_test.go

-- 
2.41.0
#1059558 alpine-edge.yml success
#1059559 openbsd.yml success
aerc/patches: SUCCESS in 5m8s

[Allow styling part selector MIME and filename][0] from [Robin Jarry][1]

[0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/44854
[1]: mailto:robin@jarry.cc

✓ #1059559 SUCCESS aerc/patches/openbsd.yml     https://builds.sr.ht/~rjarry/job/1059559
✓ #1059558 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1059558
Beautiful!

Tested-By: inwit <inwit@sindominio.net>



          
          
          
        
      

      
      
      
      
      
      

      
      
        
          






inwit, Sep 19, 2023 at 16:22:
Next
Vitaly Ovchinnikov, Sep 19, 2023 at 23:04:
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~rjarry/aerc-devel/patches/44854/mbox | git am -3
Learn more about email & git

[PATCH aerc 1/3] config: add default values for empty stylesets Export this patch

When adding new style objects it is impossible to give them a default
color nor attributes without modifying each existing user styleset.
Also, if the user has an incomplete styleset, some parts of aerc will
have no style at all. These quirks are not nice from a user experience
point of view.

Before parsing the user styleset, initialize aerc style with basic
defaults. Reuse the exact same content than the actual "default"
styleset provided in /usr/share/aerc/stylesets. Comment all of the
default styleset to make it obvious that these are default values.

This has some implications:

* To reset these defaults, the user styleset must now start with these
  two lines:

    *.default = true
    *.normal = true

  If these two lines are not present, the default style will be kept and
  only changed if the user styleset explicitly sets them.

* Empty stylesets no longer produce weird results.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
 CHANGELOG.md             |  9 ++++
 config/style.go          | 89 ++++++++++++++++++++++++++++++++--------
 doc/aerc-stylesets.7.scd | 39 ++++++++++++++++++
 stylesets/blue           |  3 ++
 stylesets/default        | 49 +++++++++++-----------
 stylesets/dracula        |  3 ++
 stylesets/nord           |  1 +
 stylesets/pink           |  3 +-
 stylesets/solarized      |  1 +
 9 files changed, 156 insertions(+), 41 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d138a8383026..4e9b3f2a9064 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  default. Legacy behaviour can be restored by setting `send-with-hostname
  = true` in `accounts.conf`.
- The notmuch bindings were replaced with internal bindings
- Aerc now has a default style for most UI elements. The `default` styleset is
  now empty. Existing stylesets will only override the default attributes if
  they are set explicitly. To reset the default style and preserve existing
  stylesets appearance, these two lines must be inserted **at the beginning**:

  ```
  *.default=true
  *.normal=true
  ```

### Deprecated

diff --git a/config/style.go b/config/style.go
index 50c53de328b5..f50126bdb361 100644
--- a/config/style.go
+++ b/config/style.go
@@ -280,25 +280,82 @@ func NewStyleSet() StyleSet {
		user:     make(map[string]*Style),
	}
	for _, so := range StyleNames {
		ss.objects[so] = new(StyleConf)
		ss.selected[so] = new(StyleConf)
		conf := new(StyleConf)

		switch so {
		case STYLE_ERROR:
			// *error.bold=true
			conf.base.Bold = true
			// error.fg=red
			conf.base.Fg = tcell.ColorRed
		case STYLE_WARNING:
			// warning.fg=yellow
			conf.base.Fg = tcell.ColorYellow
		case STYLE_SUCCESS:
			// success.fg=green
			conf.base.Fg = tcell.ColorGreen
		case STYLE_TITLE:
			// title.reverse=true
			conf.base.Reverse = true
		case STYLE_HEADER:
			// header.bold=true
			conf.base.Bold = true
		case STYLE_STATUSLINE_DEFAULT:
			// statusline_default.reverse=true
			conf.base.Reverse = true
		case STYLE_STATUSLINE_ERROR:
			// *error.bold=true
			conf.base.Fg = tcell.ColorRed
			// statusline_error.fg=red
			conf.base.Bold = true
			// statusline_error.reverse=true
			conf.base.Reverse = true
		case STYLE_STATUSLINE_WARNING:
			// statusline_warning.fg=yellow
			conf.base.Fg = tcell.ColorYellow
			// statusline_warning.reverse=true
			conf.base.Reverse = true
		case STYLE_STATUSLINE_SUCCESS:
			conf.base.Fg = tcell.ColorGreen
			conf.base.Reverse = true
		case STYLE_MSGLIST_UNREAD:
			// msglist_unread.bold=true
			conf.base.Bold = true
		case STYLE_MSGLIST_DELETED:
			// msglist_deleted.fg=gray
			conf.base.Fg = tcell.ColorGray
		case STYLE_MSGLIST_RESULT:
			// msglist_result.fg=green
			conf.base.Fg = tcell.ColorGreen
		case STYLE_MSGLIST_PILL:
			// msglist_pill.reverse=true
			conf.base.Reverse = true
		case STYLE_COMPLETION_PILL:
			// completion_pill.reverse=true
			conf.base.Reverse = true
		case STYLE_TAB:
			// tab.reverse=true
			conf.base.Reverse = true
		case STYLE_BORDER:
			// border.reverse = true
			conf.base.Reverse = true
		case STYLE_SELECTOR_FOCUSED:
			// selector_focused.reverse=true
			conf.base.Reverse = true
		case STYLE_SELECTOR_CHOOSER:
			// selector_chooser.bold=true
			conf.base.Bold = true
		}

		ss.objects[so] = conf
		selected := *conf
		// *.selected.reverse=toggle
		selected.base.Reverse = !conf.base.Reverse
		ss.selected[so] = &selected
	}
	return ss
}

func (ss StyleSet) reset() {
	for _, so := range StyleNames {
		ss.objects[so].base.Reset()
		for _, d := range ss.objects[so].dynamic {
			d.Reset()
		}
		ss.selected[so].base.Reset()
		for _, d := range ss.selected[so].dynamic {
			d.Reset()
		}
	}
}

func (c *StyleConf) getStyle(h *mail.Header) *Style {
	if h == nil {
		return &c.base
@@ -367,8 +424,6 @@ func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) {
}

func (ss *StyleSet) ParseStyleSet(file *ini.File) error {
	ss.reset()

	defaultSection, err := file.GetSection(ini.DefaultSection)
	if err != nil {
		return err
diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
index 91b6e77aff82..4f43859b3d2d 100644
--- a/doc/aerc-stylesets.7.scd
+++ b/doc/aerc-stylesets.7.scd
@@ -334,6 +334,45 @@ _<Dec number>_
	Color based on the terminal palette index. Valid numbers are
	between _0_ and _255_.

# DEFAULTS

Before parsing a styleset, it is first initialized with the following defaults:

```
*.selected.reverse=toggle
title.reverse=true
header.bold=true
*error.bold=true
error.fg=red
warning.fg=yellow
success.fg=green
statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_error.reverse=true
statusline_warning.fg=yellow
statusline_warning.reverse=true
msglist_unread.bold=true
msglist_deleted.fg=gray
msglist_result.fg=green
msglist_pill.reverse=true
completion_pill.reverse=true
tab.reverse=true
border.reverse = true
selector_focused.reverse=true
selector_chooser.bold=true
```

You can choose either to reset everything by starting your styleset with these
two lines:

```
*.default=true
*.normal=true
```

Or selectively override style object attributes.

# SEE ALSO

*aerc*(1) *aerc-config*(5)
diff --git a/stylesets/blue b/stylesets/blue
index c598ef92da78..46e04b4604ff 100644
--- a/stylesets/blue
+++ b/stylesets/blue
@@ -1,5 +1,8 @@
# vim: ft=dosini

*.default=true
*.normal=true

border.bg=#005f87
title.bg=#005f87

diff --git a/stylesets/default b/stylesets/default
index f3ea273396f7..c0d6f90919d1 100644
--- a/stylesets/default
+++ b/stylesets/default
@@ -6,36 +6,39 @@
# the aerc-stylesets(7) manpage. Please read the manual before
# modifying or creating a styleset.

*.default=true
*.selected.reverse=toggle
# Uncomment these two lines to reset all attributes and start from scratch.
#*.default=true
#*.normal=true

title.reverse=true
header.bold=true
#*.selected.reverse=toggle
#
#title.reverse=true
#header.bold=true

*error.bold=true
error.fg=red
warning.fg=yellow
success.fg=green
#*error.bold=true
#error.fg=red
#warning.fg=yellow
#success.fg=green

statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_error.reverse=true
statusline_warning.fg=yellow
statusline_warning.reverse=true
#statusline*.default=true
#statusline_default.reverse=true
#statusline_error.fg=red
#statusline_error.reverse=true
#statusline_warning.fg=yellow
#statusline_warning.reverse=true

msglist_unread.bold=true
msglist_deleted.fg=gray
msglist_result.fg=green
msglist_pill.reverse=true
#msglist_unread.bold=true
#msglist_deleted.fg=gray
#msglist_result.fg=green
#msglist_pill.reverse=true

completion_pill.reverse=true
#completion_pill.reverse=true

tab.reverse=true
border.reverse = true
#tab.reverse=true
#border.reverse = true

selector_focused.reverse=true
selector_chooser.bold=true
#selector_focused.reverse=true
#selector_chooser.bold=true

#[viewer]
#url.underline=true
diff --git a/stylesets/dracula b/stylesets/dracula
index 9e24f422af93..8b8788e9ce40 100644
--- a/stylesets/dracula
+++ b/stylesets/dracula
@@ -1,3 +1,6 @@
*.default=true
*.normal=true

#border.bg=#BD93F9
title.bg=#BD93F9

diff --git a/stylesets/nord b/stylesets/nord
index f099a43ffa3b..e9c7e853aa9f 100644
--- a/stylesets/nord
+++ b/stylesets/nord
@@ -3,6 +3,7 @@
#

*.default=true
*.normal=true

title.reverse=true
header.bold=true
diff --git a/stylesets/pink b/stylesets/pink
index 1f73d0f78700..9fd8c213feb2 100644
--- a/stylesets/pink
+++ b/stylesets/pink
@@ -1,6 +1,7 @@
# vim: ft=dosini

#de4e85
*.default=true
*.normal=true

border.bg=#de4e85
title.bg=#de4e85
diff --git a/stylesets/solarized b/stylesets/solarized
index 026c3755d162..dcd8606f7ce2 100644
--- a/stylesets/solarized
+++ b/stylesets/solarized
@@ -3,6 +3,7 @@
#

*.default=true
*.normal=true
*error.bold=true
border.reverse=true
completion_pill.reverse=true
-- 
2.41.0

[PATCH aerc 2/3] colorize: support trailing comments after values Export this patch

Strip trailing comments after attribute values in stylesets. Update the
test styleset to check it works.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
 filters/colorize.c                       |  2 +-
 filters/test.sh                          |  8 ++++----
 filters/vectors/colorize-patch.expected  | 18 +++++++++---------
 filters/vectors/colorize-quotes.expected |  4 ++--
 4 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/filters/colorize.c b/filters/colorize.c
index d991cb7214b6..03f0fc75c6a2 100644
--- a/filters/colorize.c
+++ b/filters/colorize.c
@@ -337,7 +337,7 @@ static struct {const char *n; struct style *s;} ini_objects[] = {
};

/*                         object            attribute           value */
#define STYLE_LINE_FORMAT "%127[0-9A-Za-z_-].%127[0-9a-zA-Z_-] = %127s"
#define STYLE_LINE_FORMAT "%127[0-9A-Za-z_-].%127[0-9a-zA-Z_-] = %127[#a-zA-Z0-9]s"

static int parse_styleset(void)
{
diff --git a/filters/test.sh b/filters/test.sh
index c0d9e6cdff53..16d1438c6c98 100755
--- a/filters/test.sh
+++ b/filters/test.sh
@@ -12,15 +12,15 @@ cat >$style <<EOF
url.fg = red

[viewer]
url.underline = true
header.bold=    true
url.underline = true # cxwlkj
header.bold=    true  # comment
signature.dim=true
diff_meta.bold    =true
diff_chunk.dim=		true
invalid . xxx = lkjfdslkjfdsqqqqqlkjdsq
diff_add.fg=2
diff_add.fg= #00ff00 # comment
# comment
diff_del.fg=     1
diff_del.fg=     1		# comment2
quote_*.fg     =6
quote_*.dim=true
quote_1.dim=false
diff --git a/filters/vectors/colorize-patch.expected b/filters/vectors/colorize-patch.expected
index dd04c5b7d0bf..fe9209903223 100644
--- a/filters/vectors/colorize-patch.expected
+++ b/filters/vectors/colorize-patch.expected
@@ -24,12 +24,12 @@ According to scdoc(5), numbered lists start with a period.
-4. *msglist_flagged*
-5. *msglist_deleted*
-6. *msglist_marked*
+. *msglist_default*
+. *msglist_unread*
+. *msglist_read*
+. *msglist_flagged*
+. *msglist_deleted*
+. *msglist_marked*
+. *msglist_default*
+. *msglist_unread*
+. *msglist_read*
+. *msglist_flagged*
+. *msglist_deleted*
+. *msglist_marked*
 
 So, the marked style will override all other msglist styles.
 
@@ -38,9 +38,9 @@ According to scdoc(5), numbered lists start with a period.
-1. *dirlist_default*
-2. *dirlist_unread*
-3. *dirlist_recent*
+. *dirlist_default*
+. *dirlist_unread*
+. *dirlist_recent*
+. *dirlist_default*
+. *dirlist_unread*
+. *dirlist_recent*
 
 ## COLORS
 
diff --git a/filters/vectors/colorize-quotes.expected b/filters/vectors/colorize-quotes.expected
index 4e1e643c22c6..a5d13e008c51 100644
--- a/filters/vectors/colorize-quotes.expected
+++ b/filters/vectors/colorize-quotes.expected
@@ -37,12 +37,12 @@ facilisi et pri http:// or https://!
> diff --git a/foo b/foo
> index 4b0fe8dded3a..518b67134639 100644
> --- a/foo
> +++ b/foo
> +++ b/foo
> @@ -131,6 +131,83 @@ func pouet() int {
>          err := doThis()
>  
> -        err2 := doThat()
> +        err2 := notDoThat()
> +        err2 := notDoThat()
>  
>          if err != nil || err2 != nil {

-- 
2.41.0

[PATCH aerc 3/3] msgviewer: add styles for part selector Export this patch

Allow styling the part selector mime type and (if any) attachment
filename. Remove custom alignment code since now both can be
differentiated easily with colors and/or attributes.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
config/style.go           | 19 +++++++++++
doc/aerc-stylesets.7.scd  |  9 ++++++
stylesets/blue            |  6 ++++
stylesets/default         |  3 ++
stylesets/dracula         |  6 ++++
stylesets/nord            |  5 +++
stylesets/pink            |  6 ++++
stylesets/solarized       |  2 ++
widgets/msgviewer.go      | 52 +++++++++++-------------------
widgets/msgviewer_test.go | 68 ---------------------------------------
10 files changed, 75 insertions(+), 101 deletions(-)
delete mode 100644 widgets/msgviewer_test.go

diff --git a/config/style.go b/config/style.go
index f50126bdb361..3f117fc067d4 100644
--- a/config/style.go
+++ b/config/style.go
@@ -46,6 +46,10 @@ const (
	STYLE_DIRLIST_UNREAD
	STYLE_DIRLIST_RECENT

	STYLE_PART_SWITCHER
	STYLE_PART_FILENAME
	STYLE_PART_MIMETYPE

	STYLE_COMPLETION_DEFAULT
	STYLE_COMPLETION_GUTTER
	STYLE_COMPLETION_PILL
@@ -91,6 +95,10 @@ var StyleNames = map[string]StyleObject{
	"dirlist_unread":  STYLE_DIRLIST_UNREAD,
	"dirlist_recent":  STYLE_DIRLIST_RECENT,

	"part_switcher": STYLE_PART_SWITCHER,
	"part_filename": STYLE_PART_FILENAME,
	"part_mimetype": STYLE_PART_MIMETYPE,

	"completion_default": STYLE_COMPLETION_DEFAULT,
	"completion_gutter":  STYLE_COMPLETION_GUTTER,
	"completion_pill":    STYLE_COMPLETION_PILL,
@@ -330,6 +338,9 @@ func NewStyleSet() StyleSet {
		case STYLE_MSGLIST_PILL:
			// msglist_pill.reverse=true
			conf.base.Reverse = true
		case STYLE_PART_MIMETYPE:
			// part_mimetype.dim=true
			conf.base.Dim = true
		case STYLE_COMPLETION_PILL:
			// completion_pill.reverse=true
			conf.base.Reverse = true
@@ -351,6 +362,14 @@ func NewStyleSet() StyleSet {
		selected := *conf
		// *.selected.reverse=toggle
		selected.base.Reverse = !conf.base.Reverse
		switch so {
		case STYLE_PART_MIMETYPE:
			// part_mimetype.selected.dim=false
			selected.base.Dim = false
		case STYLE_PART_FILENAME:
			// part_filename.selected.bold=true
			selected.base.Bold = true
		}
		ss.selected[so] = &selected
	}
	return ss
diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
index 4f43859b3d2d..698ad6716abf 100644
--- a/doc/aerc-stylesets.7.scd
+++ b/doc/aerc-stylesets.7.scd
@@ -119,6 +119,12 @@ styling.
:  The style used for directories with unread messages
|  *dirlist_recent*
:  The style used for directories with recent messages
|  *part_switcher*
:  Background for the part switcher in the message viewer.
|  *part_filename*
:  Attachment file name in the part switcher.
|  *part_mimetype*
:  Attachment/part MIME type in the part switcher.
|  *completion_default*
:  The default style for the completion engine.
|  *completion_gutter*
@@ -356,6 +362,9 @@ msglist_unread.bold=true
msglist_deleted.fg=gray
msglist_result.fg=green
msglist_pill.reverse=true
part_mimetype.dim=true
part_mimetype.selected.dim=false
part_filename.selected.bold=true
completion_pill.reverse=true
tab.reverse=true
border.reverse = true
diff --git a/stylesets/blue b/stylesets/blue
index 46e04b4604ff..4ffcbc876bf6 100644
--- a/stylesets/blue
+++ b/stylesets/blue
@@ -37,6 +37,12 @@ msglist_marked.bg=#005f87
msglist_marked.selected.bg=#005fff
msglist_pill.reverse=true

part_*.fg=#ffffff
part_mimetype.fg=#005f87
part_*.selected.fg=#ffffff
part_*.selected.bg=#005f87
part_filename.selected.bold=true

completion_pill.reverse=true
selector_focused.bold=true
selector_focused.bg=#005f87
diff --git a/stylesets/default b/stylesets/default
index c0d6f90919d1..74bbe97a068c 100644
--- a/stylesets/default
+++ b/stylesets/default
@@ -32,6 +32,9 @@
#msglist_result.fg=green
#msglist_pill.reverse=true

#part_mimetype.dim=true
#part_mimetype.selected.dim=false
#part_filename.selected.bold=true
#completion_pill.reverse=true

#tab.reverse=true
diff --git a/stylesets/dracula b/stylesets/dracula
index 8b8788e9ce40..73623547d7ad 100644
--- a/stylesets/dracula
+++ b/stylesets/dracula
@@ -37,6 +37,12 @@ msglist_marked.bg=#BD93F9
msglist_marked.selected.bg=#9956f5
msglist_pill.reverse=true

part_*.fg=#ffffff
part_mimetype.fg=#44475A
part_*.selected.fg=#ffffff
part_*.selected.bg=#44475A
part_filename.selected.bold=true

completion_pill.reverse=false
selector_focused.bold=false
selector_focused.bg=#44475A
diff --git a/stylesets/nord b/stylesets/nord
index e9c7e853aa9f..a1fdf882e2d1 100644
--- a/stylesets/nord
+++ b/stylesets/nord
@@ -48,6 +48,11 @@ tab.selected.fg=#2c3441
dirlist_unread.fg=#64A6B3
dirlist_recent.fg=#64A6B3

part_*.fg=#ffffff
part_mimetype.fg=#394353
part_*.selected.fg=#ffffff
part_filename.selected.bold=true

[viewer]
url.underline=true
header.bold=true
diff --git a/stylesets/pink b/stylesets/pink
index 9fd8c213feb2..deaae5ac7079 100644
--- a/stylesets/pink
+++ b/stylesets/pink
@@ -37,6 +37,12 @@ msglist_marked.bg=#de4e85
msglist_marked.selected.bg=#c93687
msglist_pill.reverse=true

part_*.fg=#ffffff
part_mimetype.fg=#de4e85
part_*.selected.fg=#ffffff
part_*.selected.bg=#de4e85
part_filename.selected.bold=true

completion_pill.reverse=true
selector_focused.bold=true
selector_focused.bg=#de4e85
diff --git a/stylesets/solarized b/stylesets/solarized
index dcd8606f7ce2..9874e2d8b098 100644
--- a/stylesets/solarized
+++ b/stylesets/solarized
@@ -33,6 +33,8 @@ tab.bg=#eee8d5
tab.fg=#586e75
tab.selected.bg=#b58900
tab.selected.fg=#002b36
part_mimetype.fg=#b58900
part_filename.selected.bold=true

[viewer]
diff_add.fg=#859900 # green
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index c94d57a3f498..41cb639e34b6 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -422,18 +422,32 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
		ps.parts[ps.selected].Draw(ctx)
		return
	}

	var styleSwitcher, styleFile, styleMime tcell.Style

	// TODO: cap height and add scrolling for messages with many parts
	ps.height = ctx.Height()
	y := ctx.Height() - height
	for i, part := range ps.parts {
		style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
		if ps.selected == i {
			style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
			styleSwitcher = ps.mv.uiConfig.GetStyleSelected(config.STYLE_PART_SWITCHER)
			styleFile = ps.mv.uiConfig.GetStyleSelected(config.STYLE_PART_FILENAME)
			styleMime = ps.mv.uiConfig.GetStyleSelected(config.STYLE_PART_MIMETYPE)
		} else {
			styleSwitcher = ps.mv.uiConfig.GetStyle(config.STYLE_PART_SWITCHER)
			styleFile = ps.mv.uiConfig.GetStyle(config.STYLE_PART_FILENAME)
			styleMime = ps.mv.uiConfig.GetStyle(config.STYLE_PART_MIMETYPE)
		}
		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', styleSwitcher)
		left := len(part.index) * 2
		name := formatMessagePart(part.part.FullMIMEType(), part.part.FileName(), ctx.Width()-left)
		ctx.Printf(left, y+i, style, "%s", name)
		if part.part.FileName() != "" {
			name := runewidth.Truncate(part.part.FileName(),
				ctx.Width()-left-1, "…")
			left += ctx.Printf(left, y+i, styleFile, "%s ", name)
		}
		t := "(" + part.part.FullMIMEType() + ")"
		t = runewidth.Truncate(t, ctx.Width()-left, "…")
		ctx.Printf(left, y+i, styleMime, "%s", t)
	}
	ps.parts[ps.selected].Draw(ctx.Subcontext(
		0, 0, ctx.Width(), ctx.Height()-height))
@@ -500,34 +514,6 @@ func (ps *PartSwitcher) Cleanup() {
	}
}

func formatMessagePart(mime, filename string, width int) string {
	lname := runewidth.StringWidth(filename)
	lmime := runewidth.StringWidth(mime)

	switch {
	case width <= 0:
		return ""

	case filename == "":
		return runewidth.Truncate(mime, width, "…")

	case lname+lmime+3 <= width:
		// simple scenario - everything fits
		return fmt.Sprintf("%s (%s)",
			runewidth.FillRight(filename, width-lmime-3), mime)

	case lname+3 < width:
		// file name fits + we have space for parentheses and at least
		// one symbol of mime
		return fmt.Sprintf("%s (%s)", filename,
			runewidth.Truncate(mime, width-lname-3, "…"))

	default:
		// ok, we don't have space even for the file name
		return runewidth.Truncate(filename, width, "…")
	}
}

func (mv *MessageViewer) Event(event tcell.Event) bool {
	return mv.switcher.Event(event)
}
diff --git a/widgets/msgviewer_test.go b/widgets/msgviewer_test.go
deleted file mode 100644
index f6d01edbdd09..000000000000
--- a/widgets/msgviewer_test.go
@@ -1,68 +0,0 @@
package widgets

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFormatMessageNoFilename(t *testing.T) {
	assert.Equal(t, "mime/type", formatMessagePart("mime/type", "", 24))
	assert.Equal(t, "m/type", formatMessagePart("m/type", "", 24))
	assert.Equal(t, "2", formatMessagePart("2", "", 24))
	assert.Equal(t, "2", formatMessagePart("2", "", 20))
}

func TestFormatMessageNoFilenameNotEnoguhSpace(t *testing.T) {
	assert.Equal(t, "mime/type", formatMessagePart("mime/type", "", 24))
	assert.Equal(t, "mime/type", formatMessagePart("mime/type", "", 9))
	assert.Equal(t, "mime/ty…", formatMessagePart("mime/type", "", 8))
	assert.Equal(t, "mime/…", formatMessagePart("mime/type", "", 6))
	assert.Equal(t, "m…", formatMessagePart("mime/type", "", 2))
	assert.Equal(t, "…", formatMessagePart("mime/type", "", 1))

	assert.Equal(t, "", formatMessagePart("mime/type", "", 0))
	assert.Equal(t, "", formatMessagePart("mime/type", "", -1))
	assert.Equal(t, "", formatMessagePart("mime/type", "", -10))
}

func TestFormatMessagePartSimpleCases(t *testing.T) {
	assert.Equal(t, "filename.doc (mime/type)", formatMessagePart("mime/type", "filename.doc", 24))
	assert.Equal(t, "имяфайла.док (mime/type)", formatMessagePart("mime/type", "имяфайла.док", 24))
	assert.Equal(t, "file.doc        (m/type)", formatMessagePart("m/type", "file.doc", 24))
	assert.Equal(t, "1                    (2)", formatMessagePart("2", "1", 24))
	assert.Equal(t, "1                (2)", formatMessagePart("2", "1", 20))
	assert.Equal(t, "1 (2)", formatMessagePart("2", "1", 5))
}

func TestFormatMessagePartNotEnoughSpaceForMime(t *testing.T) {
	assert.Equal(t, "filename.doc       (mime/type)", formatMessagePart("mime/type", "filename.doc", 30))
	assert.Equal(t, "filename.doc  (mime/type)", formatMessagePart("mime/type", "filename.doc", 25))
	assert.Equal(t, "filename.doc (mime/type)", formatMessagePart("mime/type", "filename.doc", 24))
	assert.Equal(t, "filename.doc (mime/ty…)", formatMessagePart("mime/type", "filename.doc", 23))
	assert.Equal(t, "имяфайла.док (mime/ty…)", formatMessagePart("mime/type", "имяфайла.док", 23))
	assert.Equal(t, "filename.doc (m…)", formatMessagePart("mime/type", "filename.doc", 17))
	assert.Equal(t, "filename.doc (…)", formatMessagePart("mime/type", "filename.doc", 16))
	assert.Equal(t, "имяфайла.док (…)", formatMessagePart("mime/type", "имяфайла.док", 16))
	assert.Equal(t, "filename.doc", formatMessagePart("mime/type", "filename.doc", 15))
	assert.Equal(t, "filename.doc", formatMessagePart("mime/type", "filename.doc", 14))
	assert.Equal(t, "filename.doc", formatMessagePart("mime/type", "filename.doc", 13))
	assert.Equal(t, "filename.doc", formatMessagePart("mime/type", "filename.doc", 12))
	assert.Equal(t, "имяфайла.док", formatMessagePart("mime/type", "имяфайла.док", 12))
}

func TestFormatMessagePartNotEnoughSpaceForFilename(t *testing.T) {
	assert.Equal(t, "filename.d…", formatMessagePart("mime/type", "filename.doc", 11))
	assert.Equal(t, "filename…", formatMessagePart("mime/type", "filename.doc", 9))
	assert.Equal(t, "f…", formatMessagePart("mime/type", "filename.doc", 2))
	assert.Equal(t, "…", formatMessagePart("mime/type", "filename.doc", 1))

	assert.Equal(t, "", formatMessagePart("mime/type", "filename.doc", 0))
	assert.Equal(t, "", formatMessagePart("mime/type", "filename.doc", -1))
	assert.Equal(t, "", formatMessagePart("mime/type", "filename.doc", -10))

	assert.Equal(t, "имяфайла.д…", formatMessagePart("mime/type", "имяфайла.док", 11))
	assert.Equal(t, "имяфайла…", formatMessagePart("mime/type", "имяфайла.док", 9))
	assert.Equal(t, "и…", formatMessagePart("mime/type", "имяфайла.док", 2))
	assert.Equal(t, "…", formatMessagePart("mime/type", "имяфайла.док", 1))
}
-- 
2.41.0
aerc/patches: SUCCESS in 5m8s

[Allow styling part selector MIME and filename][0] from [Robin Jarry][1]

[0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/44854
[1]: mailto:robin@jarry.cc

✓ #1059559 SUCCESS aerc/patches/openbsd.yml     https://builds.sr.ht/~rjarry/job/1059559
✓ #1059558 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1059558
Beautiful!

Tested-By: inwit <inwit@sindominio.net>