about summary refs log tree commit diff stats
path: root/widgets/terminal.go
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 /widgets/terminal.go
parent13ba53c9d03c375877395a6580d6f694bd19020f (diff)
downloadaerc-1170893e395ff5e3e7bee7ff51224b4a572f01cf.tar.gz
Add basic terminal widget
Diffstat (limited to 'widgets/terminal.go')
-rw-r--r--widgets/terminal.go179
1 files changed, 179 insertions, 0 deletions
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
+}