Updates to make it more library-like

This commit is contained in:
Melon 2024-02-25 00:59:22 +00:00
parent f88e5b3f21
commit ab00b45d05
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
7 changed files with 266 additions and 220 deletions

View File

@ -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
}
}

104
cmd/voidterm/main.go Normal file
View File

@ -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
}

View File

@ -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{}}
}

51
viewframe.go Normal file
View File

@ -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
}

View File

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