about summary refs log blame commit diff stats
path: root/widgets/terminal.go
blob: 4cf7d9aaaf72233a94b74dea18f6620ae06e6d42 (plain) (tree)


















































































































































































                                                                                                 
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
}