From 1a20091388dcce0e0d168ff43a95b88fd7678424 Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Sun, 1 Jul 2018 21:57:25 +0100 Subject: [PATCH] lots of unicode work --- README.md | 45 ++++- config/config.go | 19 +- gui/cell.go | 26 ++- gui/gui.go | 86 +++++---- gui/input.go | 21 ++- main.go | 67 +++++-- md | 0 shit | 1 + terminal/cell.go | 9 + terminal/colours.go | 2 + terminal/terminal.go | 424 +++++++++++++++++++++++++------------------ 11 files changed, 452 insertions(+), 248 deletions(-) create mode 100644 md create mode 100644 shit diff --git a/README.md b/README.md index da3d28e..8ac5c56 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -Raft is a terminal emulator utilising OpenGL. +Raft is a terminal emulator utilising OpenGL v4.1. -This project is purely a learning exercise right now. +The project is purely a learning exercise right now. + +Ensure you have your latest graphics card drivers installed before use. ## Build Dependencies @@ -9,11 +11,40 @@ This project is purely a learning exercise right now. - On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev xorg-dev`. - On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel`. +## Planned Features + +| Feature | Done | Notes | +|-------------------|------|-------| +| Pty allocation | ✔ | Linux only so far +| OpenGL rendering | ✔ | +| Resizing/content reordering | ✔ | +| ANSI escape codes | 50% | +| UTF-8 input | 90% | No copy + paste as yet +| UTF-8 output | ✘ | +| Copy/paste | ✘ | +| Customisable colour schemes | ✔ | Complete, but the config file has no entry for this yet +| Config file | 5% | +| Scrolling | 50% | Infinite buffer implemented, need GUI scrollbar & render updates +| Sweet render effects | ✘ | +|||| ## Platform Support -| Platform| Supported -|---------|------------ -| Linux | Yes -| MacOSX | Not yet... -| Windows | Not yet... +| Platform | Supported +|----------|------------ +| Linux | ✔ +| MacOSX | ✘ (nearly) +| Windows | ✘ + +## Configuration + +Raft looks for a config file in the following places: `~/.raft.yml`, `~/.raft/config.yml`, `~/.config/raft/config.yml` (earlier in the list prioritised). + +Example config: +``` +debug: False +``` + +The following options are available: +| Name | Type | Description +| debug | bool | Enables debug logging diff --git a/config/config.go b/config/config.go index 7079131..0e93c73 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,22 @@ package config +import ( + "gitlab.com/liamg/raft/terminal" + yaml "gopkg.in/yaml.v2" +) + type Config struct { - DebugMode bool + DebugMode bool `yaml:"debug"` + ColourScheme terminal.ColourScheme +} + +var DefaultConfig = Config{ + DebugMode: false, + ColourScheme: terminal.DefaultColourScheme, +} + +func Parse(data []byte) (*Config, error) { + c := DefaultConfig + err := yaml.Unmarshal(data, &c) + return &c, err } diff --git a/gui/cell.go b/gui/cell.go index 2f7cfb5..b5bdef5 100644 --- a/gui/cell.go +++ b/gui/cell.go @@ -14,14 +14,16 @@ type Cell struct { colourAttr uint32 points []float32 colour [3]float32 + hidden bool } -func (gui *GUI) NewCell(font *v41.Font, x float32, y float32, w float32, h float32, colourAttr uint32) Cell { +func (gui *GUI) NewCell(font *v41.Font, x float32, y float32, w float32, h float32, colourAttr uint32, bgColour [3]float32) Cell { cell := Cell{ text: v41.NewText(font, 1.0, 1.1), colourAttr: colourAttr, } + cell.colour = bgColour cell.text.SetPosition(mgl32.Vec2{x, y}) x = (x - (w / 2)) / (float32(gui.width) / 2) @@ -97,12 +99,17 @@ func (cell *Cell) makeVao() { } func (cell *Cell) DrawBg() { + if cell.hidden { + return + } gl.BindVertexArray(cell.vao) gl.DrawArrays(gl.TRIANGLES, 0, 6) } func (cell *Cell) DrawText() { - + if cell.hidden { + return + } if cell.text != nil { cell.text.Draw() } @@ -110,20 +117,21 @@ func (cell *Cell) DrawText() { } func (cell *Cell) Show() { - if cell.text != nil { - cell.text.Show() - } + cell.hidden = false } func (cell *Cell) Hide() { - if cell.text != nil { - cell.text.Hide() - } + cell.hidden = true } func (cell *Cell) SetRune(r rune) { if cell.text != nil { - cell.text.SetString(string(r)) + if r == '%' { + cell.text.SetString("%%") + } else { + cell.text.SetString(string(r)) + } + } } diff --git a/gui/gui.go b/gui/gui.go index 7037107..f15ef97 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -64,6 +64,8 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { gui.font.ResizeWindow(float32(width), float32(height)) } + gl.Viewport(0, 0, int32(gui.width), int32(gui.height)) + scaleMin, scaleMax := float32(1.0), float32(1.1) text := v41.NewText(gui.font, scaleMin, scaleMax) text.SetString("A") @@ -98,12 +100,7 @@ func (gui *GUI) updateTexts() { c, err := gui.terminal.GetCellAtPos(terminal.Position{Line: row, Col: col}) - if err != nil || c == nil { - gui.cells[row][col].Hide() - continue - } - - if c.IsHidden() { + if err != nil || c == nil || c.IsHidden() { gui.cells[row][col].Hide() continue } @@ -113,6 +110,13 @@ func (gui *GUI) updateTexts() { gui.cells[row][col].SetRune(c.GetRune()) gui.cells[row][col].Show() + if gui.terminal.GetPosition().Col == col && gui.terminal.GetPosition().Line == row { + gui.cells[row][col].SetBgColour( + gui.config.ColourScheme.Cursor[0], + gui.config.ColourScheme.Cursor[1], + gui.config.ColourScheme.Cursor[2], + ) + } } } } @@ -134,7 +138,7 @@ func (gui *GUI) createTexts() { x := ((float32(col) * gui.charWidth) - (float32(gui.width) / 2)) + (gui.charWidth / 2) y := -(((float32(row) * gui.charHeight) - (float32(gui.height) / 2)) + (gui.charHeight / 2)) - cells[row] = append(cells[row], gui.NewCell(gui.font, x, y, gui.charWidth, gui.charHeight, gui.colourAttr)) + cells[row] = append(cells[row], gui.NewCell(gui.font, x, y, gui.charWidth, gui.charHeight, gui.colourAttr, gui.config.ColourScheme.DefaultBg)) } } } @@ -171,8 +175,8 @@ func (gui *GUI) Render() error { gl.BindFragDataLocation(program, 0, gl.Str("outColour\x00")) gui.logger.Debugf("Loading font...") - //gui.font, err = gui.loadFont("/usr/share/fonts/nerd-fonts-complete/ttf/Roboto Mono Nerd Font Complete.ttf", 12) - if err := gui.loadFont("./fonts/CamingoCode-Regular.ttf", 12); err != nil { + if err := gui.loadFont("/usr/share/fonts/nerd-fonts-complete/ttf/Roboto Mono Nerd Font Complete.ttf", 12); err != nil { + //if err := gui.loadFont("./fonts/CamingoCode-Regular.ttf", 12); err != nil { return fmt.Errorf("Failed to load font: %s", err) } @@ -202,51 +206,60 @@ func (gui *GUI) Render() error { text.SetColor(mgl32.Vec3{1, 0, 0}) text.SetPosition(mgl32.Vec2{0, 0}) - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(time.Millisecond * 100) defer ticker.Stop() //gl.Disable(gl.MULTISAMPLE) // stop smoothing fonts gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - updateRequired := false + updateRequired := 0 gui.logger.Debugf("Starting render...") + gl.UseProgram(program) + // todo set bg colour - //bgColour := gui.terminal.colourScheme.DefaultBgColor - gl.ClearColor(0.1, 0.1, 0.1, 1.0) + //bgColour := + gl.ClearColor( + gui.config.ColourScheme.DefaultBg[0], + gui.config.ColourScheme.DefaultBg[1], + gui.config.ColourScheme.DefaultBg[2], + 1.0, + ) for !gui.window.ShouldClose() { - updateRequired = false + if updateRequired > 0 { - CheckUpdate: - for { - select { - case <-updateChan: - updateRequired = true - case <-ticker.C: - text.SetString(fmt.Sprintf("%dx%d", gui.cols, gui.rows)) - updateRequired = true - default: - break CheckUpdate + updateRequired-- + + } else { + CheckUpdate: + for { + select { + case <-updateChan: + updateRequired = 2 + case <-ticker.C: + text.SetString(fmt.Sprintf("%dx%d@%d,%d", gui.cols, gui.rows, gui.terminal.GetPosition().Col, gui.terminal.GetPosition().Line)) + updateRequired = 2 + default: + break CheckUpdate + } } } gl.UseProgram(program) - if updateRequired { - - gl.Viewport(0, 0, int32(gui.width), int32(gui.height)) + if updateRequired > 0 { gui.updateTexts() // Render the string. gui.window.SetTitle(gui.terminal.GetTitle()) + //gl.ClearColor(0.5, 0.5, 0.5, 1.0) gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) - cols, rows := gui.getTermSize() for row := 0; row < rows; row++ { @@ -257,16 +270,16 @@ func (gui *GUI) Render() error { for row := 0; row < rows; row++ { for col := 0; col < cols; col++ { - gui.cells[row][col].DrawText() } } - text.Draw() + // debug to show co-ords + //text.Draw() } glfw.PollEvents() - if updateRequired { + if updateRequired > 0 { gui.window.SwapBuffers() } } @@ -286,11 +299,12 @@ func (gui *GUI) loadFont(path string, scale int32) error { runeRanges := make(gltext.RuneRanges, 0) runeRanges = append(runeRanges, gltext.RuneRange{Low: 32, High: 127}) - /*runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x3000, High: 0x3030}) - runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x3040, High: 0x309f}) - runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x30a0, High: 0x30ff}) - runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x4e00, High: 0x9faf}) - runeRanges = append(runeRanges, gltext.RuneRange{Low: 0xff00, High: 0xffef}) + /* + runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x0, High: 0x3030}) + runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x3040, High: 0x309f}) + runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x30a0, High: 0x30ff}) + runeRanges = append(runeRanges, gltext.RuneRange{Low: 0x4e00, High: 0x9faf}) + runeRanges = append(runeRanges, gltext.RuneRange{Low: 0xff00, High: 0xffef}) */ runesPerRow := fixed.Int26_6(128) diff --git a/gui/input.go b/gui/input.go index c1aa5c7..a295171 100644 --- a/gui/input.go +++ b/gui/input.go @@ -14,10 +14,23 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti switch true { case mods&glfw.ModControl > 0: - switch key { - case glfw.KeyC: // ctrl^c - gui.logger.Debugf("Sending CTRL^C") - gui.terminal.Write([]byte{0x3}) // send EOT + + if mods&glfw.ModAlt > 0 { + // ctrl + alt + + switch key { + case glfw.KeyV: + // todo handle both these errors + if buf, err := gui.window.GetClipboardString(); err == nil { + _ = gui.terminal.Write([]byte(buf)) + } + } + } else { + // ctrl + + switch key { + case glfw.KeyC: // ctrl^c + gui.logger.Debugf("Sending CTRL^C") + gui.terminal.Write([]byte{0x3}) // send EOT + } } } diff --git a/main.go b/main.go index 668188e..490a1bf 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io/ioutil" "os" "gitlab.com/liamg/raft/config" @@ -11,10 +12,33 @@ import ( "go.uber.org/zap" ) -func main() { +func loadConfig() config.Config { - // parse this - conf := config.Config{DebugMode: true} + home := os.Getenv("HOME") + if home == "" { + return config.DefaultConfig + } + + places := []string{ + fmt.Sprintf("%s/.raft.yml", home), + fmt.Sprintf("%s/.raft/config.yml", home), + fmt.Sprintf("%s/.config/raft/config.yml", home), + } + + for _, place := range places { + if b, err := ioutil.ReadFile(place); err == nil { + if c, err := config.Parse(b); err == nil { + return *c + } else { + fmt.Printf("Invalid config at %s: %s\n", place, err) + } + } + } + + return config.DefaultConfig +} + +func getLogger(conf config.Config) (*zap.SugaredLogger, error) { var logger *zap.Logger var err error @@ -24,30 +48,35 @@ func main() { logger, err = zap.NewProduction() } if err != nil { - fmt.Printf("Failed to create logger: %s", err) + return nil, fmt.Errorf("Failed to create logger: %s", err) + } + return logger.Sugar(), nil +} + +func main() { + + // parse this + conf := loadConfig() + + logger, err := getLogger(conf) + if err != nil { + fmt.Printf("Failed to create logger: %s\n", err) os.Exit(1) } - sugaredLogger := logger.Sugar() - defer sugaredLogger.Sync() + defer logger.Sync() - sugaredLogger.Infof("Allocationg pty...") + logger.Infof("Allocating pty...") pty, err := pty.NewPtyWithShell() if err != nil { - sugaredLogger.Fatalf("Failed to allocate pty: %s", err) + logger.Fatalf("Failed to allocate pty: %s", err) } - sugaredLogger.Infof("Creating terminal...") - terminal := terminal.New(pty, sugaredLogger, terminal.DefaultColourScheme) - /* - go func() { - time.Sleep(time.Second * 1) - terminal.Write([]byte("tput cols && tput lines\n")) - terminal.Write([]byte("ls -la\n")) - }() - */ + logger.Infof("Creating terminal...") + terminal := terminal.New(pty, logger, conf.ColourScheme) - g := gui.New(conf, terminal, sugaredLogger) + g := gui.New(conf, terminal, logger) if err := g.Render(); err != nil { - sugaredLogger.Fatalf("Render error: %s", err) + logger.Fatalf("Render error: %s", err) } + } diff --git a/md b/md new file mode 100644 index 0000000..e69de29 diff --git a/shit b/shit new file mode 100644 index 0000000..aa381ae --- /dev/null +++ b/shit @@ -0,0 +1 @@ +☠ diff --git a/terminal/cell.go b/terminal/cell.go index 8d5c4bc..64df833 100644 --- a/terminal/cell.go +++ b/terminal/cell.go @@ -16,6 +16,15 @@ type CellAttributes struct { Hidden bool } +func (terminal *Terminal) NewCell() Cell { + return Cell{ + attr: CellAttributes{ + FgColour: terminal.colourScheme.DefaultFg, + BgColour: terminal.colourScheme.DefaultBg, + }, + } +} + func (cell *Cell) GetRune() rune { return cell.r } diff --git a/terminal/colours.go b/terminal/colours.go index 679c5e6..3263ee1 100644 --- a/terminal/colours.go +++ b/terminal/colours.go @@ -1,6 +1,7 @@ package terminal type ColourScheme struct { + Cursor [3]float32 DefaultFg [3]float32 BlackFg [3]float32 RedFg [3]float32 @@ -38,6 +39,7 @@ type ColourScheme struct { } var DefaultColourScheme = ColourScheme{ + Cursor: [3]float32{0.8, 0.8, 0.8}, //fg DefaultFg: [3]float32{1, 1, 1}, BlackFg: [3]float32{0, 0, 0}, diff --git a/terminal/terminal.go b/terminal/terminal.go index fd77c62..747ee5b 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -1,6 +1,7 @@ package terminal import ( + "bufio" "fmt" "os" "strconv" @@ -126,6 +127,10 @@ func (terminal *Terminal) incrementPosition() { position := terminal.getPosition() if position.Col+1 >= int(terminal.size.Width) { position.Line++ + _, h := terminal.GetSize() + if position.Line >= h { + position.Line-- + } position.Col = 0 } else { position.Col++ @@ -137,6 +142,10 @@ func (terminal *Terminal) SetPosition(position Position) { terminal.position = position } +func (terminal *Terminal) GetPosition() Position { + return terminal.position +} + func (terminal *Terminal) GetTitle() string { return terminal.title } @@ -151,11 +160,13 @@ func (terminal *Terminal) ClearToEndOfLine() { position := terminal.getPosition() - if position.Line < len(terminal.lines) { - if position.Col < len(terminal.lines[position.Line].Cells) { - terminal.lines[position.Line].Cells = terminal.lines[position.Line].Cells[:position.Col] + line := terminal.getBufferedLine(position.Line) + if line != nil { + if position.Col < len(line.Cells) { + line.Cells = line.Cells[:position.Col] } } + } // we have thousands of lines of output. if the terminal is X lines high, we just want to lookat the most recent X lines to render (unless scroll etc) @@ -172,179 +183,246 @@ func (terminal *Terminal) getBufferedLine(line int) *Line { return &terminal.lines[line] } +func (terminal *Terminal) processInput(buffer chan rune) { + + // https://en.wikipedia.org/wiki/ANSI_escape_code + + for { + b := <-buffer + + if b == 0x1b { // if the byte is an escape character, read the next byte to determine which one + b = <-buffer + switch b { + case 0x5b: // CSI: Control Sequence Introducer ] + var final rune + params := []rune{} + intermediate := []rune{} + CSI: + for { + b = <-buffer + switch true { + case b >= 0x30 && b <= 0x3F: + params = append(params, b) + case b >= 0x20 && b <= 0x2F: + intermediate = append(intermediate, b) + case b >= 0x40 && b <= 0x7e: + final = b + break CSI + } + } + + switch final { + case rune('A'): + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(string(params[0])) + if err != nil { + distance = 1 + } + } + if terminal.position.Line-distance >= 0 { + terminal.position.Line -= distance + } + case rune('B'): + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(string(params[0])) + if err != nil { + distance = 1 + } + } + + terminal.position.Line += distance + + case 0x4b: // K - EOL - Erase to end of line + if len(params) == 0 || params[0] == rune('0') { + terminal.ClearToEndOfLine() + } else { + terminal.logger.Errorf("Unsupported EL") + } + case rune('m'): + // SGR: colour and shit + sgr := string(params) + sgrParams := strings.Split(sgr, ";") + for i := range sgrParams { + param := sgrParams[i] + switch param { + case "0": + terminal.cellAttr = terminal.defaultCellAttr + case "1": + terminal.cellAttr.Bold = true + case "2": + terminal.cellAttr.Dim = true + case "4": + terminal.cellAttr.Underline = true + case "5": + terminal.cellAttr.Blink = true + case "7": + terminal.cellAttr.Reverse = true + case "8": + terminal.cellAttr.Hidden = true + case "21": + terminal.cellAttr.Bold = false + case "22": + terminal.cellAttr.Dim = false + case "24": + terminal.cellAttr.Underline = false + case "25": + terminal.cellAttr.Blink = false + case "27": + terminal.cellAttr.Reverse = false + case "28": + terminal.cellAttr.Hidden = false + case "39": + terminal.cellAttr.FgColour = terminal.colourScheme.DefaultFg + case "30": + terminal.cellAttr.FgColour = terminal.colourScheme.BlackFg + case "31": + terminal.cellAttr.FgColour = terminal.colourScheme.RedFg + case "32": + terminal.cellAttr.FgColour = terminal.colourScheme.GreenFg + case "33": + terminal.cellAttr.FgColour = terminal.colourScheme.YellowFg + case "34": + terminal.cellAttr.FgColour = terminal.colourScheme.BlueFg + case "35": + terminal.cellAttr.FgColour = terminal.colourScheme.MagentaFg + case "36": + terminal.cellAttr.FgColour = terminal.colourScheme.CyanFg + case "37": + terminal.cellAttr.FgColour = terminal.colourScheme.LightGreyFg + case "90": + terminal.cellAttr.FgColour = terminal.colourScheme.DarkGreyFg + case "91": + terminal.cellAttr.FgColour = terminal.colourScheme.LightRedFg + case "92": + terminal.cellAttr.FgColour = terminal.colourScheme.LightGreenFg + case "93": + terminal.cellAttr.FgColour = terminal.colourScheme.LightYellowFg + case "94": + terminal.cellAttr.FgColour = terminal.colourScheme.LightBlueFg + case "95": + terminal.cellAttr.FgColour = terminal.colourScheme.LightMagentaFg + case "96": + terminal.cellAttr.FgColour = terminal.colourScheme.LightCyanFg + case "97": + terminal.cellAttr.FgColour = terminal.colourScheme.WhiteFg + case "49": + terminal.cellAttr.BgColour = terminal.colourScheme.DefaultBg + case "40": + terminal.cellAttr.BgColour = terminal.colourScheme.BlackBg + case "41": + terminal.cellAttr.BgColour = terminal.colourScheme.RedBg + case "42": + terminal.cellAttr.BgColour = terminal.colourScheme.GreenBg + case "43": + terminal.cellAttr.BgColour = terminal.colourScheme.YellowBg + case "44": + terminal.cellAttr.BgColour = terminal.colourScheme.BlueBg + case "45": + terminal.cellAttr.BgColour = terminal.colourScheme.MagentaBg + case "46": + terminal.cellAttr.BgColour = terminal.colourScheme.CyanBg + case "47": + terminal.cellAttr.BgColour = terminal.colourScheme.LightGreenBg + case "100": + terminal.cellAttr.BgColour = terminal.colourScheme.DarkGreyBg + case "101": + terminal.cellAttr.BgColour = terminal.colourScheme.LightRedBg + case "102": + terminal.cellAttr.BgColour = terminal.colourScheme.LightGreenBg + case "103": + terminal.cellAttr.BgColour = terminal.colourScheme.LightYellowBg + case "104": + terminal.cellAttr.BgColour = terminal.colourScheme.LightBlueBg + case "105": + terminal.cellAttr.BgColour = terminal.colourScheme.LightMagentaBg + case "106": + terminal.cellAttr.BgColour = terminal.colourScheme.LightCyanBg + case "107": + terminal.cellAttr.BgColour = terminal.colourScheme.WhiteBg + + } + } + + default: + b = <-buffer + terminal.logger.Errorf("Unknown CSI control sequence: 0x%02X (%s)", final, string(final)) + } + case 0x5d: // OSC: Operating System Command + b = <-buffer + switch b { + case rune('0'): + b = <-buffer + if b == rune(';') { + title := []rune{} + for { + b = <-buffer + if b == 0x07 { + break + } + title = append(title, b) + } + terminal.logger.Debugf("Terminal title set to: %s", string(title)) + terminal.title = string(title) + } else { + terminal.logger.Errorf("Invalid OSC 0 control sequence: 0x%02X", b) + } + default: + terminal.logger.Errorf("Unknown OSC control sequence: 0x%02X", b) + } + case rune('c'): + terminal.logger.Errorf("RIS not yet supported") + case rune(')'), rune('('): + b = <-buffer + terminal.logger.Debugf("Ignoring character set control code )%s", string(b)) + default: + terminal.logger.Errorf("Unknown control sequence: 0x%02X [%s]", b, string(b)) + } + } else { + + switch b { + case 0x0a: + terminal.position.Line++ + _, h := terminal.GetSize() + if terminal.position.Line >= h { + terminal.position.Line-- + } + terminal.lines = append(terminal.lines, NewLine()) + case 0x0d: + terminal.position.Col = 0 + case 0x08: + // backspace + terminal.position.Col-- + case 0x07: + // @todo ring bell + default: + // render character at current location + // fmt.Printf("%s\n", string([]byte{b})) + terminal.writeRune(b) + } + + } + terminal.triggerOnUpdate() + } +} + // 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 byte, 0xffff) + buffer := make(chan rune, 0xffff) - go func() { - - // https://en.wikipedia.org/wiki/ANSI_escape_code - - for { - b := <-buffer - - if b == 0x1b { // if the byte is an escape character, read the next byte to determine which one - b = <-buffer - switch b { - case 0x5b: // CSI: Control Sequence Introducer ] - var final byte - params := []byte{} - intermediate := []byte{} - CSI: - for { - b = <-buffer - switch true { - case b >= 0x30 && b <= 0x3F: - params = append(params, b) - case b >= 0x20 && b <= 0x2F: - intermediate = append(intermediate, b) - case b >= 0x40 && b <= 0x7e: - final = b - break CSI - } - } - - switch final { - case byte('A'): - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(string([]byte{params[0]})) - if err != nil { - distance = 1 - } - } - if terminal.position.Line-distance >= 0 { - terminal.position.Line -= distance - } - case byte('B'): - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(string([]byte{params[0]})) - if err != nil { - distance = 1 - } - } - - terminal.position.Line += distance - - case 0x4b: // K - EOL - Erase to end of line - if len(params) == 0 || params[0] == byte('0') { - terminal.ClearToEndOfLine() - } else { - terminal.logger.Errorf("Unsupported EL") - } - case byte('m'): - // SGR: colour and shit - sgr := string(params) - sgrParams := strings.Split(sgr, ";") - for i := range sgrParams { - param := sgrParams[i] - switch param { - case "0": - terminal.cellAttr = terminal.defaultCellAttr - case "1": - terminal.cellAttr.Bold = true - case "2": - terminal.cellAttr.Dim = true - case "4": - terminal.cellAttr.Underline = true - case "5": - terminal.cellAttr.Blink = true - case "7": - terminal.cellAttr.Reverse = true - case "8": - terminal.cellAttr.Hidden = true - case "21": - terminal.cellAttr.Bold = false - case "22": - terminal.cellAttr.Dim = false - case "24": - terminal.cellAttr.Underline = false - case "25": - terminal.cellAttr.Blink = false - case "27": - terminal.cellAttr.Reverse = false - case "28": - terminal.cellAttr.Hidden = false - case "39": - - } - } - terminal.logger.Debugf("SGR params %#v intermediate %#v", params, intermediate) - default: - b = <-buffer - terminal.logger.Debugf("Unknown CSI control sequence: 0x%02X (%s)", final, string([]byte{final})) - } - case 0x5d: // OSC: Operating System Command - b = <-buffer - switch b { - case byte('0'): - b = <-buffer - if b == byte(';') { - title := []byte{} - for { - b = <-buffer - if b == 0x07 { - break - } - title = append(title, b) - } - terminal.logger.Debugf("Terminal title set to: %s", string(title)) - terminal.title = string(title) - } else { - terminal.logger.Debugf("Invalid OSC 0 control sequence: 0x%02X", b) - } - default: - terminal.logger.Debugf("Unknown OSC control sequence: 0x%02X", b) - } - case byte('c'): - terminal.logger.Errorf("RIS not yet supported") - default: - terminal.logger.Debugf("Unknown control sequence: 0x%02X", b) - } - } else { - - switch b { - case 0x0a: - for terminal.position.Line+1 >= len(terminal.lines) { - terminal.lines = append(terminal.lines, NewLine()) - } - terminal.position.Line++ - case 0x0d: - terminal.position.Col = 0 - case 0x08: - // backspace - terminal.position.Col-- - case 0x07: - // @todo ring bell - default: - // render character at current location - // fmt.Printf("%s\n", string([]byte{b})) - terminal.writeRune([]rune(string([]byte{b}))[0]) - } - - } - terminal.triggerOnUpdate() - } - }() + reader := bufio.NewReader(terminal.pty) + go terminal.processInput(buffer) for { - readBytes := make([]byte, 1024) - n, err := terminal.pty.Read(readBytes) + r, size, err := reader.ReadRune() if err != nil { - terminal.logger.Errorf("Failed to read from pty: %s", err) return err - } - if len(readBytes) > 0 { - readBytes = readBytes[:n] - fmt.Printf("Data in: %q\n", string(readBytes)) - for _, x := range readBytes { - buffer <- x - } - + } else if size > 0 { + buffer <- r } } } @@ -380,7 +458,7 @@ func (terminal *Terminal) GetCellAtPos(pos Position) (*Cell, error) { return nil, fmt.Errorf("Line missing") } for pos.Col >= len(line.Cells) { - line.Cells = append(line.Cells, Cell{}) + line.Cells = append(line.Cells, terminal.NewCell()) } return &line.Cells[pos.Col], nil } @@ -396,15 +474,17 @@ func (terminal *Terminal) setRuneAtPos(pos Position, r rune) error { fmt.Printf("\n\nSetting %d %d to %q\n\n\n", pos.Line, pos.Col, string(r)) } + for terminal.position.Line >= len(terminal.lines) { + terminal.lines = append(terminal.lines, NewLine()) + } + line := terminal.getBufferedLine(pos.Line) if line == nil { - return fmt.Errorf("Impossible?") - } for pos.Col >= len(line.Cells) { - line.Cells = append(line.Cells, Cell{}) + line.Cells = append(line.Cells, terminal.NewCell()) } line.Cells[pos.Col].attr = terminal.cellAttr