mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-01-01 07:31:13 +01:00
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>
208 lines
4.8 KiB
Go
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()
|
|
}
|
|
}
|