Add basic terminal widget

This commit is contained in:
Drew DeVault 2019-03-17 14:02:33 -04:00
parent 13ba53c9d0
commit 1170893e39
8 changed files with 228 additions and 6 deletions

2
go.mod
View File

@ -1,6 +1,7 @@
module git.sr.ht/~sircmpwn/aerc2 module git.sr.ht/~sircmpwn/aerc2
require ( 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 v1.0.0-beta.1
github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b github.com/emersion/go-imap-idle v0.0.0-20180114101550-2af93776db6b
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197 // indirect 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/gdamore/tcell v1.0.0
github.com/go-ini/ini v1.42.0 github.com/go-ini/ini v1.42.0
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf 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/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c
github.com/mattn/go-isatty v0.0.3 github.com/mattn/go-isatty v0.0.3

10
go.sum
View File

@ -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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= 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/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 h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 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 h1:vLFQnHOnCnmlySdpHAKF+mH7MhsthJgpBbfexVhHwxY=
github.com/kyoh86/xdg v0.0.0-20171127140545-8db68a8ea76a/go.mod h1:Z5mDqe0fxyxn3W2yTxsBAOQqIrXADQIh02wrTnaRM38= 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 h1:b11Y3yxg40v2/9KUz76a4mSC1DMlgnPGAt+4pJSgmyU=
github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= 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 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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= github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=

View File

@ -16,6 +16,8 @@ type Drawable interface {
type Interactive interface { type Interactive interface {
// Returns true if the event was handled by this component // Returns true if the event was handled by this component
Event(event tcell.Event) bool Event(event tcell.Event) bool
// Indicates whether or not this control will receive input events
Focus(focus bool)
} }
type Simulator interface { type Simulator interface {

View File

@ -54,6 +54,7 @@ func Initialize(conf *config.AercConfig,
state.invalidations <- nil state.invalidations <- nil
})() })()
}) })
content.Focus(true)
return &state, nil return &state, nil
} }

View File

@ -20,7 +20,7 @@ type AccountView struct {
dirlist *DirectoryList dirlist *DirectoryList
grid *ui.Grid grid *ui.Grid
logger *log.Logger logger *log.Logger
interactive ui.Interactive interactive []ui.Interactive
onInvalidate func(d ui.Drawable) onInvalidate func(d ui.Drawable)
runCmd func(cmd string) error runCmd func(cmd string) error
msglist *MessageList msglist *MessageList
@ -116,6 +116,21 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
acct.grid.Draw(ctx) 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() { func (acct *AccountView) beginExCommand() {
exline := NewExLine(func(command string) { exline := NewExLine(func(command string) {
err := acct.runCmd(command) err := acct.runCmd(command)
@ -124,18 +139,18 @@ func (acct *AccountView) beginExCommand() {
Color(tcell.ColorRed, tcell.ColorWhite) Color(tcell.ColorRed, tcell.ColorWhite)
} }
acct.statusbar.Pop() acct.statusbar.Pop()
acct.interactive = nil acct.popInteractive()
}, func() { }, func() {
acct.statusbar.Pop() acct.statusbar.Pop()
acct.interactive = nil acct.popInteractive()
}) })
acct.interactive = exline acct.pushInteractive(exline)
acct.statusbar.Push(exline) acct.statusbar.Push(exline)
} }
func (acct *AccountView) Event(event tcell.Event) bool { func (acct *AccountView) Event(event tcell.Event) bool {
if acct.interactive != nil { if len(acct.interactive) != 0 {
return acct.interactive.Event(event) return acct.interactive[len(acct.interactive)-1].Event(event)
} }
switch event := event.(type) { switch event := event.(type) {

View File

@ -66,6 +66,10 @@ func (aerc *Aerc) Invalidate() {
aerc.grid.Invalidate() aerc.grid.Invalidate()
} }
func (aerc *Aerc) Focus(focus bool) {
// who cares
}
func (aerc *Aerc) Draw(ctx *libui.Context) { func (aerc *Aerc) Draw(ctx *libui.Context) {
aerc.grid.Draw(ctx) aerc.grid.Draw(ctx)
} }

View File

@ -17,6 +17,7 @@ type ExLine struct {
ctx *ui.Context ctx *ui.Context
cancel func() cancel func()
cells int cells int
focus bool
index int index int
scroll 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) { func (ex *ExLine) insert(ch rune) {
left := ex.command[:ex.index] left := ex.command[:ex.index]
right := ex.command[ex.index:] right := ex.command[ex.index:]

179
widgets/terminal.go Normal file
View File

@ -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
}