diff --git a/gui/gui.go b/gui/gui.go index b989e7c..299e638 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -5,6 +5,8 @@ import ( "math" "os" "runtime" + "sync" + "time" "github.com/4ydx/gltext" v41 "github.com/4ydx/gltext/v4.1" @@ -18,13 +20,17 @@ import ( ) type GUI struct { - window *glfw.Window - logger *zap.SugaredLogger - config config.Config - font *v41.Font - terminal *terminal.Terminal - width int - height int + window *glfw.Window + logger *zap.SugaredLogger + config config.Config + font *v41.Font + terminal *terminal.Terminal + width int + height int + charWidth float32 + charHeight float32 + texts [][]*v41.Text + textLock sync.Mutex } func New(config config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI { @@ -36,6 +42,7 @@ func New(config config.Config, terminal *terminal.Terminal, logger *zap.SugaredL width: 600, height: 300, terminal: terminal, + texts: [][]*v41.Text{}, } } @@ -57,14 +64,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { scaleMin, scaleMax := float32(1.0), float32(1.1) text := v41.NewText(gui.font, scaleMin, scaleMax) text.SetString("A") - cw, ch := text.Width(), text.Height() + gui.charWidth, gui.charHeight = text.Width(), text.Height() - cols := int(math.Floor(float64(float32(width) / cw))) - rows := int(math.Floor(float64(float32(height) / ch))) + cols := int(math.Floor(float64(float32(width) / gui.charWidth))) + rows := int(math.Floor(float64(float32(height) / gui.charHeight))) if err := gui.terminal.SetSize(cols, rows); err != nil { gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err) } + + gui.createTexts() } func (gui *GUI) getTermSize() (int, int) { @@ -75,16 +84,64 @@ func (gui *GUI) getTermSize() (int, int) { } // checks if the terminals cells have been updated, and updates the text objects if needed -func (gui *GUI) updateCells() { +func (gui *GUI) updateTexts() { + gui.textLock.Lock() + defer gui.textLock.Unlock() + + cols, rows := gui.getTermSize() + + for row := 0; row < rows; row++ { + for col := 0; col < cols; col++ { + + r, err := gui.terminal.GetRuneAtPos(terminal.Position{Row: row, Col: col}) + if err != nil { + gui.logger.Errorf("Failed to read rune: %s", err) + } + if r > 0 { + gui.texts[row][col].SetString(string(r)) + gui.texts[row][col].SetColor(mgl32.Vec3{1, 1, 1}) + // @todo set colour + } + } + } } // builds text objects -func (gui *GUI) createCells() { +func (gui *GUI) createTexts() { + gui.textLock.Lock() + defer gui.textLock.Unlock() scaleMin, scaleMax := float32(1.0), float32(1.1) - text := v41.NewText(gui.font, scaleMin, scaleMax) - text.SetString("hello") - text.SetColor(mgl32.Vec3{1, 1, 1}) - text.SetPosition(mgl32.Vec2{-float32(gui.width) / 2, -float32(gui.height) / 2}) + + cols, rows := gui.getTermSize() + + texts := [][]*v41.Text{} + for row := 0; row < rows; row++ { + + if len(texts) <= row { + texts = append(texts, []*v41.Text{}) + } + for col := 0; col < cols; col++ { + if len(texts[row]) <= col { + text := v41.NewText(gui.font, scaleMin, scaleMax) + + if row < len(gui.texts) { + if col < len(gui.texts[row]) { + text.SetString(gui.texts[row][col].String) + } + } + + text.SetColor(mgl32.Vec3{1, 1, 1}) + + x := ((float32(col) * gui.charWidth) - (float32(gui.width) / 2)) + (gui.charWidth / 2) + y := -(((float32(row) * gui.charHeight) - (float32(gui.height) / 2)) + (gui.charHeight / 2)) + + text.SetPosition(mgl32.Vec2{x, y}) + texts[row] = append(texts[row], text) + } + } + } + + gui.texts = texts } @@ -119,13 +176,22 @@ func (gui *GUI) Render() error { w, h := gui.window.GetFramebufferSize() gl.Viewport(0, 0, int32(w), int32(h)) + gui.logger.Debugf("Starting pty read handling...") + gui.terminal.OnUpdate(func() { + gui.updateTexts() + }) + go gui.terminal.Read() + scaleMin, scaleMax := float32(1.0), float32(1.1) text := v41.NewText(gui.font, scaleMin, scaleMax) - text.SetString("hello") - text.SetColor(mgl32.Vec3{1, 1, 1}) - text.SetPosition(mgl32.Vec2{-float32(gui.width) / 2, -float32(gui.height) / 2}) + text.SetString("") + text.SetColor(mgl32.Vec3{1, 0, 0}) text.SetPosition(mgl32.Vec2{0, 0}) + frames := 0 + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + //gl.Disable(gl.MULTISAMPLE) // stop smoothing fonts gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) @@ -133,17 +199,35 @@ func (gui *GUI) Render() error { gui.logger.Debugf("Starting render...") for !gui.window.ShouldClose() { + select { + case <-ticker.C: + text.SetString(fmt.Sprintf("%d fps | %d, %d", frames, gui.terminal.GetPosition().Row, gui.terminal.GetPosition().Col)) + frames = 0 + default: + } + gl.UseProgram(program) gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) // Render the string. gui.window.SetTitle(gui.terminal.GetTitle()) + gui.textLock.Lock() + cols, rows := gui.getTermSize() + + for row := 0; row < rows; row++ { + for col := 0; col < cols; col++ { + gui.texts[row][col].Draw() + } + } + gui.textLock.Unlock() + text.Draw() glfw.PollEvents() gui.window.SwapBuffers() + frames++ } gui.logger.Debugf("Stopping render...") diff --git a/main.go b/main.go index 59f4b7c..599937d 100644 --- a/main.go +++ b/main.go @@ -40,8 +40,6 @@ func main() { sugaredLogger.Infof("Creating terminal...") terminal := terminal.New(pty, sugaredLogger) - go terminal.Read() - go func() { time.Sleep(time.Second * 5) terminal.Write([]byte("tput cols && tput lines\n")) diff --git a/raft b/raft index 2f19098..6b1a478 100755 Binary files a/raft and b/raft differ diff --git a/terminal/terminal.go b/terminal/terminal.go index e67da59..ed877ee 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -17,6 +17,7 @@ type Terminal struct { logger *zap.SugaredLogger title string position Position + onUpdate []func() } type Winsize struct { @@ -33,9 +34,20 @@ type Position struct { func New(pty *os.File, logger *zap.SugaredLogger) *Terminal { return &Terminal{ - cells: [][]Cell{}, - pty: pty, - logger: logger, + cells: [][]Cell{}, + pty: pty, + logger: logger, + onUpdate: []func(){}, + } +} + +func (terminal *Terminal) OnUpdate(handler func()) { + terminal.onUpdate = append(terminal.onUpdate, handler) +} + +func (terminal *Terminal) triggerOnUpdate() { + for _, handler := range terminal.onUpdate { + go handler() } } @@ -49,6 +61,15 @@ func (terminal *Terminal) Write(data []byte) error { return err } +func (terminal *Terminal) ClearToEndOfLine() error { + w, _ := terminal.GetSize() + for i := terminal.position.Col; i < w; i++ { + // @todo handle errors? + terminal.setRuneAtPos(Position{Row: terminal.position.Row, Col: i}, 0) + } + return nil +} + // Read needs to be run on a goroutine, as it continually reads output to set on the terminal func (terminal *Terminal) Read() error { @@ -63,20 +84,21 @@ func (terminal *Terminal) Read() error { if b == 0x1b { // if the byte is an escape character, read the next byte to determine which one b = <-buffer - terminal.logger.Debugf("Escape: 0x%X", b) switch b { - case 0x5b: // CSI: Control Sequence Introducer + case 0x5b: // CSI: Control Sequence Introducer ] b = <-buffer switch b { - default: - terminal.logger.Debugf("Unknown CSI control sequence: 0x%X", b) - } + case 0x4b: // K - EOL - Erase to end of line + default: + terminal.logger.Debugf("Unknown CSI control sequence: 0x%02X (%s)", b, string([]byte{b})) + } case 0x5d: // OSC: Operating System Command b = <-buffer switch b { case byte('0'): - if <-buffer == byte(';') { + b = <-buffer + if b == byte(';') { title := []byte{} for { b = <-buffer @@ -88,17 +110,27 @@ func (terminal *Terminal) Read() error { terminal.logger.Debugf("Terminal title set to: %s", string(title)) terminal.title = string(title) } else { - terminal.logger.Debugf("Invalid OSC 0 control sequence") + terminal.logger.Debugf("Invalid OSC 0 control sequence: 0x%02X", b) } default: - terminal.logger.Debugf("Unknown OSC control sequence: 0x%X", b) + terminal.logger.Debugf("Unknown OSC control sequence: 0x%02X", b) } default: - terminal.logger.Debugf("Unknown control sequence: 0x%X", b) + terminal.logger.Debugf("Unknown control sequence: 0x%02X", b) } } else { - // render character at current location - terminal.writeRune([]rune(string([]byte{b}))[0]) + + switch b { + case 0x0a: + terminal.newLine() + case 0x0d: + terminal.position.Col = 0 + default: + // render character at current location + // fmt.Printf("%s\n", string([]byte{b})) + terminal.writeRune([]rune(string([]byte{b}))[0]) + } + } } @@ -112,15 +144,17 @@ func (terminal *Terminal) Read() error { } if len(readBytes) > 0 { readBytes = readBytes[:n] + fmt.Printf("Data in: %q\n", string(readBytes)) for _, x := range readBytes { buffer <- x } + terminal.triggerOnUpdate() } } } func (terminal *Terminal) writeRune(r rune) error { - fmt.Println(string(r)) + err := terminal.setRuneAtPos(terminal.position, r) if err != nil { return err @@ -139,15 +173,53 @@ func (terminal *Terminal) writeRune(r rune) error { return nil } +func (terminal *Terminal) newLine() { + _, h := terminal.GetSize() + terminal.position.Col = 0 + if terminal.position.Row <= h-1 { + terminal.position.Row++ + } else { + panic(fmt.Errorf("Not implemented - need to shuffle all rows up one")) + } + +} + +func (terminal *Terminal) Clear() { + for y := range terminal.cells { + for x := range terminal.cells[y] { + terminal.cells[y][x].rune = 0 + } + } + terminal.position = Position{0, 0} +} + +func (terminal *Terminal) GetPosition() Position { + return terminal.position +} + +func (terminal *Terminal) GetRuneAtPos(pos Position) (rune, error) { + if len(terminal.cells) <= pos.Row { + return 0, fmt.Errorf("Row %d does not exist", pos.Row) + } + + if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Col { + return 0, fmt.Errorf("Col %d does not exist", pos.Col) + } + + return terminal.cells[pos.Row][pos.Col].rune, nil +} + func (terminal *Terminal) setRuneAtPos(pos Position, r rune) error { - if len(terminal.cells) <= pos.Col { + if len(terminal.cells) <= pos.Row { + return fmt.Errorf("Row %d does not exist", pos.Row) + } + + if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Col { return fmt.Errorf("Col %d does not exist", pos.Col) } - if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Row { - return fmt.Errorf("Row %d does not exist", pos.Row) - } + //fmt.Printf("%d %d %s\n", pos.Row, pos.Col, string(r)) terminal.cells[pos.Row][pos.Col].rune = r return nil