summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2019-03-17 14:02:33 -0400
committerDrew DeVault <sir@cmpwn.com>2019-03-17 14:02:33 -0400
commit1170893e395ff5e3e7bee7ff51224b4a572f01cf (patch)
treef15e996bb529293c772159dbcf7a8b3fcd1a312f
parent13ba53c9d03c375877395a6580d6f694bd19020f (diff)
downloadaerc-1170893e395ff5e3e7bee7ff51224b4a572f01cf.tar.gz
Add basic terminal widget
-rw-r--r--go.mod2
-rw-r--r--go.sum10
-rw-r--r--lib/ui/interfaces.go2
-rw-r--r--lib/ui/ui.go1
-rw-r--r--widgets/account.go27
-rw-r--r--widgets/aerc.go4
-rw-r--r--widgets/exline.go9
-rw-r--r--widgets/terminal.go179
8 files changed, 228 insertions, 6 deletions
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
+}