about summary refs log blame commit diff stats
path: root/widgets/terminal.go
blob: 07b0f45001dbbde2de207133ff58a9c14e23dd04 (plain) (tree)
1
2
3
4


               
                             












                                          
                                                



                                 
                          


                                        
                                     
                                 
 
                               
                                  




                                                    
                                      
                                
                                           
                   
                            



                                                    
                                               


                                                          
                                               






                                               
                                                 
                          


                                                       
                                     



                                          








                                                                                       









                                                                                     


                        















                                                       

                          












                                                            








                                                                                        









                                                                 
                                       




                                 
                               





                                                        






























                                                                                                 
                                                                 











                                                                                   


                    


                                                                         
                               

                                                             
                               

























                                                             


























                                                                                      









                                                                          
package widgets

import (
	gocolor "image/color"
	"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
	colors       map[tcell.Color]tcell.Color
	ctx          *ui.Context
	cursorPos    vterm.Pos
	cursorShown  bool
	damage       []vterm.Rect
	err          error
	focus        bool
	onInvalidate func(d ui.Drawable)
	pty          *os.File
	start        chan interface{}
	vterm        *vterm.VTerm

	OnClose func(err error)
	OnTitle func(title string)
}

func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
	term := &Terminal{}
	term.cmd = cmd
	term.vterm = vterm.New(24, 80)
	term.vterm.SetUTF8(true)
	term.start = make(chan interface{})
	go func() {
		<-term.start
		buf := make([]byte, 2048)
		for {
			n, err := term.pty.Read(buf)
			if err != nil {
				term.Close(err)
			}
			n, err = term.vterm.Write(buf[:n])
			if err != nil {
				term.Close(err)
			}
			term.Invalidate()
		}
	}()
	screen := term.vterm.ObtainScreen()
	screen.OnDamage = term.onDamage
	screen.OnMoveCursor = term.onMoveCursor
	screen.OnSetTermProp = term.onSetTermProp
	screen.Reset(true)

	state := term.vterm.ObtainState()
	term.colors = make(map[tcell.Color]tcell.Color)
	for i := 0; i < 256; i += 1 {
		tcolor := tcell.Color(i)
		var r uint8 = 0
		var g uint8 = 0
		var b uint8 = uint8(i + 1)
		if i < 16 {
			// Set the first 16 colors to predictable near-black RGB values
			state.SetPaletteColor(i,
				vterm.NewVTermColorRGB(gocolor.RGBA{r, g, b, 255}))
		} else {
			// The rest use RGB
			vcolor := state.GetPaletteColor(i)
			r, g, b = vcolor.GetRGB()
		}
		term.colors[tcell.NewRGBColor(int32(r), int32(g), int32(b))] = tcolor
	}
	fg, bg := state.GetDefaultColors()
	r, g, b := bg.GetRGB()
	term.colors[tcell.NewRGBColor(
		int32(r), int32(g), int32(b))] = tcell.ColorDefault
	r, g, b = fg.GetRGB()
	term.colors[tcell.NewRGBColor(
		int32(r), int32(g), int32(b))] = tcell.ColorDefault

	return term, nil
}

func (term *Terminal) Close(err error) {
	term.err = err
	if term.vterm != nil {
		term.vterm.Close()
		term.vterm = nil
	}
	if term.pty != nil {
		term.pty.Close()
		term.pty = nil
	}
	if term.cmd != nil && term.cmd.Process != nil {
		term.cmd.Process.Kill()
		term.cmd = nil
	}
	if !term.closed && term.OnClose != nil {
		term.OnClose(err)
	}
	term.closed = true
}

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) {
	if term.closed {
		if term.err != nil {
			ui.NewText(term.err.Error()).Strategy(ui.TEXT_CENTER).Draw(ctx)
		} else {
			ui.NewText("Terminal closed").Strategy(ui.TEXT_CENTER).Draw(ctx)
		}
		return
	}

	winsize := pty.Winsize{
		Cols: uint16(ctx.Width()),
		Rows: uint16(ctx.Height()),
	}

	if term.pty == nil {
		term.vterm.SetSize(ctx.Height(), ctx.Width())
		tty, err := pty.StartWithSize(term.cmd, &winsize)
		term.pty = tty
		if err != nil {
			term.Close(err)
			return
		}
		term.start <- nil
	}

	term.ctx = ctx // gross

	rows, cols, err := pty.Getsize(term.pty)
	if err != nil {
		return
	}
	if ctx.Width() != cols || ctx.Height() != rows {
		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 := term.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 {
	return false
}

func (term *Terminal) styleFromCell(cell *vterm.ScreenCell) tcell.Style {
	style := tcell.StyleDefault

	background := cell.Bg()
	r, g, b := background.GetRGB()
	bg := tcell.NewRGBColor(int32(r), int32(g), int32(b))
	foreground := cell.Fg()
	r, g, b = foreground.GetRGB()
	fg := tcell.NewRGBColor(int32(r), int32(g), int32(b))

	if color, ok := term.colors[bg]; ok {
		style = style.Background(color)
	} else {
		style = style.Background(bg)
	}
	if color, ok := term.colors[fg]; ok {
		style = style.Foreground(color)
	} else {
		style = style.Foreground(fg)
	}

	if cell.Attrs().Bold != 0 {
		style = style.Bold(true)
	}
	if cell.Attrs().Underline != 0 {
		style = style.Underline(true)
	}
	if cell.Attrs().Blink != 0 {
		style = style.Blink(true)
	}
	if cell.Attrs().Reverse != 0 {
		style = style.Reverse(true)
	}
	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
}

func (term *Terminal) onSetTermProp(prop int, val *vterm.VTermValue) int {
	switch prop {
	case vterm.VTERM_PROP_TITLE:
		if term.OnTitle != nil {
			term.OnTitle(val.String)
		}
	}
	return 1
}