Add basic terminal widget
This commit is contained in:
parent
13ba53c9d0
commit
1170893e39
8 changed files with 228 additions and 6 deletions
2
go.mod
2
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
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -54,6 +54,7 @@ func Initialize(conf *config.AercConfig,
|
|||
state.invalidations <- nil
|
||||
})()
|
||||
})
|
||||
content.Focus(true)
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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:]
|
||||
|
|
179
widgets/terminal.go
Normal file
179
widgets/terminal.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue