mirror of
https://github.com/1f349/voidterm.git
synced 2024-12-21 23:44: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) {
|
||||
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
|
||||
|
||||
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