package terminal import ( "bufio" "context" "fmt" "io" "os" "sync" "syscall" "unsafe" "gitlab.com/liamg/raft/buffer" "go.uber.org/zap" ) type Terminal struct { buffer *buffer.Buffer position Position // line and col lock sync.Mutex pty *os.File logger *zap.SugaredLogger title string onUpdate []func() size Winsize colourScheme ColourScheme cursorVisible bool } type Line struct { Cells []Cell wrapped bool } type Winsize struct { Height uint16 Width uint16 x uint16 //ignored, but necessary for ioctl calls y uint16 //ignored, but necessary for ioctl calls } type Position struct { Line int Col int } func New(pty *os.File, logger *zap.SugaredLogger, colourScheme ColourScheme) *Terminal { return &Terminal{ buffer: buffer.NewBuffer(0, 0, buffer.CellAttributes{ FgColour: colourScheme.DefaultFg, BgColour: colourScheme.DefaultBg, }), pty: pty, logger: logger, onUpdate: []func(){}, colourScheme: colourScheme, cursorVisible: true, } } func (terminal *Terminal) GetCell(col int, row int) *buffer.Cell { return terminal.buffer.GetCell(col, row) } func (terminal *Terminal) OnUpdate(handler func()) { terminal.onUpdate = append(terminal.onUpdate, handler) } func (terminal *Terminal) triggerOnUpdate() { for _, handler := range terminal.onUpdate { go handler() } } func (terminal *Terminal) getPosition() Position { return terminal.position } func (terminal *Terminal) IsCursorVisible() bool { return terminal.cursorVisible } func (terminal *Terminal) showCursor() { terminal.cursorVisible = true } func (terminal *Terminal) hideCursor() { terminal.cursorVisible = false } func (terminal *Terminal) incrementPosition() { terminal.SetPosition(terminal.position.Col+1, terminal.position.Line) } func (terminal *Terminal) SetPosition(col int, line int) { terminal.position = Position{ Col: col, Line: line, } } func (terminal *Terminal) GetPosition() Position { return terminal.position } func (terminal *Terminal) GetTitle() string { return terminal.title } // Write sends data, i.e. locally typed keystrokes to the pty func (terminal *Terminal) Write(data []byte) error { _, err := terminal.pty.Write(data) return err } // Read needs to be run on a goroutine, as it continually reads output to set on the terminal func (terminal *Terminal) Read() error { buffer := make(chan rune, 0xffff) reader := bufio.NewReader(terminal.pty) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go terminal.processInput(ctx, buffer) for { r, size, err := reader.ReadRune() if err != nil { if err == io.EOF { break } return err } else if size > 0 { buffer <- r } } //clean exit return nil } func (terminal *Terminal) Clear() { terminal.buffer.Clear() } func (terminal *Terminal) GetSize() (int, int) { return int(terminal.size.Width), int(terminal.size.Height) } func (terminal *Terminal) SetSize(newCols int, newLines int) error { terminal.lock.Lock() defer terminal.lock.Unlock() terminal.size.Width = uint16(newCols) terminal.size.Height = uint16(newLines) terminal.buffer.ResizeView(terminal.size.Width, terminal.size.Height) _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(terminal.pty.Fd()), uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(&terminal.size))) if err != 0 { return fmt.Errorf("Failed to set terminal size vai ioctl: Error no %d", err) } return nil } /* ------------------ -> ssssssssssssssssss ssssPPPPPPPPPPPPPP xxxxxxxxx xxxxxxxxxxxxxxxxxx -------------------------- ssssssssssssssssss SsssPPPPPPPPPPPPPP xxxxxxxxx xxxxxxxxxxxxxxxxxx */