erm/app/darktile/termutil/terminal.go

324 lines
6.5 KiB
Go

package termutil
import (
"bufio"
"bytes"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"sync"
"github.com/creack/pty"
"tuxpa.in/t/erm/app/darktile/config"
)
const (
MainBuffer uint8 = 0
AltBuffer uint8 = 1
InternalBuffer uint8 = 2
)
// Terminal communicates with the underlying terminal
type Terminal struct {
mu sync.Mutex
windowManipulator WindowManipulator
pty *os.File
updateChan chan struct{}
processChan chan MeasuredRune
closeChan chan struct{}
buffers []*Buffer
activeBuffer *Buffer
mouseMode MouseMode
mouseExtMode MouseExtMode
theme *Theme
running bool
shell string
initialCommand string
config *config.Lark
log *slog.Logger
}
// NewTerminal creates a new terminal instance
func New(c *config.Lark, log *slog.Logger) *Terminal {
term := &Terminal{
processChan: make(chan MeasuredRune, 0xffff),
closeChan: make(chan struct{}),
theme: ThemeFromLark(c),
log: log,
config: c,
}
fg := term.theme.DefaultForeground()
bg := term.theme.DefaultBackground()
term.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
term.activeBuffer = term.buffers[0]
return term
}
func (t *Terminal) SetWindowManipulator(m WindowManipulator) {
t.windowManipulator = m
}
func (t *Terminal) reset() {
fg := t.theme.DefaultForeground()
bg := t.theme.DefaultBackground()
t.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
t.useMainBuffer()
}
// Pty exposes the underlying terminal pty, if it exists
func (t *Terminal) Pty() *os.File {
return t.pty
}
func (t *Terminal) WriteToPty(data []byte) error {
_, err := t.pty.Write(data)
return err
}
func (t *Terminal) GetTitle() string {
return t.windowManipulator.GetTitle()
}
func (t *Terminal) Theme() *Theme {
return t.theme
}
// write takes data from StdOut of the child shell and processes it
func (t *Terminal) Write(data []byte) (n int, err error) {
reader := bufio.NewReader(bytes.NewBuffer(data))
for {
r, size, err := reader.ReadRune()
if err == io.EOF {
break
}
t.processChan <- MeasuredRune{Rune: r, Width: size}
}
return len(data), nil
}
func (t *Terminal) SetSize(rows, cols uint16) error {
if t.pty == nil {
return fmt.Errorf("terminal is not running")
}
t.log.Debug("RESIZE", "cols", cols, "rows", rows)
t.activeBuffer.resizeView(cols, rows)
if err := pty.Setsize(t.pty, &pty.Winsize{
Rows: rows,
Cols: cols,
}); err != nil {
return err
}
return nil
}
// Run starts the terminal/shell proxying process
func (t *Terminal) Run(updateChan chan struct{}, rows uint16, cols uint16) error {
t.log.Info("starting terminal", "rows", rows, "cols", cols)
os.Setenv("TERM", "xterm-256color")
t.updateChan = updateChan
if t.shell == "" {
t.shell = os.Getenv("SHELL")
if t.shell == "" {
t.shell = "/bin/sh"
}
}
// Create arbitrary command.
c := exec.Command(t.shell)
// Start the command with a pty.
var err error
t.pty, err = pty.Start(c)
if err != nil {
return err
}
// Make sure to close the pty at the end.
defer func() { _ = t.pty.Close() }() // Best effort.
t.log.Info("terminal started", "rows", rows, "cols", cols)
if err := t.SetSize(rows, cols); err != nil {
return err
}
// Set stdin in raw mode.
//if fd := int(os.Stdin.Fd()); term.IsTerminal(fd) {
// oldState, err := term.MakeRaw(fd)
// if err != nil {
// t.windowManipulator.ReportError(err)
// }
// defer func() { _ = term.Restore(fd, oldState) }() // Best effort.
//}
go t.process()
t.running = true
title := config.Default("erm")(t.config.Str("title"))
t.windowManipulator.SetTitle(title)
if t.initialCommand != "" {
if err := t.WriteToPty([]byte(t.initialCommand)); err != nil {
return err
}
}
_, _ = io.Copy(t, t.pty)
close(t.closeChan)
return nil
}
func (t *Terminal) IsRunning() bool {
return t.running
}
func (t *Terminal) requestRender() {
select {
case t.updateChan <- struct{}{}:
default:
}
}
func (t *Terminal) processSequence(mr MeasuredRune) (render bool) {
if mr.Rune == 0x1b {
return t.handleANSI(t.processChan)
}
return t.processRunes(mr)
}
func (t *Terminal) process() {
for {
select {
case <-t.closeChan:
return
case mr := <-t.processChan:
if t.processSequence(mr) {
t.requestRender()
}
}
}
}
func (t *Terminal) processRunes(runes ...MeasuredRune) (renderRequired bool) {
t.mu.Lock()
defer t.mu.Unlock()
for _, r := range runes {
t.log.Debug(fmt.Sprintf("%c 0x%X", r.Rune, r.Rune))
switch r.Rune {
case 0x05: //enq
continue
case 0x07: //bell
//DING DING DING
continue
case 0x8: //backspace
t.activeBuffer.backspace()
renderRequired = true
case 0x9: //tab
t.activeBuffer.tab()
renderRequired = true
case 0xa, 0xc: //newLine/form feed
t.activeBuffer.newLine()
renderRequired = true
case 0xb: //vertical tab
t.activeBuffer.verticalTab()
renderRequired = true
case 0xd: //carriageReturn
t.activeBuffer.carriageReturn()
renderRequired = true
case 0xe: //shiftOut
t.activeBuffer.currentCharset = 1
case 0xf: //shiftIn
t.activeBuffer.currentCharset = 0
default:
if r.Rune < 0x20 {
// handle any other control chars here?
continue
}
t.activeBuffer.write(t.translateRune(r))
renderRequired = true
}
}
return renderRequired
}
func (t *Terminal) translateRune(b MeasuredRune) MeasuredRune {
table := t.activeBuffer.charsets[t.activeBuffer.currentCharset]
if table == nil {
return b
}
chr, ok := (*table)[b.Rune]
if ok {
return MeasuredRune{Rune: chr, Width: 1}
}
return b
}
func (t *Terminal) setTitle(title string) {
t.windowManipulator.SetTitle(title)
}
func (t *Terminal) switchBuffer(index uint8) {
var carrySize bool
var w, h uint16
if t.activeBuffer != nil {
w, h = t.activeBuffer.viewWidth, t.activeBuffer.viewHeight
carrySize = true
}
t.activeBuffer = t.buffers[index]
if carrySize {
t.activeBuffer.resizeView(w, h)
}
}
func (t *Terminal) GetMouseMode() MouseMode {
return t.mouseMode
}
func (t *Terminal) GetMouseExtMode() MouseExtMode {
return t.mouseExtMode
}
func (t *Terminal) GetActiveBuffer() *Buffer {
return t.activeBuffer
}
func (t *Terminal) useMainBuffer() {
t.switchBuffer(MainBuffer)
}
func (t *Terminal) useAltBuffer() {
t.switchBuffer(AltBuffer)
}
func (t *Terminal) Lock() {
t.mu.Lock()
}
func (t *Terminal) Unlock() {
t.mu.Unlock()
}