diff --git a/cmd/voidterm-debug/main.go b/cmd/voidterm-debug/main.go deleted file mode 100644 index 0c012b1..0000000 --- a/cmd/voidterm-debug/main.go +++ /dev/null @@ -1,220 +0,0 @@ -package main - -import ( - "context" - _ "embed" - "encoding/hex" - "fmt" - "github.com/1f349/voidterm" - "github.com/1f349/voidterm/termutil" - "github.com/creack/pty" - docker "github.com/fsouza/go-dockerclient" - "github.com/gorilla/websocket" - "html/template" - "io" - "log" - "net/http" - "os" - "strings" - "sync/atomic" -) - -var upgrader = websocket.Upgrader{} - -//go:embed index.go.html -var indexPage string - -//go:embed keysight.umd.js -var keysightJs string - -type TermSend struct { - Text string -} - -type TermAction struct { - Code byte -} - -func main() { - fmt.Println("PID:", os.Getpid()) - updateChan := make(chan struct{}) - - client, err := docker.NewClient("unix:///var/run/docker.sock") - if err != nil { - log.Fatal(err) - } - execInst, err := client.CreateExec(docker.CreateExecOptions{ - Cmd: []string{"/bin/sh"}, - Container: "07d2ab561d0a", - User: "root", - WorkingDir: "/", - Context: context.Background(), - AttachStdin: true, - AttachStdout: true, - Tty: true, - }) - if err != nil { - log.Fatal(err) - } - - var rows uint16 = 40 - var cols uint16 = 132 - - pty1, tty1, err := pty.Open() - if err != nil { - log.Fatal(err) - } - pty.Setsize(pty1, &pty.Winsize{Rows: rows, Cols: cols}) - - ir, iw := io.Pipe() - or, ow := io.Pipe() - - go func() { - err = client.StartExec(execInst.ID, docker.StartExecOptions{ - InputStream: ir, - OutputStream: ow, - ErrorStream: nil, - Tty: true, - RawTerminal: true, - Context: context.Background(), - }) - if err != nil { - log.Fatal(err) - } - }() - - err = client.ResizeExecTTY(execInst.ID, int(rows), int(cols)) - if err != nil { - log.Fatal(err) - } - - go func() { - r := io.TeeReader(or, hex.NewEncoder(os.Stdout)) - _, _ = io.Copy(tty1, r) - }() - - term := termutil.New(termutil.WithWindowManipulator(&voidterm.FakeWindow{Rows: rows, Cols: cols})) - go func() { - err = term.Run(updateChan, rows, cols, pty1) - if err != nil { - log.Fatal(err) - } - }() - - var outputString atomic.Pointer[string] - { - a := "" - outputString.Store(&a) - } - outputChan := make(chan string, 1) - - go func() { - for { - <-updateChan - fmt.Println("Update buffer") - a := viewToString(drawContent(term.GetActiveBuffer())) - outputString.Store(&a) - outputChan <- a - } - }() - - htmlPageTmpl, err := template.New("page").Parse(indexPage) - if err != nil { - log.Fatal(err) - } - - go func() { - err := http.ListenAndServe(":8080", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.Header.Get("Upgrade") == "websocket" { - c, err := upgrader.Upgrade(rw, req, nil) - if err != nil { - return - } - _ = c.WriteJSON(TermSend{Text: *outputString.Load()}) - go func() { - for { - a := <-outputChan - err := c.WriteJSON(TermSend{Text: a}) - if err != nil { - return - } - } - }() - for { - var a TermAction - err := c.ReadJSON(&a) - if err != nil { - return - } - _, _ = iw.Write([]byte{a.Code}) - } - } - - rw.Header().Set("Content-Type", "text/html; charset=utf-8") - rw.WriteHeader(http.StatusOK) - var vv []map[string]string - for _, i := range []byte{'C' - '@', 'G' - '@', 'X' - '@', '\n'} { - t := fmt.Sprintf("Ctrl+%c", i+'@') - if i == '\n' { - t = "Enter" - } - vv = append(vv, map[string]string{"Hex": fmt.Sprintf("%02x", i), "Text": t}) - } - _ = htmlPageTmpl.Execute(rw, map[string]any{ - "Keysight": template.JS(keysightJs), - "Buttons": vv, - }) - })) - if err != nil { - log.Fatal(err) - } - }() - - done := make(chan struct{}, 1) - <-done -} - -func viewToString(view [][]rune) string { - var sb strings.Builder - for _, row := range view { - for _, cell := range row { - sb.WriteRune(cell) - } - sb.WriteByte('\n') - } - return sb.String() -} - -func drawContent(buffer *termutil.Buffer) [][]rune { - view := make([][]rune, buffer.ViewHeight()) - for i := range view { - view[i] = make([]rune, buffer.ViewWidth()) - } - - // draw base content for each row - for viewY := int(buffer.ViewHeight() - 1); viewY >= 0; viewY-- { - drawRow(view, buffer, viewY) - } - return view -} - -func drawRow(view [][]rune, buffer *termutil.Buffer, viewY int) { - rowView := view[viewY] - - for i := range rowView { - rowView[i] = ' ' - } - - // draw text content of each cell in row - for viewX := uint16(0); viewX < buffer.ViewWidth(); viewX++ { - cell := buffer.GetCell(viewX, uint16(viewY)) - - // we don't need to draw empty cells - if cell == nil || cell.Rune().Rune == 0 { - continue - } - - // draw the text for the cell - rowView[viewX] = cell.Rune().Rune - } -} diff --git a/cmd/voidterm-debug/index.go.html b/cmd/voidterm/index.go.html similarity index 100% rename from cmd/voidterm-debug/index.go.html rename to cmd/voidterm/index.go.html diff --git a/cmd/voidterm-debug/keysight.umd.js b/cmd/voidterm/keysight.umd.js similarity index 100% rename from cmd/voidterm-debug/keysight.umd.js rename to cmd/voidterm/keysight.umd.js diff --git a/cmd/voidterm/main.go b/cmd/voidterm/main.go new file mode 100644 index 0000000..070946b --- /dev/null +++ b/cmd/voidterm/main.go @@ -0,0 +1,104 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "github.com/1f349/voidterm" + "github.com/gorilla/websocket" + "html/template" + "log" + "net/http" + "os" +) + +var upgrader = websocket.Upgrader{} + +//go:embed index.go.html +var indexPage string + +//go:embed keysight.umd.js +var keysightJs string + +type TermSend struct { + Text string +} + +type TermAction struct { + Code byte +} + +func main() { + dockerEndpoint := flag.String("docker", "unix:///var/run/docker.sock", "docker endpoint") + contId := flag.String("c", "", "container ID") + contUser := flag.String("u", "", "container user") + rows := flag.Uint64("rows", 40, "number of rows") + cols := flag.Uint64("cols", 132, "number of columns") + flag.Parse() + + fmt.Println("PID:", os.Getpid()) + term, err := voidterm.New(*dockerEndpoint, *contId, *contUser, "/", []string{"/bin/sh"}, uint16(*rows), uint16(*cols)) + if err != nil { + log.Fatal(err) + } + go func() { + if err := term.Run(); err != nil { + log.Fatal(err) + } + }() + + htmlPageTmpl, err := template.New("page").Parse(indexPage) + if err != nil { + log.Fatal(err) + } + + go func() { + err := http.ListenAndServe(":8080", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Header.Get("Upgrade") == "websocket" { + c, err := upgrader.Upgrade(rw, req, nil) + if err != nil { + return + } + _ = c.WriteJSON(TermSend{Text: term.LastFrame().RenderRawString()}) + go func() { + for { + a := <-term.FrameChan + err := c.WriteJSON(TermSend{Text: a.RenderRawString()}) + if err != nil { + return + } + } + }() + for { + var a TermAction + err := c.ReadJSON(&a) + if err != nil { + return + } + _, _ = term.PipeInput.Write([]byte{a.Code}) + } + } + + rw.Header().Set("Content-Type", "text/html; charset=utf-8") + rw.WriteHeader(http.StatusOK) + var vv []map[string]string + for _, i := range []byte{'C' - '@', 'G' - '@', 'X' - '@', '\n'} { + t := fmt.Sprintf("Ctrl+%c", i+'@') + if i == '\n' { + t = "Enter" + } + vv = append(vv, map[string]string{"Hex": fmt.Sprintf("%02x", i), "Text": t}) + } + _ = htmlPageTmpl.Execute(rw, map[string]any{ + "Keysight": template.JS(keysightJs), + "Buttons": vv, + }) + })) + if err != nil { + log.Fatal(err) + } + }() + + done := make(chan struct{}, 1) + <-done +} diff --git a/termutil/cell.go b/termutil/cell.go index edfb7c9..a14effb 100644 --- a/termutil/cell.go +++ b/termutil/cell.go @@ -57,3 +57,7 @@ func (cell *Cell) erase(bgColour color.Color) { func (cell *Cell) setRune(r MeasuredRune) { cell.r = r } + +func EmptyCell() Cell { + return Cell{MeasuredRune{Rune: ' '}, CellAttributes{}} +} diff --git a/viewframe.go b/viewframe.go new file mode 100644 index 0000000..a5d814a --- /dev/null +++ b/viewframe.go @@ -0,0 +1,51 @@ +package voidterm + +import ( + "github.com/1f349/voidterm/termutil" + "strings" +) + +type ViewFrame struct { + // Cells stores individual cells in [row][col] format + Cells [][]*termutil.Cell +} + +func (v *ViewFrame) RenderRawString() string { + var sb strings.Builder + for _, row := range v.Cells { + for _, cell := range row { + if cell == nil { + sb.WriteRune(' ') + } else { + sb.WriteRune(cell.Rune().Rune) + } + } + sb.WriteByte('\n') + } + return sb.String() +} + +func ViewFrameFromBuffer(buffer *termutil.Buffer) *ViewFrame { + frame := &ViewFrame{} + frame.Cells = make([][]*termutil.Cell, buffer.ViewHeight()) + for i := range frame.Cells { + frame.Cells[i] = make([]*termutil.Cell, buffer.ViewWidth()) + } + + // draw base content for each row + for viewY := uint16(0); viewY < buffer.ViewHeight(); viewY++ { + for viewX := uint16(0); viewX < buffer.ViewWidth(); viewX++ { + cell := buffer.GetCell(viewX, viewY) + + // we don't need to draw empty cells + if cell == nil || cell.Rune().Rune == 0 { + continue + } + + a := *cell + frame.Cells[viewY][viewX] = &a + } + } + + return frame +} diff --git a/voidterm.go b/voidterm.go index be5df69..047d955 100644 --- a/voidterm.go +++ b/voidterm.go @@ -1 +1,108 @@ package voidterm + +import ( + "context" + "encoding/hex" + "github.com/1f349/voidterm/termutil" + "github.com/creack/pty" + docker "github.com/fsouza/go-dockerclient" + "io" + "os" + "sync/atomic" +) + +type VoidTerm struct { + rows uint16 + cols uint16 + updateChan chan struct{} + dockerClient *docker.Client + execInst *docker.Exec + pty1 *os.File + tty1 *os.File + execClose docker.CloseWaiter + term *termutil.Terminal + lastFrame atomic.Pointer[ViewFrame] + FrameChan chan *ViewFrame + PipeInput io.Writer +} + +func New(dockerEndpoint, container, user, workingDir string, cmd []string, rows, cols uint16) (v *VoidTerm, err error) { + v = &VoidTerm{ + rows: rows, + cols: cols, + updateChan: make(chan struct{}), + FrameChan: make(chan *ViewFrame, 1), + } + v.lastFrame.Store(&ViewFrame{}) + + v.dockerClient, err = docker.NewClient(dockerEndpoint) + if err != nil { + return nil, err + } + v.execInst, err = v.dockerClient.CreateExec(docker.CreateExecOptions{ + Cmd: cmd, + Container: container, + User: user, + WorkingDir: workingDir, + Context: context.Background(), + AttachStdin: true, + AttachStdout: true, + Tty: true, + }) + if err != nil { + return nil, err + } + + v.pty1, v.tty1, err = pty.Open() + if err != nil { + return nil, err + } + if err := pty.Setsize(v.pty1, &pty.Winsize{Rows: rows, Cols: cols}); err != nil { + return nil, err + } + + ir, iw := io.Pipe() + or, ow := io.Pipe() + v.PipeInput = iw + + v.execClose, err = v.dockerClient.StartExecNonBlocking(v.execInst.ID, docker.StartExecOptions{ + InputStream: ir, + OutputStream: ow, + Tty: true, + RawTerminal: true, + Context: context.Background(), + }) + if err != nil { + return nil, err + } + err = v.dockerClient.ResizeExecTTY(v.execInst.ID, int(rows), int(cols)) + if err != nil { + return nil, err + } + + go func() { + r := io.TeeReader(or, hex.NewEncoder(os.Stdout)) + _, _ = io.Copy(v.tty1, r) + }() + + v.term = termutil.New(termutil.WithWindowManipulator(&FakeWindow{Rows: rows, Cols: cols})) + + go func() { + for { + <-v.updateChan + frame := ViewFrameFromBuffer(v.term.GetActiveBuffer()) + v.lastFrame.Store(frame) + v.FrameChan <- frame + } + }() + + return +} + +func (v *VoidTerm) Run() error { + return v.term.Run(v.updateChan, v.rows, v.cols, v.pty1) +} + +func (v *VoidTerm) LastFrame() *ViewFrame { + return v.lastFrame.Load() +}