From 1170893e395ff5e3e7bee7ff51224b4a572f01cf Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sun, 17 Mar 2019 14:02:33 -0400 Subject: [PATCH] Add basic terminal widget --- go.mod | 2 + go.sum | 10 +++ lib/ui/interfaces.go | 2 + lib/ui/ui.go | 1 + widgets/account.go | 27 +++++-- widgets/aerc.go | 4 + widgets/exline.go | 9 +++ widgets/terminal.go | 179 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 widgets/terminal.go diff --git a/go.mod b/go.mod index 13a1259..42b932f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module git.sr.ht/~sircmpwn/aerc2 require ( + git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0 github.com/emersion/go-imap v1.0.0-beta.1 github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect @@ -8,6 +9,7 @@ require ( github.com/gdamore/tcell v1.0.0 github.com/go-ini/ini v1.42.0 github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf + github.com/kr/pty v1.1.3 github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c github.com/mattn/go-isatty v0.0.3 diff --git a/go.sum b/go.sum index 99e88d9..dc3ccf1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0 h1:aIQh7m6L3uS8/lg021Cia2QtttUgZO0LuuxJ8wc57dQ= +git.sr.ht/~sircmpwn/go-libvterm v0.0.0-20190316225658-2a4963dd9ec0/go.mod h1:cp37LbiS1y4CrTOmKSF87ZMLwawWUF612RYKTi8vbDc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-imap v1.0.0-beta.1 h1:bTCaVlUnb5mKoW9lEukusxguSYYZPer+q0g5t+vw5X0= @@ -16,14 +18,22 @@ github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38= github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= +github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a h1:vLFQnHOnCnmlySdpHAKF+mH7MhsthJgpBbfexVhHwxY= github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38= github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c h1:b11Y3yxg40v2/9KUz76a4mSC1DMlgnPGAt+4pJSgmyU= github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-libvterm v0.0.0-20190121020430-725de0572324 h1:0C5/KYb9AMSjg9VhXk0RxNMZN/4y3vztCYVNSHIkHlg= +github.com/mattn/go-libvterm v0.0.0-20190121020430-725de0572324/go.mod h1:E9ZjxjhK3K5YoeO/TCZVNsquRRZX2LeIX0+G33613Io= +github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791 h1:PfHMsLQJwoc0ccjK0sam6J0wQo4s8mOuAo2yQGw+T2U= +github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed h1:SDQJB+uDFtSsq49UlzhnJJkFNXqoSG5CHdOnoN/fWF0= +github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed/go.mod h1:TEYd4HSsUc2pZan5xJmjJQLA7c3d9dkV9lNsf8Xh3TY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= diff --git a/lib/ui/interfaces.go b/lib/ui/interfaces.go index c38fcff..d27afe2 100644 --- a/lib/ui/interfaces.go +++ b/lib/ui/interfaces.go @@ -16,6 +16,8 @@ type Drawable interface { type Interactive interface { // Returns true if the event was handled by this component Event(event tcell.Event) bool + // Indicates whether or not this control will receive input events + Focus(focus bool) } type Simulator interface { diff --git a/lib/ui/ui.go b/lib/ui/ui.go index eb86e70..ced039f 100644 --- a/lib/ui/ui.go +++ b/lib/ui/ui.go @@ -54,6 +54,7 @@ func Initialize(conf *config.AercConfig, state.invalidations <- nil })() }) + content.Focus(true) return &state, nil } diff --git a/widgets/account.go b/widgets/account.go index 6919c0e..b6ba595 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -20,7 +20,7 @@ type AccountView struct { dirlist *DirectoryList grid *ui.Grid logger *log.Logger - interactive ui.Interactive + interactive []ui.Interactive onInvalidate func(d ui.Drawable) runCmd func(cmd string) error msglist *MessageList @@ -116,6 +116,21 @@ func (acct *AccountView) Draw(ctx *ui.Context) { acct.grid.Draw(ctx) } +func (acct *AccountView) popInteractive() { + acct.interactive = acct.interactive[:len(acct.interactive)-1] + if len(acct.interactive) != 0 { + acct.interactive[len(acct.interactive)-1].Focus(true) + } +} + +func (acct *AccountView) pushInteractive(item ui.Interactive) { + if len(acct.interactive) != 0 { + acct.interactive[len(acct.interactive)-1].Focus(false) + } + acct.interactive = append(acct.interactive, item) + item.Focus(true) +} + func (acct *AccountView) beginExCommand() { exline := NewExLine(func(command string) { err := acct.runCmd(command) @@ -124,18 +139,18 @@ func (acct *AccountView) beginExCommand() { Color(tcell.ColorRed, tcell.ColorWhite) } acct.statusbar.Pop() - acct.interactive = nil + acct.popInteractive() }, func() { acct.statusbar.Pop() - acct.interactive = nil + acct.popInteractive() }) - acct.interactive = exline + acct.pushInteractive(exline) acct.statusbar.Push(exline) } func (acct *AccountView) Event(event tcell.Event) bool { - if acct.interactive != nil { - return acct.interactive.Event(event) + if len(acct.interactive) != 0 { + return acct.interactive[len(acct.interactive)-1].Event(event) } switch event := event.(type) { diff --git a/widgets/aerc.go b/widgets/aerc.go index 6874600..e7eebad 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -66,6 +66,10 @@ func (aerc *Aerc) Invalidate() { aerc.grid.Invalidate() } +func (aerc *Aerc) Focus(focus bool) { + // who cares +} + func (aerc *Aerc) Draw(ctx *libui.Context) { aerc.grid.Draw(ctx) } diff --git a/widgets/exline.go b/widgets/exline.go index 7eff74a..5c9f065 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -17,6 +17,7 @@ type ExLine struct { ctx *ui.Context cancel func() cells int + focus bool index int scroll int @@ -52,6 +53,14 @@ func (ex *ExLine) Draw(ctx *ui.Context) { } } +func (ex *ExLine) Focus(focus bool) { + ex.focus = focus + if focus && ex.ctx != nil { + cells := runewidth.StringWidth(string(ex.command[:ex.index])) + ex.ctx.SetCursor(cells+1, 0) + } +} + func (ex *ExLine) insert(ch rune) { left := ex.command[:ex.index] right := ex.command[ex.index:] diff --git a/widgets/terminal.go b/widgets/terminal.go new file mode 100644 index 0000000..4cf7d9a --- /dev/null +++ b/widgets/terminal.go @@ -0,0 +1,179 @@ +package widgets + +import ( + "os" + "os/exec" + + "git.sr.ht/~sircmpwn/aerc2/lib/ui" + + "git.sr.ht/~sircmpwn/go-libvterm" + "github.com/gdamore/tcell" + "github.com/kr/pty" +) + +type Terminal struct { + closed bool + cmd *exec.Cmd + ctx *ui.Context + cursorPos vterm.Pos + cursorShown bool + damage []vterm.Rect + focus bool + onInvalidate func(d ui.Drawable) + pty *os.File + vterm *vterm.VTerm +} + +func NewTerminal(cmd *exec.Cmd) (*Terminal, error) { + term := &Terminal{} + term.cmd = cmd + tty, err := pty.Start(cmd) + if err != nil { + return nil, err + } + term.pty = tty + rows, cols, err := pty.Getsize(term.pty) + if err != nil { + return nil, err + } + term.vterm = vterm.New(rows, cols) + term.vterm.SetUTF8(true) + go func() { + buf := make([]byte, 2048) + for { + n, err := term.pty.Read(buf) + if err != nil { + term.Close() + } + n, err = term.vterm.Write(buf[:n]) + if err != nil { + term.Close() + } + term.Invalidate() + } + }() + screen := term.vterm.ObtainScreen() + screen.OnDamage = term.onDamage + screen.OnMoveCursor = term.onMoveCursor + screen.Reset(true) + return term, nil +} + +func (term *Terminal) Close() { + if term.closed { + return + } + term.closed = true + term.vterm.Close() + term.pty.Close() + term.cmd.Process.Kill() +} + +func (term *Terminal) OnInvalidate(cb func(d ui.Drawable)) { + term.onInvalidate = cb +} + +func (term *Terminal) Invalidate() { + if term.onInvalidate != nil { + term.onInvalidate(term) + } +} + +func (term *Terminal) Draw(ctx *ui.Context) { + term.ctx = ctx // gross + if term.closed { + return + } + + rows, cols, err := pty.Getsize(term.pty) + if err != nil { + return + } + if ctx.Width() != cols || ctx.Height() != rows { + winsize := pty.Winsize{ + Cols: uint16(ctx.Width()), + Rows: uint16(ctx.Height()), + } + pty.Setsize(term.pty, &winsize) + term.vterm.SetSize(ctx.Height(), ctx.Width()) + return + } + + screen := term.vterm.ObtainScreen() + screen.Flush() + + type coords struct { + x int + y int + } + + // naive optimization + visited := make(map[coords]interface{}) + + for _, rect := range term.damage { + for x := rect.StartCol(); x < rect.EndCol() && x < ctx.Width(); x += 1 { + + for y := rect.StartCol(); y < rect.EndCol() && y < ctx.Height(); y += 1 { + + coords := coords{x, y} + if _, ok := visited[coords]; ok { + continue + } + visited[coords] = nil + + cell, err := screen.GetCellAt(y, x) + if err != nil { + continue + } + style := styleFromCell(cell) + ctx.Printf(x, y, style, "%s", string(cell.Chars())) + } + } + } +} + +func (term *Terminal) Focus(focus bool) { + term.focus = focus + term.resetCursor() +} + +func (term *Terminal) Event(event tcell.Event) bool { + // TODO + return false +} + +func styleFromCell(cell *vterm.ScreenCell) tcell.Style { + background := cell.Bg() + br, bg, bb := background.GetRGB() + foreground := cell.Fg() + fr, fg, fb := foreground.GetRGB() + style := tcell.StyleDefault. + Background(tcell.NewRGBColor(int32(br), int32(bg), int32(bb))). + Foreground(tcell.NewRGBColor(int32(fr), int32(fg), int32(fb))) + return style +} + +func (term *Terminal) onDamage(rect *vterm.Rect) int { + term.damage = append(term.damage, *rect) + term.Invalidate() + return 1 +} + +func (term *Terminal) resetCursor() { + if term.ctx != nil && term.focus { + if !term.cursorShown { + term.ctx.HideCursor() + } else { + term.ctx.SetCursor(term.cursorPos.Col(), term.cursorPos.Row()) + } + } +} + +func (term *Terminal) onMoveCursor(old *vterm.Pos, + pos *vterm.Pos, visible bool) int { + + term.cursorShown = visible + term.cursorPos = *pos + term.resetCursor() + return 1 +}