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 }