~sircmpwn/aerc

Add Mouseable v6 PROPOSED

Jeffas: 1
 Add Mouseable

 14 files changed, 376 insertions(+), 47 deletions(-)
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/~sircmpwn/aerc/patches/7972/mbox | git am -3
Learn more about email & git

[PATCH v6] Add Mouseable Export this patch

This adds the Mouseable interface. When this is implemented for a
component that item can accept and process mouseevents.

At the top level when a mouse event is received it is passed to the
grid's handler and then it trickles down until it reaches a component
that can actually handle it, such as the tablist, dirlist or msglist.

A mouse event is passed so that components can handle other things such
as scrolling with the mousewheel. The components themselves then perform
the necessary actions.

Clicking emails in the messagelist opens them in a new tab.

Textinputs can be clicked to position the cursor inside them.

Mouseevents are not forwarded to the terminal at the moment.

Elements which do not handle mouse events are not required to implement
the Mouseable interface.
---

This version drops support for mousing in the compose area which is
tricky and will be left for another separate patch as it could be
lengthy and I haven't thought of a good way yet.

This also takes into account the scroll of the messagelist when
clicking.

 lib/ui/borders.go         |   7 +++
 lib/ui/grid.go            |  41 +++++++++++++++
 lib/ui/interfaces.go      |  15 ++++--
 lib/ui/stack.go           |   9 ++++
 lib/ui/tab.go             | 102 ++++++++++++++++++++++++++++++--------
 lib/ui/textinput.go       |  14 ++++++
 widgets/account-wizard.go |   2 +-
 widgets/account.go        |  11 +++-
 widgets/aerc.go           |  41 +++++++++------
 widgets/compose.go        |  21 ++++++--
 widgets/dirlist.go        |  29 +++++++++++
 widgets/msglist.go        |  45 ++++++++++++++++-
 widgets/msgviewer.go      |  72 ++++++++++++++++++++++++++-
 widgets/terminal.go       |  14 ++++++
 14 files changed, 376 insertions(+), 47 deletions(-)

diff --git a/lib/ui/borders.go b/lib/ui/borders.go
index cffd3ca..7a75759 100644
--- a/lib/ui/borders.go
+++ b/lib/ui/borders.go
@@ -66,3 +66,10 @@ func (bordered *Bordered) Draw(ctx *Context) {
	subctx := ctx.Subcontext(x, y, width, height)
	bordered.content.Draw(subctx)
}

func (bordered *Bordered) MouseEvent(localX int, localY int, event tcell.Event) {
	switch content := bordered.content.(type) {
	case Mouseable:
		content.MouseEvent(localX, localY, event)
	}
}
diff --git a/lib/ui/grid.go b/lib/ui/grid.go
index 7f131bd..b47c6bd 100644
--- a/lib/ui/grid.go
+++ b/lib/ui/grid.go
@@ -5,6 +5,8 @@ import (
	"math"
	"sync"
	"sync/atomic"

	"github.com/gdamore/tcell"
)

type Grid struct {
@@ -141,6 +143,45 @@ func (grid *Grid) Draw(ctx *Context) {
	}
}

func (grid *Grid) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		invalid := grid.invalid

		grid.mutex.RLock()
		defer grid.mutex.RUnlock()

		for _, cell := range grid.cells {
			cellInvalid := cell.invalid.Load().(bool)
			if !cellInvalid && !invalid {
				continue
			}
			rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan]
			cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan]
			x := cols[0].Offset
			y := rows[0].Offset
			width := 0
			height := 0
			for _, col := range cols {
				width += col.Size
			}
			for _, row := range rows {
				height += row.Size
			}
			if x <= localX && localX < x+width && y <= localY && localY < y+height {
				switch content := cell.Content.(type) {
				case MouseableDrawableInteractive:
					content.MouseEvent(localX-x, localY-y, event)
				case Mouseable:
					content.MouseEvent(localX-x, localY-y, event)
				case MouseHandler:
					content.MouseEvent(localX-x, localY-y, event)
				}
			}
		}
	}
}

