180 lines
3.5 KiB
Go
180 lines
3.5 KiB
Go
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
|
|
}
|