mirror of https://github.com/liamg/aminal.git
456 lines
11 KiB
Go
456 lines
11 KiB
Go
package terminal
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/liamg/aminal/buffer"
|
|
"github.com/liamg/aminal/config"
|
|
"github.com/liamg/aminal/platform"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
MainBuffer uint8 = 0
|
|
AltBuffer uint8 = 1
|
|
InternalBuffer uint8 = 2
|
|
)
|
|
|
|
type MouseMode uint
|
|
type MouseExtMode uint
|
|
|
|
const (
|
|
MouseModeNone MouseMode = iota
|
|
MouseModeX10
|
|
MouseModeVT200
|
|
MouseModeVT200Highlight
|
|
MouseModeButtonEvent
|
|
MouseModeAnyEvent
|
|
MouseExtNone MouseExtMode = iota
|
|
MouseExtUTF
|
|
MouseExtSGR
|
|
MouseExtURXVT
|
|
)
|
|
|
|
type WindowManipulationInterface interface {
|
|
RestoreWindow(term *Terminal) error
|
|
IconifyWindow(term *Terminal) error
|
|
MoveWindow(term *Terminal, pixelX int, pixelY int) error
|
|
ResizeWindowByPixels(term *Terminal, pixelsHeight int, pixelsWidth int) error
|
|
BringWindowToFront(term *Terminal) error
|
|
ResizeWindowByChars(term *Terminal, charsHeight int, charsWidth int) error
|
|
MaximizeWindow(term *Terminal) error
|
|
ReportWindowState(term *Terminal) error
|
|
ReportWindowPosition(term *Terminal) error
|
|
ReportWindowSizeInPixels(term *Terminal) error
|
|
ReportWindowSizeInChars(term *Terminal) error
|
|
}
|
|
|
|
type Terminal struct {
|
|
program uint32
|
|
buffers []*buffer.Buffer
|
|
activeBuffer *buffer.Buffer
|
|
lock sync.Mutex
|
|
pty platform.Pty
|
|
logger *zap.SugaredLogger
|
|
title string
|
|
size Winsize
|
|
config *config.Config
|
|
titleHandlers []chan bool
|
|
resizeHandlers []chan bool
|
|
reverseHandlers []chan bool
|
|
modes Modes
|
|
mouseMode MouseMode
|
|
mouseExtMode MouseExtMode
|
|
bracketedPasteMode bool
|
|
charWidth float32
|
|
charHeight float32
|
|
lastBuffer uint8
|
|
terminalState *buffer.TerminalState
|
|
platformDependentSettings platform.PlatformDependentSettings
|
|
dirty *notifier
|
|
|
|
WindowManipulation WindowManipulationInterface
|
|
}
|
|
|
|
type Modes struct {
|
|
ShowCursor bool
|
|
ApplicationCursorKeys bool
|
|
BlinkingCursor bool
|
|
}
|
|
|
|
type Winsize struct {
|
|
Height uint16
|
|
Width uint16
|
|
x uint16 //ignored, but necessary for ioctl calls
|
|
y uint16 //ignored, but necessary for ioctl calls
|
|
}
|
|
|
|
func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Terminal {
|
|
t := &Terminal{
|
|
terminalState: buffer.NewTerminalState(1, 1, buffer.CellAttributes{
|
|
FgColour: config.ColourScheme.Foreground,
|
|
BgColour: config.ColourScheme.Background,
|
|
}, config.MaxLines),
|
|
pty: pty,
|
|
logger: logger,
|
|
config: config,
|
|
titleHandlers: []chan bool{},
|
|
modes: Modes{
|
|
ShowCursor: true,
|
|
},
|
|
platformDependentSettings: pty.GetPlatformDependentSettings(),
|
|
dirty: newNotifier(),
|
|
}
|
|
t.buffers = []*buffer.Buffer{
|
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
|
}
|
|
t.activeBuffer = t.buffers[0]
|
|
|
|
return t
|
|
}
|
|
|
|
// Dirty returns a channel that receives an empty struct whenever the
|
|
// terminal becomes dirty.
|
|
func (terminal *Terminal) Dirty() <-chan struct{} {
|
|
return terminal.dirty.C
|
|
}
|
|
|
|
// NotifyDirty is used to signal that the terminal is dirty and the
|
|
// screen must be redrawn.
|
|
func (terminal *Terminal) NotifyDirty() {
|
|
terminal.dirty.Notify()
|
|
}
|
|
|
|
func (terminal *Terminal) SetProgram(program uint32) {
|
|
terminal.program = program
|
|
}
|
|
|
|
func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
|
|
terminal.bracketedPasteMode = enabled
|
|
}
|
|
|
|
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
|
|
return terminal.modes.ApplicationCursorKeys
|
|
}
|
|
|
|
func (terminal *Terminal) SetMouseMode(mode MouseMode) {
|
|
terminal.mouseMode = mode
|
|
}
|
|
|
|
func (terminal *Terminal) GetMouseMode() MouseMode {
|
|
return terminal.mouseMode
|
|
}
|
|
|
|
func (terminal *Terminal) SetMouseExtMode(mode MouseExtMode) {
|
|
terminal.mouseExtMode = mode
|
|
}
|
|
|
|
func (terminal *Terminal) GetMouseExtMode() MouseExtMode {
|
|
return terminal.mouseExtMode
|
|
}
|
|
|
|
func (terminal *Terminal) IsOSCTerminator(char rune) bool {
|
|
_, ok := terminal.platformDependentSettings.OSCTerminators[char]
|
|
return ok
|
|
}
|
|
|
|
func (terminal *Terminal) UseMainBuffer() {
|
|
terminal.activeBuffer = terminal.buffers[MainBuffer]
|
|
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
|
|
}
|
|
|
|
func (terminal *Terminal) UseAltBuffer() {
|
|
terminal.activeBuffer = terminal.buffers[AltBuffer]
|
|
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
|
|
}
|
|
|
|
func (terminal *Terminal) UseInternalBuffer() {
|
|
terminal.activeBuffer = terminal.buffers[InternalBuffer]
|
|
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
|
|
}
|
|
|
|
func (terminal *Terminal) ExitInternalBuffer() {
|
|
terminal.activeBuffer = terminal.buffers[terminal.lastBuffer]
|
|
}
|
|
|
|
func (terminal *Terminal) ActiveBuffer() *buffer.Buffer {
|
|
return terminal.activeBuffer
|
|
}
|
|
|
|
func (terminal *Terminal) UsingMainBuffer() bool {
|
|
return terminal.activeBuffer == terminal.buffers[MainBuffer]
|
|
}
|
|
|
|
func (terminal *Terminal) GetScrollOffset() uint {
|
|
return terminal.terminalState.GetScrollOffset()
|
|
}
|
|
|
|
func (terminal *Terminal) ScreenScrollDown(lines uint16) {
|
|
defer terminal.NotifyDirty()
|
|
buffer := terminal.ActiveBuffer()
|
|
|
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
|
return
|
|
}
|
|
|
|
offset := terminal.terminalState.GetScrollOffset()
|
|
if uint(lines) > offset {
|
|
lines = uint16(offset)
|
|
}
|
|
terminal.terminalState.SetScrollOffset(offset - uint(lines))
|
|
}
|
|
|
|
func (terminal *Terminal) SetCharSize(w float32, h float32) {
|
|
terminal.charWidth = w
|
|
terminal.charHeight = h
|
|
}
|
|
|
|
func (terminal *Terminal) AreaScrollUp(lines uint16) {
|
|
terminal.ActiveBuffer().AreaScrollUp(lines)
|
|
}
|
|
|
|
func (terminal *Terminal) AreaScrollDown(lines uint16) {
|
|
terminal.ActiveBuffer().AreaScrollDown(lines)
|
|
}
|
|
|
|
func (terminal *Terminal) ScreenScrollUp(lines uint16) {
|
|
defer terminal.NotifyDirty()
|
|
buffer := terminal.ActiveBuffer()
|
|
|
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
|
return
|
|
}
|
|
|
|
offset := terminal.terminalState.GetScrollOffset()
|
|
|
|
if uint(lines)+offset >= (uint(buffer.Height()) - uint(buffer.ViewHeight())) {
|
|
terminal.terminalState.SetScrollOffset(uint(buffer.Height()) - uint(buffer.ViewHeight()))
|
|
} else {
|
|
terminal.terminalState.SetScrollOffset(offset + uint(lines))
|
|
}
|
|
}
|
|
|
|
func (terminal *Terminal) ScrollPageDown() {
|
|
terminal.ScreenScrollDown(terminal.terminalState.ViewHeight())
|
|
}
|
|
func (terminal *Terminal) ScrollPageUp() {
|
|
terminal.ScreenScrollUp(terminal.terminalState.ViewHeight())
|
|
}
|
|
|
|
func (terminal *Terminal) ScrollToEnd() {
|
|
terminal.terminalState.SetScrollOffset(0)
|
|
terminal.NotifyDirty()
|
|
}
|
|
|
|
func (terminal *Terminal) GetVisibleLines() []buffer.Line {
|
|
return terminal.ActiveBuffer().GetVisibleLines()
|
|
}
|
|
|
|
func (terminal *Terminal) GetCell(col uint16, row uint16) *buffer.Cell {
|
|
return terminal.ActiveBuffer().GetCell(col, row)
|
|
}
|
|
|
|
func (terminal *Terminal) AttachTitleChangeHandler(handler chan bool) {
|
|
terminal.titleHandlers = append(terminal.titleHandlers, handler)
|
|
}
|
|
|
|
func (terminal *Terminal) AttachResizeHandler(handler chan bool) {
|
|
terminal.resizeHandlers = append(terminal.resizeHandlers, handler)
|
|
}
|
|
|
|
func (terminal *Terminal) AttachReverseHandler(handler chan bool) {
|
|
terminal.reverseHandlers = append(terminal.reverseHandlers, handler)
|
|
}
|
|
|
|
func (terminal *Terminal) Modes() Modes {
|
|
return terminal.modes
|
|
}
|
|
|
|
func (terminal *Terminal) emitTitleChange() {
|
|
for _, h := range terminal.titleHandlers {
|
|
go func(c chan bool) {
|
|
c <- true
|
|
}(h)
|
|
}
|
|
}
|
|
|
|
func (terminal *Terminal) emitResize() {
|
|
for _, h := range terminal.resizeHandlers {
|
|
go func(c chan bool) {
|
|
c <- true
|
|
}(h)
|
|
}
|
|
}
|
|
|
|
func (terminal *Terminal) emitReverse(reverse bool) {
|
|
for _, h := range terminal.reverseHandlers {
|
|
go func(c chan bool) {
|
|
c <- reverse
|
|
}(h)
|
|
}
|
|
}
|
|
|
|
func (terminal *Terminal) GetLogicalCursorX() uint16 {
|
|
if terminal.ActiveBuffer().CursorColumn() >= terminal.ActiveBuffer().Width() {
|
|
return 0
|
|
}
|
|
|
|
return terminal.ActiveBuffer().CursorColumn()
|
|
}
|
|
|
|
func (terminal *Terminal) GetLogicalCursorY() uint16 {
|
|
if terminal.ActiveBuffer().CursorColumn() >= terminal.ActiveBuffer().Width() {
|
|
return terminal.ActiveBuffer().CursorLineAbsolute() + 1
|
|
}
|
|
|
|
return terminal.ActiveBuffer().CursorLineAbsolute()
|
|
}
|
|
|
|
func (terminal *Terminal) GetTitle() string {
|
|
return terminal.title
|
|
}
|
|
|
|
func (terminal *Terminal) SetTitle(title string) {
|
|
terminal.title = title
|
|
terminal.emitTitleChange()
|
|
terminal.NotifyDirty()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (terminal *Terminal) WriteReturn() error {
|
|
if terminal.terminalState.IsNewLineMode() {
|
|
return terminal.Write([]byte{0x0d, 0x0a})
|
|
} else {
|
|
return terminal.Write([]byte{0x0d})
|
|
}
|
|
}
|
|
|
|
func (terminal *Terminal) Paste(data []byte) error {
|
|
|
|
if terminal.bracketedPasteMode {
|
|
data = []byte(fmt.Sprintf("\x1b[200~%s\x1b[201~", string(data)))
|
|
}
|
|
_, 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)
|
|
|
|
go terminal.processInput(buffer)
|
|
for {
|
|
r, _, err := reader.ReadRune()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
buffer <- r
|
|
}
|
|
|
|
//clean exit
|
|
return nil
|
|
}
|
|
|
|
func (terminal *Terminal) Clear() {
|
|
terminal.ActiveBuffer().Clear()
|
|
}
|
|
|
|
func (terminal *Terminal) ReallyClear() {
|
|
terminal.pty.Clear()
|
|
terminal.ActiveBuffer().ReallyClear()
|
|
}
|
|
|
|
func (terminal *Terminal) GetSize() (int, int) {
|
|
return int(terminal.size.Width), int(terminal.size.Height)
|
|
}
|
|
|
|
func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
|
|
if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) {
|
|
return nil
|
|
}
|
|
|
|
err := terminal.pty.Resize(int(newCols), int(newLines))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to set terminal size vai ioctl: Error no %d", err)
|
|
}
|
|
|
|
terminal.size.Width = uint16(newCols)
|
|
terminal.size.Height = uint16(newLines)
|
|
|
|
terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
|
|
|
|
terminal.emitResize()
|
|
terminal.NotifyDirty()
|
|
return nil
|
|
}
|
|
|
|
func (terminal *Terminal) SetAutoWrap(enabled bool) {
|
|
terminal.terminalState.AutoWrap = enabled
|
|
}
|
|
|
|
func (terminal *Terminal) IsAutoWrap() bool {
|
|
return terminal.terminalState.AutoWrap
|
|
}
|
|
|
|
func (terminal *Terminal) SetOriginMode(enabled bool) {
|
|
terminal.terminalState.OriginMode = enabled
|
|
terminal.ActiveBuffer().SetPosition(0, 0)
|
|
}
|
|
|
|
func (terminal *Terminal) SetInsertMode() {
|
|
terminal.terminalState.ReplaceMode = false
|
|
}
|
|
|
|
func (terminal *Terminal) SetReplaceMode() {
|
|
terminal.terminalState.ReplaceMode = true
|
|
}
|
|
|
|
func (terminal *Terminal) SetNewLineMode() {
|
|
terminal.terminalState.LineFeedMode = false
|
|
}
|
|
|
|
func (terminal *Terminal) SetLineFeedMode() {
|
|
terminal.terminalState.LineFeedMode = true
|
|
}
|
|
|
|
func (terminal *Terminal) ResetVerticalMargins() {
|
|
terminal.terminalState.ResetVerticalMargins()
|
|
}
|
|
|
|
func (terminal *Terminal) SetScreenMode(enabled bool) {
|
|
if terminal.terminalState.ScreenMode == enabled {
|
|
return
|
|
}
|
|
terminal.terminalState.ScreenMode = enabled
|
|
terminal.terminalState.CursorAttr.ReverseVideo()
|
|
for _, buffer := range terminal.buffers {
|
|
buffer.ReverseVideo()
|
|
}
|
|
terminal.emitReverse(enabled)
|
|
terminal.NotifyDirty()
|
|
}
|
|
|
|
func (terminal *Terminal) Lock() {
|
|
terminal.lock.Lock()
|
|
}
|
|
|
|
func (terminal *Terminal) Unlock() {
|
|
terminal.lock.Unlock()
|
|
}
|