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() }