aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

terminal.go (10744B)


      1 package widgets
      2 
      3 import (
      4 	"os"
      5 	"os/exec"
      6 	"sync"
      7 
      8 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
      9 
     10 	"git.sr.ht/~sircmpwn/pty"
     11 	"github.com/ddevault/go-libvterm"
     12 	"github.com/gdamore/tcell"
     13 )
     14 
     15 type vtermKey struct {
     16 	Key  vterm.Key
     17 	Rune rune
     18 	Mod  vterm.Modifier
     19 }
     20 
     21 var keyMap map[tcell.Key]vtermKey
     22 
     23 func directKey(key vterm.Key) vtermKey {
     24 	return vtermKey{key, 0, vterm.ModNone}
     25 }
     26 
     27 func runeMod(r rune, mod vterm.Modifier) vtermKey {
     28 	return vtermKey{vterm.KeyNone, r, mod}
     29 }
     30 
     31 func keyMod(key vterm.Key, mod vterm.Modifier) vtermKey {
     32 	return vtermKey{key, 0, mod}
     33 }
     34 
     35 func init() {
     36 	keyMap = make(map[tcell.Key]vtermKey)
     37 	keyMap[tcell.KeyCtrlSpace] = runeMod(' ', vterm.ModCtrl)
     38 	keyMap[tcell.KeyCtrlA] = runeMod('a', vterm.ModCtrl)
     39 	keyMap[tcell.KeyCtrlB] = runeMod('b', vterm.ModCtrl)
     40 	keyMap[tcell.KeyCtrlC] = runeMod('c', vterm.ModCtrl)
     41 	keyMap[tcell.KeyCtrlD] = runeMod('d', vterm.ModCtrl)
     42 	keyMap[tcell.KeyCtrlE] = runeMod('e', vterm.ModCtrl)
     43 	keyMap[tcell.KeyCtrlF] = runeMod('f', vterm.ModCtrl)
     44 	keyMap[tcell.KeyCtrlG] = runeMod('g', vterm.ModCtrl)
     45 	keyMap[tcell.KeyCtrlH] = runeMod('h', vterm.ModCtrl)
     46 	keyMap[tcell.KeyCtrlI] = runeMod('i', vterm.ModCtrl)
     47 	keyMap[tcell.KeyCtrlJ] = runeMod('j', vterm.ModCtrl)
     48 	keyMap[tcell.KeyCtrlK] = runeMod('k', vterm.ModCtrl)
     49 	keyMap[tcell.KeyCtrlL] = runeMod('l', vterm.ModCtrl)
     50 	keyMap[tcell.KeyCtrlM] = runeMod('m', vterm.ModCtrl)
     51 	keyMap[tcell.KeyCtrlN] = runeMod('n', vterm.ModCtrl)
     52 	keyMap[tcell.KeyCtrlO] = runeMod('o', vterm.ModCtrl)
     53 	keyMap[tcell.KeyCtrlP] = runeMod('p', vterm.ModCtrl)
     54 	keyMap[tcell.KeyCtrlQ] = runeMod('q', vterm.ModCtrl)
     55 	keyMap[tcell.KeyCtrlR] = runeMod('r', vterm.ModCtrl)
     56 	keyMap[tcell.KeyCtrlS] = runeMod('s', vterm.ModCtrl)
     57 	keyMap[tcell.KeyCtrlT] = runeMod('t', vterm.ModCtrl)
     58 	keyMap[tcell.KeyCtrlU] = runeMod('u', vterm.ModCtrl)
     59 	keyMap[tcell.KeyCtrlV] = runeMod('v', vterm.ModCtrl)
     60 	keyMap[tcell.KeyCtrlW] = runeMod('w', vterm.ModCtrl)
     61 	keyMap[tcell.KeyCtrlX] = runeMod('x', vterm.ModCtrl)
     62 	keyMap[tcell.KeyCtrlY] = runeMod('y', vterm.ModCtrl)
     63 	keyMap[tcell.KeyCtrlZ] = runeMod('z', vterm.ModCtrl)
     64 	keyMap[tcell.KeyCtrlBackslash] = runeMod('\\', vterm.ModCtrl)
     65 	keyMap[tcell.KeyCtrlCarat] = runeMod('^', vterm.ModCtrl)
     66 	keyMap[tcell.KeyCtrlUnderscore] = runeMod('_', vterm.ModCtrl)
     67 	keyMap[tcell.KeyEnter] = directKey(vterm.KeyEnter)
     68 	keyMap[tcell.KeyTab] = directKey(vterm.KeyTab)
     69 	keyMap[tcell.KeyBackspace] = directKey(vterm.KeyBackspace)
     70 	keyMap[tcell.KeyEscape] = directKey(vterm.KeyEscape)
     71 	keyMap[tcell.KeyUp] = directKey(vterm.KeyUp)
     72 	keyMap[tcell.KeyDown] = directKey(vterm.KeyDown)
     73 	keyMap[tcell.KeyLeft] = directKey(vterm.KeyLeft)
     74 	keyMap[tcell.KeyRight] = directKey(vterm.KeyRight)
     75 	keyMap[tcell.KeyInsert] = directKey(vterm.KeyIns)
     76 	keyMap[tcell.KeyDelete] = directKey(vterm.KeyDel)
     77 	keyMap[tcell.KeyHome] = directKey(vterm.KeyHome)
     78 	keyMap[tcell.KeyEnd] = directKey(vterm.KeyEnd)
     79 	keyMap[tcell.KeyPgUp] = directKey(vterm.KeyPageUp)
     80 	keyMap[tcell.KeyPgDn] = directKey(vterm.KeyPageDown)
     81 	for i := 0; i < 64; i++ {
     82 		keyMap[tcell.Key(int(tcell.KeyF1)+i)] =
     83 			directKey(vterm.Key(int(vterm.KeyFunction0) + i))
     84 	}
     85 	keyMap[tcell.KeyTAB] = directKey(vterm.KeyTab)
     86 	keyMap[tcell.KeyESC] = directKey(vterm.KeyEscape)
     87 	keyMap[tcell.KeyDEL] = directKey(vterm.KeyBackspace)
     88 }
     89 
     90 type Terminal struct {
     91 	ui.Invalidatable
     92 	closed      bool
     93 	cmd         *exec.Cmd
     94 	ctx         *ui.Context
     95 	cursorPos   vterm.Pos
     96 	cursorShown bool
     97 	destroyed   bool
     98 	err         error
     99 	focus       bool
    100 	pty         *os.File
    101 	start       chan interface{}
    102 	vterm       *vterm.VTerm
    103 
    104 	damage      []vterm.Rect // protected by damageMutex
    105 	damageMutex sync.Mutex
    106 	writeMutex  sync.Mutex
    107 
    108 	OnClose func(err error)
    109 	OnEvent func(event tcell.Event) bool
    110 	OnStart func()
    111 	OnTitle func(title string)
    112 }
    113 
    114 func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
    115 	term := &Terminal{
    116 		cursorShown: true,
    117 	}
    118 	term.cmd = cmd
    119 	term.vterm = vterm.New(24, 80)
    120 	term.vterm.SetUTF8(true)
    121 	term.start = make(chan interface{})
    122 	screen := term.vterm.ObtainScreen()
    123 	go func() {
    124 		<-term.start
    125 		buf := make([]byte, 4096)
    126 		for {
    127 			n, err := term.pty.Read(buf)
    128 			if err != nil || term.closed {
    129 				// These are generally benine errors when the process exits
    130 				term.Close(nil)
    131 				return
    132 			}
    133 			term.writeMutex.Lock()
    134 			n, err = term.vterm.Write(buf[:n])
    135 			term.writeMutex.Unlock()
    136 			if err != nil {
    137 				term.Close(err)
    138 				return
    139 			}
    140 			screen.Flush()
    141 			term.flushTerminal()
    142 			term.invalidate()
    143 		}
    144 	}()
    145 	screen.OnDamage = term.onDamage
    146 	screen.OnMoveCursor = term.onMoveCursor
    147 	screen.OnSetTermProp = term.onSetTermProp
    148 	screen.EnableAltScreen(true)
    149 	screen.Reset(true)
    150 	return term, nil
    151 }
    152 
    153 func (term *Terminal) flushTerminal() {
    154 	buf := make([]byte, 4096)
    155 	for {
    156 		n, err := term.vterm.Read(buf)
    157 		if err != nil {
    158 			term.Close(err)
    159 			return
    160 		}
    161 		if n == 0 {
    162 			break
    163 		}
    164 		n, err = term.pty.Write(buf[:n])
    165 		if err != nil {
    166 			term.Close(err)
    167 			return
    168 		}
    169 	}
    170 }
    171 
    172 func (term *Terminal) Close(err error) {
    173 	if term.closed {
    174 		return
    175 	}
    176 	term.err = err
    177 	if term.pty != nil {
    178 		term.pty.Close()
    179 		term.pty = nil
    180 	}
    181 	if term.cmd != nil && term.cmd.Process != nil {
    182 		term.cmd.Process.Kill()
    183 		term.cmd = nil
    184 	}
    185 	if !term.closed && term.OnClose != nil {
    186 		term.OnClose(err)
    187 	}
    188 	term.closed = true
    189 	term.ctx.HideCursor()
    190 }
    191 
    192 func (term *Terminal) Destroy() {
    193 	if term.destroyed {
    194 		return
    195 	}
    196 	if term.vterm != nil {
    197 		term.vterm.Close()
    198 		term.vterm = nil
    199 	}
    200 	if term.ctx != nil {
    201 		term.ctx.HideCursor()
    202 	}
    203 	term.destroyed = true
    204 }
    205 
    206 func (term *Terminal) Invalidate() {
    207 	if term.vterm != nil {
    208 		width, height := term.vterm.Size()
    209 		rect := vterm.NewRect(0, width, 0, height)
    210 		term.damageMutex.Lock()
    211 		term.damage = append(term.damage, *rect)
    212 		term.damageMutex.Unlock()
    213 	}
    214 	term.invalidate()
    215 }
    216 
    217 func (term *Terminal) invalidate() {
    218 	term.DoInvalidate(term)
    219 }
    220 
    221 func (term *Terminal) Draw(ctx *ui.Context) {
    222 	if term.destroyed {
    223 		return
    224 	}
    225 
    226 	term.ctx = ctx // gross
    227 
    228 	if !term.closed {
    229 		winsize := pty.Winsize{
    230 			Cols: uint16(ctx.Width()),
    231 			Rows: uint16(ctx.Height()),
    232 		}
    233 
    234 		if term.pty == nil {
    235 			term.vterm.SetSize(ctx.Height(), ctx.Width())
    236 			tty, err := pty.StartWithSize(term.cmd, &winsize)
    237 			term.pty = tty
    238 			if err != nil {
    239 				term.Close(err)
    240 				return
    241 			}
    242 			term.start <- nil
    243 			if term.OnStart != nil {
    244 				term.OnStart()
    245 			}
    246 		}
    247 
    248 		rows, cols, err := pty.Getsize(term.pty)
    249 		if err != nil {
    250 			return
    251 		}
    252 		if ctx.Width() != cols || ctx.Height() != rows {
    253 			term.writeMutex.Lock()
    254 			pty.Setsize(term.pty, &winsize)
    255 			term.vterm.SetSize(ctx.Height(), ctx.Width())
    256 			term.writeMutex.Unlock()
    257 			rect := vterm.NewRect(0, ctx.Width(), 0, ctx.Height())
    258 			term.damageMutex.Lock()
    259 			term.damage = append(term.damage, *rect)
    260 			term.damageMutex.Unlock()
    261 			return
    262 		}
    263 	}
    264 
    265 	screen := term.vterm.ObtainScreen()
    266 
    267 	type coords struct {
    268 		x int
    269 		y int
    270 	}
    271 
    272 	// naive optimization
    273 	visited := make(map[coords]interface{})
    274 
    275 	term.damageMutex.Lock()
    276 	for _, rect := range term.damage {
    277 		for x := rect.StartCol(); x < rect.EndCol() && x < ctx.Width(); x += 1 {
    278 
    279 			for y := rect.StartRow(); y < rect.EndRow() && y < ctx.Height(); y += 1 {
    280 
    281 				coords := coords{x, y}
    282 				if _, ok := visited[coords]; ok {
    283 					continue
    284 				}
    285 				visited[coords] = nil
    286 
    287 				cell, err := screen.GetCellAt(y, x)
    288 				if err != nil {
    289 					continue
    290 				}
    291 				style := term.styleFromCell(cell)
    292 				ctx.Printf(x, y, style, "%s", string(cell.Chars()))
    293 			}
    294 		}
    295 	}
    296 
    297 	term.damage = nil
    298 	term.damageMutex.Unlock()
    299 
    300 	if term.focus && !term.closed {
    301 		if !term.cursorShown {
    302 			ctx.HideCursor()
    303 		} else {
    304 			state := term.vterm.ObtainState()
    305 			row, col := state.GetCursorPos()
    306 			ctx.SetCursor(col, row)
    307 		}
    308 	}
    309 }
    310 
    311 func (term *Terminal) Focus(focus bool) {
    312 	if term.closed {
    313 		return
    314 	}
    315 	term.focus = focus
    316 	if term.ctx != nil {
    317 		if !term.focus {
    318 			term.ctx.HideCursor()
    319 		} else {
    320 			state := term.vterm.ObtainState()
    321 			row, col := state.GetCursorPos()
    322 			term.ctx.SetCursor(col, row)
    323 			term.Invalidate()
    324 		}
    325 	}
    326 }
    327 
    328 func convertMods(mods tcell.ModMask) vterm.Modifier {
    329 	var (
    330 		ret  uint = 0
    331 		mask uint = uint(mods)
    332 	)
    333 	if mask&uint(tcell.ModShift) > 0 {
    334 		ret |= uint(vterm.ModShift)
    335 	}
    336 	if mask&uint(tcell.ModCtrl) > 0 {
    337 		ret |= uint(vterm.ModCtrl)
    338 	}
    339 	if mask&uint(tcell.ModAlt) > 0 {
    340 		ret |= uint(vterm.ModAlt)
    341 	}
    342 	return vterm.Modifier(ret)
    343 }
    344 
    345 func (term *Terminal) Event(event tcell.Event) bool {
    346 	if term.OnEvent != nil {
    347 		if term.OnEvent(event) {
    348 			return true
    349 		}
    350 	}
    351 	if term.closed {
    352 		return false
    353 	}
    354 	switch event := event.(type) {
    355 	case *tcell.EventKey:
    356 		if event.Key() == tcell.KeyRune {
    357 			term.vterm.KeyboardUnichar(
    358 				event.Rune(), convertMods(event.Modifiers()))
    359 		} else {
    360 			if key, ok := keyMap[event.Key()]; ok {
    361 				if key.Key == vterm.KeyNone {
    362 					term.vterm.KeyboardUnichar(
    363 						key.Rune, key.Mod)
    364 				} else if key.Mod == vterm.ModNone {
    365 					term.vterm.KeyboardKey(key.Key,
    366 						convertMods(event.Modifiers()))
    367 				} else {
    368 					term.vterm.KeyboardKey(key.Key, key.Mod)
    369 				}
    370 			}
    371 		}
    372 		term.flushTerminal()
    373 	}
    374 	return false
    375 }
    376 
    377 func (term *Terminal) styleFromCell(cell *vterm.ScreenCell) tcell.Style {
    378 	style := tcell.StyleDefault
    379 
    380 	background := cell.Bg()
    381 	foreground := cell.Fg()
    382 
    383 	var (
    384 		bg tcell.Color
    385 		fg tcell.Color
    386 	)
    387 	if background.IsDefaultBg() {
    388 		bg = tcell.ColorDefault
    389 	} else if background.IsIndexed() {
    390 		bg = tcell.Color(background.GetIndex())
    391 	} else if background.IsRgb() {
    392 		r, g, b := background.GetRGB()
    393 		bg = tcell.NewRGBColor(int32(r), int32(g), int32(b))
    394 	}
    395 	if foreground.IsDefaultFg() {
    396 		fg = tcell.ColorDefault
    397 	} else if foreground.IsIndexed() {
    398 		fg = tcell.Color(foreground.GetIndex())
    399 	} else if foreground.IsRgb() {
    400 		r, g, b := foreground.GetRGB()
    401 		fg = tcell.NewRGBColor(int32(r), int32(g), int32(b))
    402 	}
    403 
    404 	style = style.Background(bg).Foreground(fg)
    405 
    406 	if cell.Attrs().Bold != 0 {
    407 		style = style.Bold(true)
    408 	}
    409 	if cell.Attrs().Underline != 0 {
    410 		style = style.Underline(true)
    411 	}
    412 	if cell.Attrs().Blink != 0 {
    413 		style = style.Blink(true)
    414 	}
    415 	if cell.Attrs().Reverse != 0 {
    416 		style = style.Reverse(true)
    417 	}
    418 	return style
    419 }
    420 
    421 func (term *Terminal) onDamage(rect *vterm.Rect) int {
    422 	term.damageMutex.Lock()
    423 	term.damage = append(term.damage, *rect)
    424 	term.damageMutex.Unlock()
    425 	term.invalidate()
    426 	return 1
    427 }
    428 
    429 func (term *Terminal) onMoveCursor(old *vterm.Pos,
    430 	pos *vterm.Pos, visible bool) int {
    431 
    432 	rows, cols, _ := pty.Getsize(term.pty)
    433 	if pos.Row() >= rows || pos.Col() >= cols {
    434 		return 1
    435 	}
    436 
    437 	term.cursorPos = *pos
    438 	term.invalidate()
    439 	return 1
    440 }
    441 
    442 func (term *Terminal) onSetTermProp(prop int, val *vterm.VTermValue) int {
    443 	switch prop {
    444 	case vterm.VTERM_PROP_TITLE:
    445 		if term.OnTitle != nil {
    446 			term.OnTitle(val.String)
    447 		}
    448 	case vterm.VTERM_PROP_CURSORVISIBLE:
    449 		term.cursorShown = val.Boolean
    450 		term.invalidate()
    451 	}
    452 	return 1
    453 }