mirror of
https://github.com/1f349/voidterm.git
synced 2024-12-22 07:54:12 +00:00
Updates to make it more library-like
This commit is contained in:
parent
f88e5b3f21
commit
ab00b45d05
@ -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
104
cmd/voidterm/main.go
Normal 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
|
||||||
|
}
|
@ -57,3 +57,7 @@ func (cell *Cell) erase(bgColour color.Color) {
|
|||||||
func (cell *Cell) setRune(r MeasuredRune) {
|
func (cell *Cell) setRune(r MeasuredRune) {
|
||||||
cell.r = r
|
cell.r = r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EmptyCell() Cell {
|
||||||
|
return Cell{MeasuredRune{Rune: ' '}, CellAttributes{}}
|
||||||
|
}
|
||||||
|
51
viewframe.go
Normal file
51
viewframe.go
Normal 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
|
||||||
|
}
|
107
voidterm.go
107
voidterm.go
@ -1 +1,108 @@
|
|||||||
package voidterm
|
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()
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user