func (grid *Grid) reflow(ctx *Context) {
	grid.rowLayout = nil
	grid.columnLayout = nil
diff --git a/lib/ui/interfaces.go b/lib/ui/interfaces.go
index 2f63424..9e79571 100644
--- a/lib/ui/interfaces.go
+++ b/lib/ui/interfaces.go
@@ -50,9 +50,18 @@ type Container interface {
	Children() []Drawable
}

// A drawable that can be clicked
type Clickable interface {
type MouseHandler interface {
	// Handle a mouse event which occurred at the local x and y positions
	MouseEvent(localX int, localY int, event tcell.Event)
}

// A drawable that can be interacted with by the mouse
type Mouseable interface {
	Drawable
	MouseHandler
}

	MouseEvent(event tcell.Event)
type MouseableDrawableInteractive interface {
	DrawableInteractive
	MouseHandler
}
diff --git a/lib/ui/stack.go b/lib/ui/stack.go
index 75cc780..690a869 100644
--- a/lib/ui/stack.go
+++ b/lib/ui/stack.go
@@ -37,6 +37,15 @@ func (stack *Stack) Draw(ctx *Context) {
	}
}

func (stack *Stack) MouseEvent(localX int, localY int, event tcell.Event) {
	if len(stack.children) > 0 {
		switch element := stack.Peek().(type) {
		case Mouseable:
			element.MouseEvent(localX, localY, event)
		}
	}
}

func (stack *Stack) Push(d Drawable) {
	if len(stack.children) != 0 {
		stack.Peek().OnInvalidate(nil)
diff --git a/lib/ui/tab.go b/lib/ui/tab.go
index 90c7ce9..1fd2b80 100644
--- a/lib/ui/tab.go
+++ b/lib/ui/tab.go
@@ -14,6 +14,9 @@ type Tabs struct {

	onInvalidateStrip   func(d Drawable)
	onInvalidateContent func(d Drawable)

	parent   *Tabs
	CloseTab func(index int)
}

type Tab struct {
@@ -28,7 +31,9 @@ type TabContent Tabs
func NewTabs() *Tabs {
	tabs := &Tabs{}
	tabs.TabStrip = (*TabStrip)(tabs)
	tabs.TabStrip.parent = tabs
	tabs.TabContent = (*TabContent)(tabs)
	tabs.TabContent.parent = tabs
	tabs.history = []int{}
	return tabs
}
@@ -114,6 +119,22 @@ func (tabs *Tabs) SelectPrevious() bool {
	return true
}

func (tabs *Tabs) NextTab() {
	next := tabs.Selected + 1
	if next >= len(tabs.Tabs) {
		next = 0
	}
	tabs.Select(next)
}

func (tabs *Tabs) PrevTab() {
	next := tabs.Selected - 1
	if next < 0 {
		next = len(tabs.Tabs) - 1
	}
	tabs.Select(next)
}

func (tabs *Tabs) pushHistory(index int) {
	tabs.history = append(tabs.history, index)
}
@@ -146,19 +167,6 @@ func (tabs *Tabs) removeHistory(index int) {
	tabs.history = newHist
}

func (tabs *Tabs) MouseEvent(event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		if event.Buttons()&tcell.Button1 != 0 {
			x, y := event.Position()
			selectedTab, ok := tabs.TabStrip.Clicked(x, y)
			if ok {
				tabs.Select(selectedTab)
			}
		}
	}
}

// TODO: Color repository
func (strip *TabStrip) Draw(ctx *Context) {
	x := 0
@@ -187,21 +195,65 @@ func (strip *TabStrip) Invalidate() {
	}
}

func (strip *TabStrip) MouseEvent(localX int, localY int, event tcell.Event) {
	changeFocus := func(focus bool) {
		interactive, ok := strip.parent.Tabs[strip.parent.Selected].Content.(Interactive)
		if ok {
			interactive.Focus(focus)
		}
	}
	unfocus := func() { changeFocus(false) }
	refocus := func() { changeFocus(true) }
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			selectedTab, ok := strip.Clicked(localX, localY)
			if !ok || selectedTab == strip.parent.Selected {
				return
			}
			unfocus()
			strip.parent.Select(selectedTab)
			refocus()
		case tcell.WheelDown:
			unfocus()
			strip.parent.NextTab()
			refocus()
		case tcell.WheelUp:
			unfocus()
			strip.parent.PrevTab()
			refocus()
		case tcell.Button3:
			selectedTab, ok := strip.Clicked(localX, localY)
			if !ok {
				return
			}
			unfocus()
			if selectedTab == strip.parent.Selected {
				strip.parent.CloseTab(selectedTab)
			} else {
				current := strip.parent.Selected
				strip.parent.CloseTab(selectedTab)
				strip.parent.Select(current)
			}
			refocus()
		}
	}
}

func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) {
	strip.onInvalidateStrip = onInvalidate
}

