summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--lib/ui/borders.go7
-rw-r--r--lib/ui/grid.go41
-rw-r--r--lib/ui/interfaces.go15
-rw-r--r--lib/ui/stack.go9
-rw-r--r--lib/ui/tab.go102
-rw-r--r--lib/ui/textinput.go14
-rw-r--r--widgets/account-wizard.go2
-rw-r--r--widgets/account.go11
-rw-r--r--widgets/aerc.go41
-rw-r--r--widgets/compose.go21
-rw-r--r--widgets/dirlist.go29
-rw-r--r--widgets/msglist.go45
-rw-r--r--widgets/msgviewer.go72
-rw-r--r--widgets/terminal.go14
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