diff --git a/buffer/buffer.go b/buffer/buffer.go index 0f427d6..0ce00d1 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -2,8 +2,6 @@ package buffer import ( "fmt" - - "github.com/sirupsen/logrus" ) type Buffer struct { @@ -46,7 +44,7 @@ func (buffer *Buffer) GetCell(viewCol int, viewRow int) *Cell { return &line.cells[viewCol] } -func (buffer *Buffer) attachDisplayChangeHandler(handler chan bool) { +func (buffer *Buffer) AttachDisplayChangeHandler(handler chan bool) { if buffer.displayChangeHandlers == nil { buffer.displayChangeHandlers = []chan bool{} } @@ -57,7 +55,10 @@ func (buffer *Buffer) attachDisplayChangeHandler(handler chan bool) { func (buffer *Buffer) emitDisplayChange() { for _, channel := range buffer.displayChangeHandlers { go func(c chan bool) { - c <- true + select { + case c <- true: + default: + } }(channel) } } @@ -132,44 +133,64 @@ func (buffer *Buffer) Write(runes ...rune) { func (buffer *Buffer) incrementCursorPosition() { - if buffer.CursorColumn()+1 < buffer.Width() { + defer buffer.emitDisplayChange() + + if buffer.CursorColumn()+1 < buffer.Width() { // if not at end of line + buffer.cursorX++ - } else { - if buffer.cursorY == buffer.viewHeight-1 { // if we're on the last line, we can't move the cursor down, we have to move the buffer up, i.e. add a new line + + } else { // we're at the end of the current line + + if buffer.cursorY == buffer.viewHeight-1 { + // if we're on the last line, we can't move the cursor down, we have to move the buffer up, i.e. add a new line + line := newLine() line.setWrapped(true) buffer.lines = append(buffer.lines, line) buffer.cursorX = 0 + } else { + // if we're not on the bottom line... + buffer.cursorX = 0 - if buffer.Height() < int(buffer.ViewHeight()) { + buffer.cursorY++ + + _, err := buffer.getCurrentLine() + if err != nil { line := newLine() line.setWrapped(true) buffer.lines = append(buffer.lines, line) - buffer.cursorY++ - } else { - // @todo test this branch - line := &buffer.lines[buffer.RawLine()] - line.setWrapped(true) } + } } } func (buffer *Buffer) CarriageReturn() { + + defer buffer.emitDisplayChange() + line, err := buffer.getCurrentLine() if err != nil { + // @todo check this... buffer.cursorX = 0 return } if buffer.cursorX == 0 && line.wrapped { buffer.cursorY-- + if len(line.cells) == 0 { + rawLine := int(buffer.RawLine()) + buffer.lines = append(buffer.lines[:rawLine], buffer.lines[rawLine+1:]...) + } } else { buffer.cursorX = 0 } } func (buffer *Buffer) NewLine() { + + defer buffer.emitDisplayChange() + // if we're at the beginning of a line which wrapped from the previous one, and we need a new line, we can effectively not add a new line, and set the current one to non-wrapped if buffer.cursorX == 0 { line := &buffer.lines[buffer.RawLine()] @@ -190,25 +211,45 @@ func (buffer *Buffer) NewLine() { func (buffer *Buffer) MovePosition(x int16, y int16) { + var toX uint16 + var toY uint16 + if int16(buffer.cursorX)+x < 0 { - x = -int16(buffer.cursorX) + toX = 0 + } else { + toX = uint16(int16(buffer.cursorX) + x) } if int16(buffer.cursorY)+y < 0 { - y = -int16(buffer.cursorY) + toY = 0 + } else { + toY = uint16(int16(buffer.cursorY) + y) } - buffer.SetPosition(uint16(int16(buffer.cursorX)+x), uint16(int16(buffer.cursorY)+y)) + buffer.SetPosition(toX, toY) +} + +func (buffer *Buffer) ShowCursor() { + +} + +func (buffer *Buffer) HideCursor() { + +} + +func (buffer *Buffer) SetCursorBlink(enabled bool) { + } func (buffer *Buffer) SetPosition(col uint16, line uint16) { + defer buffer.emitDisplayChange() if col >= buffer.ViewWidth() { col = buffer.ViewWidth() - 1 - logrus.Errorf("Cannot set cursor position: column %d is outside of the current view width (%d columns)", col, buffer.ViewWidth()) + //logrus.Errorf("Cannot set cursor position: column %d is outside of the current view width (%d columns)", col, buffer.ViewWidth()) } if line >= buffer.ViewHeight() { line = buffer.ViewHeight() - 1 - logrus.Errorf("Cannot set cursor position: line %d is outside of the current view height (%d lines)", line, buffer.ViewHeight()) + //logrus.Errorf("Cannot set cursor position: line %d is outside of the current view height (%d lines)", line, buffer.ViewHeight()) } buffer.cursorX = col buffer.cursorY = line @@ -227,10 +268,11 @@ func (buffer *Buffer) GetVisibleLines() []Line { // tested to here func (buffer *Buffer) Clear() { + defer buffer.emitDisplayChange() for i := 0; i < int(buffer.ViewHeight()); i++ { buffer.lines = append(buffer.lines, newLine()) } - buffer.SetPosition(0, 0) + buffer.SetPosition(0, 0) // do we need to set position? } func (buffer *Buffer) getCurrentLine() (*Line, error) { @@ -243,6 +285,7 @@ func (buffer *Buffer) getCurrentLine() (*Line, error) { } func (buffer *Buffer) EraseLine() { + defer buffer.emitDisplayChange() line, err := buffer.getCurrentLine() if err != nil { return @@ -251,6 +294,7 @@ func (buffer *Buffer) EraseLine() { } func (buffer *Buffer) EraseLineToCursor() { + defer buffer.emitDisplayChange() line, err := buffer.getCurrentLine() if err != nil { return @@ -263,42 +307,52 @@ func (buffer *Buffer) EraseLineToCursor() { } func (buffer *Buffer) EraseLineAfterCursor() { + defer buffer.emitDisplayChange() line, err := buffer.getCurrentLine() if err != nil { return } - for i := int(buffer.cursorX + 1); i < len(line.cells); i++ { - line.cells[i].erase() + + max := int(buffer.cursorX + 1) + if max > len(line.cells) { + max = len(line.cells) } + + line.cells = line.cells[:max] } func (buffer *Buffer) EraseDisplayAfterCursor() { + defer buffer.emitDisplayChange() line, err := buffer.getCurrentLine() if err != nil { return } line.cells = line.cells[:buffer.cursorX] - for i := int(buffer.RawLine() + 1); i < buffer.Height(); i++ { - if i < len(buffer.lines) { - buffer.lines[i].cells = []Cell{} + for i := buffer.cursorY + 1; i < buffer.ViewHeight(); i++ { + rawLine := buffer.convertViewLineToRawLine(i) + if int(rawLine) < len(buffer.lines) { + buffer.lines[int(rawLine)].cells = []Cell{} } } } func (buffer *Buffer) EraseDisplayToCursor() { + defer buffer.emitDisplayChange() line, err := buffer.getCurrentLine() if err != nil { return } line.cells = line.cells[buffer.cursorX+1:] - for i := 0; i < int(buffer.RawLine()); i++ { - if i < len(buffer.lines) { - buffer.lines[i].cells = []Cell{} + for i := uint16(0); i < buffer.cursorY; i++ { + rawLine := buffer.convertViewLineToRawLine(i) + if int(rawLine) < len(buffer.lines) { + buffer.lines[int(rawLine)].cells = []Cell{} } } } func (buffer *Buffer) ResizeView(width uint16, height uint16) { + defer buffer.emitDisplayChange() buffer.viewWidth = width buffer.viewHeight = height diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index 722858c..24d1823 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -9,7 +9,7 @@ import ( ) func TestOffsets(t *testing.T) { - b := NewBuffer(10, 8) + b := NewBuffer(10, 8, CellAttributes{}) test := "hellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\nhellothere\n?" b.Write([]rune(test)...) assert.Equal(t, uint16(10), b.ViewWidth()) @@ -19,7 +19,7 @@ func TestOffsets(t *testing.T) { } func TestBufferCreation(t *testing.T) { - b := NewBuffer(10, 20) + b := NewBuffer(10, 20, CellAttributes{}) assert.Equal(t, uint16(10), b.Width()) assert.Equal(t, uint16(20), b.ViewHeight()) assert.Equal(t, uint16(0), b.CursorColumn()) @@ -29,7 +29,7 @@ func TestBufferCreation(t *testing.T) { func TestBufferCursorIncrement(t *testing.T) { - b := NewBuffer(5, 4) + b := NewBuffer(5, 4, CellAttributes{}) b.incrementCursorPosition() require.Equal(t, uint16(1), b.CursorColumn()) require.Equal(t, uint16(0), b.CursorLine()) @@ -76,7 +76,7 @@ func TestBufferCursorIncrement(t *testing.T) { } func TestBufferWrite(t *testing.T) { - b := NewBuffer(5, 20) + b := NewBuffer(5, 20, CellAttributes{}) assert.Equal(t, uint16(0), b.CursorColumn()) assert.Equal(t, uint16(0), b.CursorLine()) @@ -110,7 +110,7 @@ func TestBufferWrite(t *testing.T) { } func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) { - b := NewBuffer(3, 20) + b := NewBuffer(3, 20, CellAttributes{}) b.Write('a', 'b', 'c') assert.Equal(t, uint16(0), b.cursorX) b.Write(0x0a) @@ -124,7 +124,7 @@ func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) { } func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) { - b := NewBuffer(3, 20) + b := NewBuffer(3, 20, CellAttributes{}) b.Write('a', 'b', 'c', 'd') b.Write(0x0a) b.Write('e', 'f') @@ -143,7 +143,7 @@ func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) { func TestSetPosition(t *testing.T) { - b := NewBuffer(120, 80) + b := NewBuffer(120, 80, CellAttributes{}) assert.Equal(t, 0, int(b.CursorColumn())) assert.Equal(t, 0, int(b.CursorLine())) b.SetPosition(60, 10) @@ -159,7 +159,7 @@ func TestSetPosition(t *testing.T) { } func TestMovePosition(t *testing.T) { - b := NewBuffer(120, 80) + b := NewBuffer(120, 80, CellAttributes{}) assert.Equal(t, 0, int(b.CursorColumn())) assert.Equal(t, 0, int(b.CursorLine())) b.MovePosition(-1, -1) @@ -181,7 +181,7 @@ func TestMovePosition(t *testing.T) { func TestVisibleLines(t *testing.T) { - b := NewBuffer(80, 10) + b := NewBuffer(80, 10, CellAttributes{}) b.Write([]rune("hello 1\n")...) b.Write([]rune("hello 2\n")...) b.Write([]rune("hello 3\n")...) @@ -205,7 +205,7 @@ func TestVisibleLines(t *testing.T) { } func TestClearWithoutFullView(t *testing.T) { - b := NewBuffer(80, 10) + b := NewBuffer(80, 10, CellAttributes{}) b.Write([]rune("hello 1\n")...) b.Write([]rune("hello 2\n")...) b.Write([]rune("hello 3")...) @@ -217,7 +217,7 @@ func TestClearWithoutFullView(t *testing.T) { } func TestClearWithFullView(t *testing.T) { - b := NewBuffer(80, 5) + b := NewBuffer(80, 5, CellAttributes{}) b.Write([]rune("hello 1\n")...) b.Write([]rune("hello 2\n")...) b.Write([]rune("hello 3\n")...) @@ -233,7 +233,32 @@ func TestClearWithFullView(t *testing.T) { } } +func TestCarriageReturn(t *testing.T) { + b := NewBuffer(80, 20, CellAttributes{}) + b.Write([]rune("hello!\rsecret")...) + lines := b.GetVisibleLines() + assert.Equal(t, "secret", lines[0].String()) +} + +func TestCarriageReturnOnWrappedLine(t *testing.T) { + b := NewBuffer(80, 6, CellAttributes{}) + b.Write([]rune("hello!\rsecret")...) + lines := b.GetVisibleLines() + assert.Equal(t, "secret", lines[0].String()) +} + +func TestCarriageReturnOnOverWrappedLine(t *testing.T) { + b := NewBuffer(6, 10, CellAttributes{}) + b.Write([]rune("hello there!\rsecret sauce")...) + lines := b.GetVisibleLines() + require.Equal(t, 4, len(lines)) + assert.Equal(t, "hello ", lines[0].String()) + assert.Equal(t, "secret", lines[1].String()) + assert.Equal(t, " sauce", lines[2].String()) + assert.Equal(t, "", lines[3].String()) +} + func TestResizeView(t *testing.T) { - b := NewBuffer(80, 20) + b := NewBuffer(80, 20, CellAttributes{}) b.ResizeView(40, 10) } diff --git a/buffer/cell.go b/buffer/cell.go index 03e511e..28966b0 100644 --- a/buffer/cell.go +++ b/buffer/cell.go @@ -22,11 +22,18 @@ func newCell() Cell { } func (cell *Cell) Rune() rune { + if cell.r == 0 { + return '~' + } return cell.r } func (cell *Cell) Fg() [3]float32 { - return cell.attr.FgColour + if cell.r == 0 { + return [3]float32{0.5, 0.5, 0.5} + } + return [3]float32{0.9, 0.9, 0.9} // @todo fix this + //return cell.attr.FgColour } func (cell *Cell) Bg() [3]float32 { diff --git a/config/config.go b/config/config.go index 0e93c73..87e5b41 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,11 @@ import ( type Config struct { DebugMode bool `yaml:"debug"` ColourScheme terminal.ColourScheme + Rendering RenderingConfig `yaml:"rendering"` +} + +type RenderingConfig struct { + AlwaysRepaint bool `yaml:"always_repaint"` } var DefaultConfig = Config{ diff --git a/cover.out b/cover.out new file mode 100644 index 0000000..5f02b11 --- /dev/null +++ b/cover.out @@ -0,0 +1 @@ +mode: set diff --git a/fonts/CamingoCode-Regular.ttf b/fonts/CamingoCode-Regular.ttf deleted file mode 100644 index 04b8860..0000000 Binary files a/fonts/CamingoCode-Regular.ttf and /dev/null differ diff --git a/gui/gui.go b/gui/gui.go index 9625a42..da57119 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -116,11 +116,6 @@ func (gui *GUI) Render() error { gui.logger.Debugf("Starting pty read handling...") - updateChan := make(chan bool, 1024) - - gui.terminal.OnUpdate(func() { - updateChan <- true - }) go func() { err := gui.terminal.Read() if err != nil { @@ -129,9 +124,6 @@ func (gui *GUI) Render() error { gui.Close() }() - ticker := time.NewTicker(time.Millisecond * 100) - defer ticker.Stop() - gui.logger.Debugf("Starting render...") gl.UseProgram(program) @@ -142,7 +134,7 @@ func (gui *GUI) Render() error { //gl.DepthFunc(gl.LESS) gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - glfw.SwapInterval(1) + //glfw.SwapInterval(1) gl.ClearColor( gui.config.ColourScheme.DefaultBg[0], @@ -151,26 +143,61 @@ func (gui *GUI) Render() error { 1.0, ) + changeChan := make(chan bool, 1) + titleChan := make(chan bool, 1) + + gui.terminal.AttachTitleChangeHandler(titleChan) + gui.terminal.AttachDisplayChangeHandler(changeChan) + + frames := 0 + frameCount := 0 + fps := 0 + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for !gui.window.ShouldClose() { + select { + + case <-changeChan: + frames = 2 + gui.logger.Sync() + case <-titleChan: + gui.window.SetTitle(gui.terminal.GetTitle()) + case <-ticker.C: + fps = frameCount + frameCount = 0 + default: + } + gl.UseProgram(program) - // Render the string. - // @todo uncommentbut dont run all of the time... - perhaps use onTitleChange event from terminal? - //gui.window.SetTitle(gui.terminal.GetTitle()) + if gui.config.Rendering.AlwaysRepaint || frames > 0 { - //gl.ClearColor(0.5, 0.5, 0.5, 1.0) - gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) - cols, rows := gui.getTermSize() + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + cols, rows := gui.getTermSize() - for row := 0; row < rows; row++ { - for col := 0; col < cols; col++ { - gui.renderer.DrawCell(gui.terminal.GetCell(col, row), col, row) + for row := 0; row < rows; row++ { + for col := 0; col < cols; col++ { + gui.renderer.DrawCell(gui.terminal.GetCell(col, row), col, row) + } } + + gui.font.SetColor(1, 0.5, 0.5, 0.5) + fpsData := "" + if gui.config.Rendering.AlwaysRepaint { + fpsData = fmt.Sprintf("%d FPS", fps) + } + gui.font.Print(10, float32(gui.height-20), 1.5, fmt.Sprintf("%s", fpsData)) } glfw.PollEvents() - gui.window.SwapBuffers() + + if gui.config.Rendering.AlwaysRepaint || frames > 0 { + frameCount++ + gui.window.SwapBuffers() + frames-- + } } gui.logger.Debugf("Stopping render...") diff --git a/main.go b/main.go index 3dfbdfd..fb0b97e 100644 --- a/main.go +++ b/main.go @@ -34,8 +34,7 @@ func loadConfigFile() config.Config { places := []string{ fmt.Sprintf("%s/.raft.yml", home), - fmt.Sprintf("%s/.raft/config.yml", home), - fmt.Sprintf("%s/.config/raft/config.yml", home), + fmt.Sprintf("%s/.config/raft.yml", home), } for _, place := range places { diff --git a/terminal/csi.go b/terminal/csi.go index 61b63f4..cb4d927 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -53,11 +53,7 @@ CSI: distance = 1 } } - if terminal.position.Line-distance >= 0 { - terminal.position.Line -= distance - } else { - terminal.position.Line = 0 - } + terminal.buffer.MovePosition(0, -int16(distance)) case 'B': distance := 1 if len(params) > 0 { @@ -68,12 +64,7 @@ CSI: } } - _, h := terminal.GetSize() - if terminal.position.Line+distance >= h { - terminal.position.Line = h - 1 - } else { - terminal.position.Line += distance - } + terminal.buffer.MovePosition(0, int16(distance)) case 'C': distance := 1 @@ -85,11 +76,7 @@ CSI: } } - terminal.position.Col += distance - w, _ := terminal.GetSize() - if terminal.position.Col >= w { - terminal.position.Col = w - 1 - } + terminal.buffer.MovePosition(int16(distance), 0) case 'D': @@ -102,10 +89,7 @@ CSI: } } - terminal.position.Col -= distance - if terminal.position.Col < 0 { - terminal.position.Col = 0 - } + terminal.buffer.MovePosition(-int16(distance), 0) case 'E': distance := 1 @@ -117,8 +101,8 @@ CSI: } } - terminal.position.Line += distance - terminal.position.Col = 0 + terminal.buffer.MovePosition(0, int16(distance)) + terminal.buffer.SetPosition(0, terminal.buffer.CursorLine()) case 'F': @@ -130,12 +114,8 @@ CSI: distance = 1 } } - if terminal.position.Line-distance >= 0 { - terminal.position.Line -= distance - } else { - terminal.position.Line = 0 - } - terminal.position.Col = 0 + terminal.buffer.MovePosition(0, -int16(distance)) + terminal.buffer.SetPosition(0, terminal.buffer.CursorLine()) case 'G': @@ -148,7 +128,7 @@ CSI: } } - terminal.position.Col = distance - 1 // 1 based to 0 based + terminal.buffer.SetPosition(uint16(distance-1), terminal.buffer.CursorLine()) case 'H', 'f': @@ -168,19 +148,19 @@ CSI: } } } - terminal.position.Col = x - 1 - terminal.position.Line = y - 1 + + terminal.buffer.SetPosition(uint16(x-1), uint16(y-1)) default: switch param + intermediate + string(final) { case "?25h": - terminal.showCursor() + terminal.buffer.ShowCursor() case "?25l": - terminal.hideCursor() + terminal.buffer.HideCursor() case "?12h": - // todo enable cursor blink + terminal.buffer.SetCursorBlink(true) case "?12l": - // todo disable cursor blink + terminal.buffer.SetCursorBlink(false) default: return fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) } diff --git a/terminal/escapes.go b/terminal/escapes.go index a62976d..84abfa3 100644 --- a/terminal/escapes.go +++ b/terminal/escapes.go @@ -38,15 +38,11 @@ func (terminal *Terminal) processInput(ctx context.Context, buffer chan rune) { continue } - if b != 0x0d { - //lineOverflow = false - } - switch b { case 0x0a: terminal.buffer.NewLine() case 0x0d: - terminal.buffer.SetPosition(0, terminal.buffer.CursorLine()) + terminal.buffer.CarriageReturn() case 0x08: // backspace terminal.buffer.MovePosition(-1, 0) @@ -62,6 +58,5 @@ func (terminal *Terminal) processInput(ctx context.Context, buffer chan rune) { } } - terminal.triggerOnUpdate() } } diff --git a/terminal/osc.go b/terminal/osc.go index ca75847..77b2547 100644 --- a/terminal/osc.go +++ b/terminal/osc.go @@ -16,7 +16,7 @@ func oscHandler(buffer chan rune, terminal *Terminal) error { } title = append(title, b) } - terminal.title = string(title) + terminal.SetTitle(string(title)) } else { return fmt.Errorf("Invalid OSC 0 control sequence: 0x%02X", b) } diff --git a/terminal/terminal.go b/terminal/terminal.go index 0674015..bccd68d 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -16,15 +16,13 @@ import ( 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 + titleHandlers []chan bool } type Line struct { @@ -53,9 +51,8 @@ func New(pty *os.File, logger *zap.SugaredLogger, colourScheme ColourScheme) *Te }), pty: pty, logger: logger, - onUpdate: []func(){}, colourScheme: colourScheme, - cursorVisible: true, + titleHandlers: []chan bool{}, } } @@ -63,51 +60,31 @@ 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) AttachDisplayChangeHandler(handler chan bool) { + terminal.buffer.AttachDisplayChangeHandler(handler) } -func (terminal *Terminal) triggerOnUpdate() { - for _, handler := range terminal.onUpdate { - go handler() +func (terminal *Terminal) AttachTitleChangeHandler(handler chan bool) { + terminal.titleHandlers = append(terminal.titleHandlers, handler) +} + +func (terminal *Terminal) emitTitleChange() { + for _, h := range terminal.titleHandlers { + go func(c chan bool) { + c <- true + }(h) } } -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 } +func (terminal *Terminal) SetTitle(title string) { + terminal.title = title + terminal.emitTitleChange() +} + // Write sends data, i.e. locally typed keystrokes to the pty func (terminal *Terminal) Write(data []byte) error { _, err := terminal.pty.Write(data)