func (strip *TabStrip) Clicked(mouseX int, mouseY int) (int, bool) {
	x := 0
	if mouseY == 0 {
		for i, tab := range strip.Tabs {
			trunc := runewidth.Truncate(tab.Name, 32, "…")
			length := len(trunc) + 2
			if x <= mouseX && mouseX < x+length {
				return i, true
			}
			x += length
	for i, tab := range strip.Tabs {
		trunc := runewidth.Truncate(tab.Name, 32, "…")
		length := len(trunc) + 2
		if x <= mouseX && mouseX < x+length {
			return i, true
		}
		x += length
	}
	return 0, false
}
@@ -225,6 +277,14 @@ func (content *TabContent) Draw(ctx *Context) {
	tab.Content.Draw(ctx)
}

func (content *TabContent) MouseEvent(localX int, localY int, event tcell.Event) {
	tab := content.Tabs[content.Selected]
	switch tabContent := tab.Content.(type) {
	case Mouseable:
		tabContent.MouseEvent(localX, localY, event)
	}
}

func (content *TabContent) Invalidate() {
	if content.onInvalidateContent != nil {
		content.onInvalidateContent(content)
diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go
index 00e91ee..3935173 100644
--- a/lib/ui/textinput.go
+++ b/lib/ui/textinput.go
@@ -97,6 +97,20 @@ func (ti *TextInput) Draw(ctx *Context) {
	}
}

func (ti *TextInput) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			if localX >= len(ti.prompt)+1 && localX <= len(ti.text[ti.scroll:])+len(ti.prompt)+1 {
				ti.index = localX - len(ti.prompt) - 1
				ti.ensureScroll()
				ti.Invalidate()
			}
		}
	}
}

