1
0
Fork 0
mirror of https://git.sr.ht/~rjarry/aerc synced 2026-01-01 07:31:13 +01:00
aerc/app/partswitcher.go
Terrance 1dd2508124 config: add contextual viewer configuration
This allows setting viewer options for specific message senders or
subjects via conditional `viewer` blocks in the main configuration file,
similar to contextual UI configuration.  Each block can target the
envelope sender and the From/Subject headers, as either an exact match
with `=` (on the email address for sender/from) or a regex with `~` (on
the full "$name <$email>" string representation).

For example, some senders like to include a useless plain text part in
their alternatives, telling you that your client doesn't support HTML.
The documented example would prefer plain text where present by default,
but switch to the HTML part for such senders.

Signed-off-by: Terrance <git@terrance.allofti.me>
Reviewed-by: Julio B <julio.bacel@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2025-10-30 21:17:44 +01:00

208 lines
4.8 KiB
Go

package app
import (
"math"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rockorager/vaxis"
"github.com/mattn/go-runewidth"
)
type PartSwitcher struct {
Scrollable
parts []*PartViewer
selected int
height int
offset int
uiConfig *config.UIConfig
}
func (ps *PartSwitcher) PreviousPart() {
for {
ps.selected--
if ps.selected < 0 {
ps.selected = len(ps.parts) - 1
}
if ps.parts[ps.selected].part.MIMEType != "multipart" {
break
}
}
}
func (ps *PartSwitcher) NextPart() {
for {
ps.selected++
if ps.selected >= len(ps.parts) {
ps.selected = 0
}
if ps.parts[ps.selected].part.MIMEType != "multipart" {
break
}
}
}
func (ps *PartSwitcher) SelectedPart() *PartViewer {
return ps.parts[ps.selected]
}
func (ps *PartSwitcher) AttachmentParts(all bool) []*PartInfo {
var attachments []*PartInfo
for _, p := range ps.parts {
if p.part.Disposition == "attachment" || (all && p.part.FileName() != "") {
pi := &PartInfo{
Index: p.index,
Msg: p.msg.MessageInfo(),
Part: p.part,
}
attachments = append(attachments, pi)
}
}
return attachments
}
func (ps *PartSwitcher) Invalidate() {
ui.Invalidate()
}
func (ps *PartSwitcher) Focus(focus bool) {
if ps.parts[ps.selected].term != nil {
ps.parts[ps.selected].term.Focus(focus)
}
}
func (ps *PartSwitcher) Show(visible bool) {
if ps.parts[ps.selected].term != nil {
ps.parts[ps.selected].term.Show(visible)
}
}
func (ps *PartSwitcher) Event(event vaxis.Event) bool {
return ps.parts[ps.selected].Event(event)
}
func (ps *PartSwitcher) Draw(ctx *ui.Context) {
uiConfig := ps.uiConfig
n := len(ps.parts)
part := ps.parts[ps.selected]
if n == 1 && !part.viewerConfig().AlwaysShowMime {
part.Draw(ctx)
return
}
ps.height = part.viewerConfig().MaxMimeHeight
if ps.height <= 0 || n < ps.height {
ps.height = n
}
if ps.height > ctx.Height()/2 {
ps.height = ctx.Height() / 2
}
ps.UpdateScroller(ps.height, n)
ps.EnsureScroll(ps.selected)
var styleSwitcher, styleFile, styleMime vaxis.Style
scrollbarWidth := 0
if ps.NeedScrollbar() {
scrollbarWidth = 1
}
ps.offset = ctx.Height() - ps.height
y := ps.offset
row := ps.offset
ctx.Fill(0, y, ctx.Width(), ps.height, ' ', uiConfig.GetStyle(config.STYLE_PART_SWITCHER))
for i := ps.Scroll(); i < n; i++ {
part := ps.parts[i]
if ps.selected == i {
styleSwitcher = uiConfig.GetStyleSelected(config.STYLE_PART_SWITCHER)
styleFile = uiConfig.GetStyleSelected(config.STYLE_PART_FILENAME)
styleMime = uiConfig.GetStyleSelected(config.STYLE_PART_MIMETYPE)
} else {
styleSwitcher = uiConfig.GetStyle(config.STYLE_PART_SWITCHER)
styleFile = uiConfig.GetStyle(config.STYLE_PART_FILENAME)
styleMime = uiConfig.GetStyle(config.STYLE_PART_MIMETYPE)
}
ctx.Fill(0, row, ctx.Width(), 1, ' ', styleSwitcher)
left := len(part.index) * 2
if part.part.FileName() != "" {
name := runewidth.Truncate(part.part.FileName(),
ctx.Width()-left-1, "…")
left += ctx.Printf(left, row, styleFile, "%s ", name)
}
t := "(" + part.part.FullMIMEType() + ")"
t = runewidth.Truncate(t, ctx.Width()-left-scrollbarWidth, "…")
ctx.Printf(left, row, styleMime, "%s", t)
row++
if (i - ps.Scroll()) >= ps.height {
break
}
}
if ps.NeedScrollbar() {
ps.drawScrollbar(ctx.Subcontext(ctx.Width()-1, y, 1, ps.height))
}
ps.parts[ps.selected].Draw(ctx.Subcontext(
0, 0, ctx.Width(), ctx.Height()-ps.height))
}
func (ps *PartSwitcher) drawScrollbar(ctx *ui.Context) {
uiConfig := ps.uiConfig
gutterStyle := uiConfig.GetStyle(config.STYLE_MSGLIST_GUTTER)
pillStyle := uiConfig.GetStyle(config.STYLE_MSGLIST_PILL)
// gutter
ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle)
// pill
pillSize := int(math.Ceil(float64(ctx.Height()) * ps.PercentVisible()))
pillOffset := int(math.Floor(float64(ctx.Height()) * ps.PercentScrolled()))
ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
}
func (ps *PartSwitcher) MouseEvent(localX int, localY int, event vaxis.Event) {
if localY < ps.offset && ps.parts[ps.selected].term != nil {
ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
return
}
e, ok := event.(vaxis.Mouse)
if !ok {
return
}
if ps.parts[ps.selected].term != nil {
ps.parts[ps.selected].term.Focus(false)
}
switch e.Button {
case vaxis.MouseLeftButton:
i := localY - ps.offset + ps.Scroll()
if i < 0 || i >= len(ps.parts) {
break
}
if ps.parts[i].part.MIMEType == "multipart" {
break
}
ps.selected = i
ps.Invalidate()
case vaxis.MouseWheelDown:
ps.NextPart()
ps.Invalidate()
case vaxis.MouseWheelUp:
ps.PreviousPart()
ps.Invalidate()
}
if ps.parts[ps.selected].term != nil {
ps.parts[ps.selected].term.Focus(true)
}
}
func (ps *PartSwitcher) Cleanup() {
for _, partViewer := range ps.parts {
partViewer.Cleanup()
}
}