aerc/widgets/terminal.go

215 lines
4.3 KiB
Go
Raw Normal View History

2019-03-17 19:02:33 +01:00
package widgets
import (
"os/exec"
"syscall"
2019-03-17 19:02:33 +01:00
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging"
tcellterm "git.sr.ht/~rockorager/tcell-term"
2019-03-17 19:02:33 +01:00
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
2019-03-17 19:02:33 +01:00
)
type Terminal struct {
ui.Invalidatable
closed bool
cmd *exec.Cmd
ctx *ui.Context
cursorShown bool
destroyed bool
focus bool
vterm *tcellterm.Terminal
running bool
widgets/terminal: fix damage race condition Terminal.damage is accessed when drawing and when invalidating the widget. For this reason we need to protect it with a mutex. This seems to fix various damage issues I've been experiencing (where some regions of the terminal weren't correctly repainted). Race detector trace: Read at 0x00c0000c6670 by main goroutine: git.sr.ht/~sircmpwn/aerc/widgets.(*Terminal).Draw() /home/simon/src/aerc/widgets/terminal.go:292 +0x191 git.sr.ht/~sircmpwn/aerc/lib/ui.(*Grid).Draw() /home/simon/src/aerc/lib/ui/grid.go:117 +0x575 git.sr.ht/~sircmpwn/aerc/lib/ui.(*Grid).Draw() /home/simon/src/aerc/lib/ui/grid.go:117 +0x575 git.sr.ht/~sircmpwn/aerc/widgets.(*MessageViewer).Draw() /home/simon/src/aerc/widgets/msgviewer.go:231 +0x253 git.sr.ht/~sircmpwn/aerc/lib/ui.(*TabContent).Draw() /home/simon/src/aerc/lib/ui/tab.go:124 +0x12e git.sr.ht/~sircmpwn/aerc/lib/ui.(*Grid).Draw() /home/simon/src/aerc/lib/ui/grid.go:117 +0x575 git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Draw() /home/simon/src/aerc/widgets/aerc.go:95 +0x5a git.sr.ht/~sircmpwn/aerc/lib/ui.(*UI).Tick() /home/simon/src/aerc/lib/ui/ui.go:93 +0x1dd main.main() /home/simon/src/aerc/aerc.go:105 +0x539 Previous write at 0x00c0000c6670 by goroutine 37: git.sr.ht/~sircmpwn/aerc/widgets.(*Terminal).onDamage-fm() /home/simon/src/aerc/widgets/terminal.go:429 +0x131 git.sr.ht/~sircmpwn/go-libvterm._go_handle_damage() /home/simon/go/pkg/mod/git.sr.ht/~sircmpwn/go-libvterm@v0.0.0-20190421201021-3184f6f13687/vterm.go:481 +0xf9 git.sr.ht/~sircmpwn/go-libvterm._cgoexpwrap_5e22200b58b7__go_handle_damage() _cgo_gotypes.go:731 +0x58 runtime.call32() /usr/lib/go/src/runtime/asm_amd64.s:519 +0x3a git.sr.ht/~sircmpwn/go-libvterm.(*VTerm).Write.func1() /home/simon/go/pkg/mod/git.sr.ht/~sircmpwn/go-libvterm@v0.0.0-20190421201021-3184f6f13687/vterm.go:329 +0x9d git.sr.ht/~sircmpwn/go-libvterm.(*VTerm).Write() /home/simon/go/pkg/mod/git.sr.ht/~sircmpwn/go-libvterm@v0.0.0-20190421201021-3184f6f13687/vterm.go:329 +0x7f git.sr.ht/~sircmpwn/aerc/widgets.NewTerminal.func1() /home/simon/src/aerc/widgets/terminal.go:131 +0x18c Goroutine 37 (running) created at: git.sr.ht/~sircmpwn/aerc/widgets.NewTerminal() /home/simon/src/aerc/widgets/terminal.go:121 +0x23f git.sr.ht/~sircmpwn/aerc/widgets.NewMessageViewer() /home/simon/src/aerc/widgets/msgviewer.go:147 +0xfbe git.sr.ht/~sircmpwn/aerc/commands/account.ViewMessage() /home/simon/src/aerc/commands/account/view-message.go:26 +0x4a4 git.sr.ht/~sircmpwn/aerc/commands.(*Commands).ExecuteCommand() /home/simon/src/aerc/commands/commands.go:47 +0x1f0 main.main.func1() /home/simon/src/aerc/aerc.go:76 +0x205 git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).BeginExCommand.func1() /home/simon/src/aerc/widgets/aerc.go:262 +0x89 git.sr.ht/~sircmpwn/aerc/widgets.(*ExLine).Event() /home/simon/src/aerc/widgets/exline.go:47 +0x222 git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event() /home/simon/src/aerc/widgets/aerc.go:133 +0x83c git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).simulate() /home/simon/src/aerc/widgets/aerc.go:126 +0x12a git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event() /home/simon/src/aerc/widgets/aerc.go:148 +0x766 git.sr.ht/~sircmpwn/aerc/lib/ui.(*UI).Tick() /home/simon/src/aerc/lib/ui/ui.go:86 +0x11b main.main() /home/simon/src/aerc/aerc.go:105 +0x539
2019-05-19 13:40:05 +02:00
2019-03-17 22:23:53 +01:00
OnClose func(err error)
OnEvent func(event tcell.Event) bool
2019-03-30 16:58:24 +01:00
OnStart func()
2019-03-17 22:08:54 +01:00
OnTitle func(title string)
2019-03-17 19:02:33 +01:00
}
func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
term := &Terminal{
cursorShown: true,
}
2019-03-17 19:02:33 +01:00
term.cmd = cmd
term.vterm = tcellterm.New()
2019-03-17 19:02:33 +01:00
return term, nil
}
2019-03-17 22:23:53 +01:00
func (term *Terminal) Close(err error) {
2019-03-30 16:58:24 +01:00
if term.closed {
return
}
// Stop receiving events
term.vterm.Unwatch(term)
2019-03-17 22:23:53 +01:00
if term.cmd != nil && term.cmd.Process != nil {
err := term.cmd.Process.Kill()
if err != nil {
logging.Warnf("failed to kill process: %v", err)
}
// Race condition here, check if cmd exists. If process exits
// fast, this could by nil and panic
if term.cmd != nil {
err = term.cmd.Wait()
}
if err != nil {
logging.Warnf("failed for wait for process to terminate: %v", err)
}
2019-03-17 22:23:53 +01:00
term.cmd = nil
}
if term.vterm != nil {
term.vterm.Close()
}
2019-03-17 22:23:53 +01:00
if !term.closed && term.OnClose != nil {
term.OnClose(err)
2019-03-17 19:02:33 +01:00
}
if term.ctx != nil {
term.ctx.HideCursor()
}
term.closed = true
2019-03-17 19:02:33 +01:00
}
2019-03-30 21:29:52 +01:00
func (term *Terminal) Destroy() {
if term.destroyed {
return
}
if term.ctx != nil {
term.ctx.HideCursor()
}
// If we destroy, we don't want to call the OnClose callback
term.OnClose = nil
term.Close(nil)
term.vterm = nil
2019-03-30 21:29:52 +01:00
term.destroyed = true
}
2019-03-17 19:02:33 +01:00
func (term *Terminal) Invalidate() {
2019-04-15 22:07:05 +02:00
term.invalidate()
}
func (term *Terminal) invalidate() {
term.DoInvalidate(term)
2019-03-17 19:02:33 +01:00
}
func (term *Terminal) Draw(ctx *ui.Context) {
2019-03-30 21:29:52 +01:00
if term.destroyed {
2019-03-17 22:23:53 +01:00
return
}
2019-03-30 21:29:52 +01:00
term.ctx = ctx // gross
term.vterm.SetView(ctx.View())
if !term.running && !term.closed && term.cmd != nil {
go func() {
defer logging.PanicHandler()
term.vterm.Watch(term)
attr := &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1}
if err := term.vterm.RunWithAttrs(term.cmd, attr); err != nil {
logging.Errorf("error running terminal: %w", err)
2019-03-30 21:29:52 +01:00
term.Close(err)
term.running = false
2019-03-30 21:29:52 +01:00
return
}
term.running = false
term.Close(nil)
}()
for {
if term.cmd.Process != nil {
term.running = true
break
}
2019-03-30 16:58:24 +01:00
}
if term.OnStart != nil {
term.OnStart()
2019-03-17 19:02:33 +01:00
}
}
term.draw()
}
func (term *Terminal) draw() {
term.vterm.Draw()
if term.focus && !term.closed && term.ctx != nil {
if !term.cursorShown {
term.ctx.HideCursor()
} else {
_, x, y, style := term.vterm.GetCursor()
term.ctx.SetCursor(x, y)
term.ctx.SetCursorStyle(style)
}
}
2019-03-17 19:02:33 +01:00
}
func (term *Terminal) MouseEvent(localX int, localY int, event tcell.Event) {
ev, ok := event.(*tcell.EventMouse)
if !ok {
return
}
if term.OnEvent != nil {
term.OnEvent(ev)
}
if term.closed {
return
}
e := tcell.NewEventMouse(localX, localY, ev.Buttons(), ev.Modifiers())
term.vterm.HandleEvent(e)
}
2019-03-17 19:02:33 +01:00
func (term *Terminal) Focus(focus bool) {
2019-03-30 16:58:24 +01:00
if term.closed {
return
}
2019-03-17 19:02:33 +01:00
term.focus = focus
2019-03-22 02:34:12 +01:00
if term.ctx != nil {
if !term.focus {
term.ctx.HideCursor()
} else {
_, x, y, style := term.vterm.GetCursor()
term.ctx.SetCursor(x, y)
term.ctx.SetCursorStyle(style)
term.invalidate()
2019-03-22 02:34:12 +01:00
}
}
2019-03-17 19:02:33 +01:00
}
// HandleEvent is used to watch the underlying terminal events
func (term *Terminal) HandleEvent(ev tcell.Event) bool {
if term.closed || term.destroyed {
return false
2019-03-22 00:50:54 +01:00
}
switch ev := ev.(type) {
case *views.EventWidgetContent:
if !term.focus {
return false
}
// Draw here for performance improvement. We call draw again in
// the main Draw, but tcell-term only draws dirty cells, so it
// won't be too much extra CPU there. Drawing there is needed
// for certain msgviews, particularly if the pager command
// exits.
term.draw()
// Perform a tcell screen.Show() to show our updates
// immediately
if term.ctx != nil {
term.ctx.Show()
}
return true
case *tcellterm.EventTitle:
if term.OnTitle != nil {
term.OnTitle(ev.Title())
}
2019-03-22 00:50:54 +01:00
}
return false
2019-03-22 00:50:54 +01:00
}
2019-03-17 19:02:33 +01:00
func (term *Terminal) Event(event tcell.Event) bool {
if term.OnEvent != nil {
if term.OnEvent(event) {
return true
}
}
2019-03-30 16:58:24 +01:00
if term.closed {
return false
}
return term.vterm.HandleEvent(event)
2019-03-17 22:08:54 +01:00
}