func (ti *TextInput) Focus(focus bool) {
	ti.focus = focus
	if focus && ti.ctx != nil {
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 5acd26c..904013f 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -523,7 +523,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
	}
	wizard.conf.Accounts = append(wizard.conf.Accounts, account)

	view := NewAccountView(wizard.conf, &account,
	view := NewAccountView(wizard.aerc, wizard.conf, &account,
		wizard.aerc.logger, wizard.aerc)
	wizard.aerc.accounts[account.Name] = view
	wizard.aerc.NewTab(view, account.Name)
diff --git a/widgets/account.go b/widgets/account.go
index 688b660..1220753 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -17,6 +17,7 @@ import (

type AccountView struct {
	acct    *config.AccountConfig
	aerc    *Aerc
	conf    *config.AercConfig
	dirlist *DirectoryList
	grid    *ui.Grid
@@ -26,7 +27,7 @@ type AccountView struct {
	worker  *types.Worker
}

func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig,
	logger *log.Logger, host TabHost) *AccountView {

	grid := ui.NewGrid().Rows([]ui.GridSpec{
@@ -42,6 +43,7 @@ func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
			Color(tcell.ColorDefault, tcell.ColorRed)
		return &AccountView{
			acct:   acct,
			aerc:   aerc,
			grid:   grid,
			host:   host,
			logger: logger,
@@ -53,11 +55,12 @@ func NewAccountView(conf *config.AercConfig, acct *config.AccountConfig,
		grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT))
	}

	msglist := NewMessageList(conf, logger)
	msglist := NewMessageList(conf, logger, aerc)
	grid.AddChild(msglist).At(0, 1)

	view := &AccountView{
		acct:    acct,
		aerc:    aerc,
		conf:    conf,
		dirlist: dirlist,
		grid:    grid,
@@ -124,6 +127,10 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
	acct.grid.Draw(ctx)
}

func (acct *AccountView) MouseEvent(localX int, localY int, event tcell.Event) {
	acct.grid.MouseEvent(localX, localY, event)
}

func (acct *AccountView) Focus(focus bool) {
	// TODO: Unfocus children I guess
}
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 87009cd..fe3c1e2 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -74,7 +74,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
	conf.Triggers.ExecuteCommand = cmd

	for i, acct := range conf.Accounts {
		view := NewAccountView(conf, &conf.Accounts[i], logger, aerc)
		view := NewAccountView(aerc, conf, &conf.Accounts[i], logger, aerc)
		aerc.accounts[acct.Name] = view
		tabs.Add(view, acct.Name)
	}
@@ -85,6 +85,22 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
		aerc.NewTab(wizard, "New account")
	}

	tabs.CloseTab = func(index int) {
		switch content := aerc.tabs.Tabs[index].Content.(type) {
		case *AccountView:
			return
		case *AccountWizard:
			return
		case *Composer:
			aerc.RemoveTab(content)
			content.Close()
		case *Terminal:
			content.Close(nil)
		case *MessageViewer:
			aerc.RemoveTab(content)
		}
	}

	return aerc
}

@@ -235,7 +251,12 @@ func (aerc *Aerc) Event(event tcell.Event) bool {
			return false
		}
	case *tcell.EventMouse:
		aerc.tabs.MouseEvent(event)
		if event.Buttons() == tcell.ButtonNone {
			return false
		}
		x, y := event.Position()
		aerc.grid.MouseEvent(x, y, event)
		return true
	}
	return false
}
@@ -260,8 +281,8 @@ func (aerc *Aerc) SelectedTab() ui.Drawable {
	return aerc.tabs.Tabs[aerc.tabs.Selected].Content
}

func (aerc *Aerc) NewTab(drawable ui.Drawable, name string) *ui.Tab {
	tab := aerc.tabs.Add(drawable, name)
func (aerc *Aerc) NewTab(clickable ui.Drawable, name string) *ui.Tab {
	tab := aerc.tabs.Add(clickable, name)
	aerc.tabs.Select(len(aerc.tabs.Tabs) - 1)
	return tab
}
@@ -275,19 +296,11 @@ func (aerc *Aerc) ReplaceTab(tabSrc ui.Drawable, tabTarget ui.Drawable, name str
}

func (aerc *Aerc) NextTab() {
	next := aerc.tabs.Selected + 1
	if next >= len(aerc.tabs.Tabs) {
		next = 0
	}
	aerc.tabs.Select(next)
	aerc.tabs.NextTab()
}

func (aerc *Aerc) PrevTab() {
	next := aerc.tabs.Selected - 1
	if next < 0 {
		next = len(aerc.tabs.Tabs) - 1
	}
	aerc.tabs.Select(next)
	aerc.tabs.PrevTab()
}

func (aerc *Aerc) SelectTab(name string) bool {
diff --git a/widgets/compose.go b/widgets/compose.go
index bd4301a..0e7f09e 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -40,10 +40,12 @@ type Composer struct {
	worker      *types.Worker

	layout    HeaderLayout
	focusable []ui.DrawableInteractive
	focusable []ui.MouseableDrawableInteractive
	focused   int

	onClose []func(ti *Composer)

	width int
}

func NewComposer(conf *config.AercConfig,
@@ -87,10 +89,10 @@ func NewComposer(conf *config.AercConfig,
func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (
	newLayout HeaderLayout,
	editors map[string]*headerEditor,
	focusable []ui.DrawableInteractive,
	focusable []ui.MouseableDrawableInteractive,
) {
	editors = make(map[string]*headerEditor)
	focusable = make([]ui.DrawableInteractive, 0)
	focusable = make([]ui.MouseableDrawableInteractive, 0)

	for _, row := range layout {
		for _, h := range row {
@@ -99,7 +101,7 @@ func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (
			switch h {
			case "From":
				// Prepend From to support backtab
				focusable = append([]ui.DrawableInteractive{e}, focusable...)
				focusable = append([]ui.MouseableDrawableInteractive{e}, focusable...)
			default:
				focusable = append(focusable, e)
			}
@@ -176,6 +178,7 @@ func (c *Composer) OnClose(fn func(composer *Composer)) {
}

func (c *Composer) Draw(ctx *ui.Context) {
	c.width = ctx.Width()
	c.grid.Draw(ctx)
}

@@ -617,6 +620,16 @@ func (he *headerEditor) Draw(ctx *ui.Context) {
	he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
}

func (he *headerEditor) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		width := runewidth.StringWidth(he.name + " ")
		if localX >= width {
			he.input.MouseEvent(localX-width, localY, event)
		}
	}
}

func (he *headerEditor) Invalidate() {
	he.input.Invalidate()
}
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 33119dd..ec73082 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -137,6 +137,35 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
	}
}

func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			clickedDir, ok := dirlist.Clicked(localX, localY)
			if ok {
				dirlist.Select(clickedDir)
			}
		case tcell.WheelDown:
			dirlist.Next()
		case tcell.WheelUp:
			dirlist.Prev()
		}
	}
}

func (dirlist *DirectoryList) Clicked(x int, y int) (string, bool) {
	if dirlist.dirs == nil || len(dirlist.dirs) == 0 {
		return "", false
	}
	for i, name := range dirlist.dirs {
		if i == y {
			return name, true
		}
	}
	return "", false
}

func (dirlist *DirectoryList) NextPrev(delta int) {
	curIdx := sort.SearchStrings(dirlist.dirs, dirlist.selected)
	if curIdx == len(dirlist.dirs) {
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 8ed716b..b7c921c 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -25,6 +25,7 @@ type MessageList struct {
	spinner       *Spinner
	store         *lib.MessageStore
	isInitalizing bool
	aerc          *Aerc
}

type msgSorter struct {
@@ -55,12 +56,13 @@ func (s *msgSorter) Swap(i, j int) {
	s.uids[j] = tmp
}

func NewMessageList(conf *config.AercConfig, logger *log.Logger) *MessageList {
func NewMessageList(conf *config.AercConfig, logger *log.Logger, aerc *Aerc) *MessageList {
	ml := &MessageList{
		conf:          conf,
		logger:        logger,
		spinner:       NewSpinner(&conf.Ui),
		isInitalizing: true,
		aerc:          aerc,
	}
	ml.spinner.OnInvalidate(func(_ ui.Drawable) {
		ml.Invalidate()
@@ -161,6 +163,47 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
	}
}

func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			if ml.aerc == nil {
				return
			}
			selectedMsg, ok := ml.Clicked(localX, localY)
			if ok {
				ml.Select(selectedMsg)
				acct := ml.aerc.SelectedAccount()
				if acct.Messages().Empty() {
					return
				}
				store := acct.Messages().Store()
				msg := acct.Messages().Selected()
				if msg == nil {
					return
				}
				viewer := NewMessageViewer(acct, ml.aerc.Config(), store, msg)
				ml.aerc.NewTab(viewer, msg.Envelope.Subject)
			}
		case tcell.WheelDown:
			ml.store.Next()
			ml.Scroll()
		case tcell.WheelUp:
			ml.store.Prev()
			ml.Scroll()
		}
	}
}

func (ml *MessageList) Clicked(x, y int) (int, bool) {
	store := ml.Store()
	if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
		return 0, false
	}
	return y + ml.scroll, true
}

func (ml *MessageList) Height() int {
	return ml.height
}
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index e210616..c179070 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -42,6 +42,9 @@ type PartSwitcher struct {
	selected       int
	showHeaders    bool
	alwaysShowMime bool

	height int
	mv     *MessageViewer
}

func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
@@ -77,7 +80,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
	grid.AddChild(header).At(0, 0)
	grid.AddChild(switcher).At(1, 0)

	return &MessageViewer{
	mv := &MessageViewer{
		acct:     acct,
		conf:     conf,
		grid:     grid,
@@ -85,6 +88,9 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
		store:    store,
		switcher: switcher,
	}
	switcher.mv = mv

	return mv
}

func fmtHeader(msg *models.MessageInfo, header string) string {
@@ -194,6 +200,13 @@ func (mv *MessageViewer) Draw(ctx *ui.Context) {
	mv.grid.Draw(ctx)
}

func (mv *MessageViewer) MouseEvent(localX int, localY int, event tcell.Event) {
	if mv.err != nil {
		return
	}
	mv.grid.MouseEvent(localX, localY, event)
}

func (mv *MessageViewer) Invalidate() {
	mv.grid.Invalidate()
}
@@ -295,6 +308,7 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
		return
	}
	// 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 := tcell.StyleDefault.Reverse(ps.selected == i)
@@ -311,6 +325,62 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
		0, 0, ctx.Width(), ctx.Height()-height))
}

func (ps *PartSwitcher) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		switch event.Buttons() {
		case tcell.Button1:
			height := len(ps.parts)
			y := ps.height - height
			if localY < y {
				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
			}
			for i, _ := range ps.parts {
				if localY != y+i {
					continue
				}
				if ps.parts[i].part.MIMEType == "multipart" {
					continue
				}
				if ps.parts[ps.selected].term != nil {
					ps.parts[ps.selected].term.Focus(false)
				}
				ps.selected = i
				ps.Invalidate()
				if ps.parts[ps.selected].term != nil {
					ps.parts[ps.selected].term.Focus(true)
				}
			}
		case tcell.WheelDown:
			height := len(ps.parts)
			y := ps.height - height
			if localY < y {
				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
			}
			if ps.parts[ps.selected].term != nil {
				ps.parts[ps.selected].term.Focus(false)
			}
			ps.mv.NextPart()
			if ps.parts[ps.selected].term != nil {
				ps.parts[ps.selected].term.Focus(true)
			}
		case tcell.WheelUp:
			height := len(ps.parts)
			y := ps.height - height
			if localY < y {
				ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
			}
			if ps.parts[ps.selected].term != nil {
				ps.parts[ps.selected].term.Focus(false)
			}
			ps.mv.PreviousPart()
			if ps.parts[ps.selected].term != nil {
				ps.parts[ps.selected].term.Focus(true)
			}
		}
	}
}

func (mv *MessageViewer) Event(event tcell.Event) bool {
	return mv.switcher.Event(event)
}
diff --git a/widgets/terminal.go b/widgets/terminal.go
index 008a36f..6ad6904 100644
--- a/widgets/terminal.go
+++ b/widgets/terminal.go
@@ -311,6 +311,20 @@ func (term *Terminal) Draw(ctx *ui.Context) {
	}
}

func (term *Terminal) MouseEvent(localX int, localY int, event tcell.Event) {
	switch event := event.(type) {
	case *tcell.EventMouse:
		if term.OnEvent != nil {
			if term.OnEvent(event) {
				return
			}
		}
		if term.closed {
			return
		}
	}
}

func (term *Terminal) Focus(focus bool) {
	if term.closed {
		return
--
2.23.0
This one looks good. Thanks for all of your hard work!

To git.sr.ht:~sircmpwn/aerc
   a441e3b..f6216bb  master -> master
View this thread in the archives