feat(muxterm): wire protocol types and parser

This commit is contained in:
Ken
2026-05-28 06:11:35 +00:00
parent 3f0e31007f
commit b72d82624d
2 changed files with 296 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
package main
import "encoding/json"
// --- Client-to-server message types ---
// AttachMsg requests attaching to a named tmux session.
type AttachMsg struct {
Attach string `json:"attach"`
}
// ResizeMsg requests a PTY resize.
type ResizeMsg struct {
Resize struct {
Cols int `json:"cols"`
Rows int `json:"rows"`
} `json:"resize"`
}
// DetachMsg requests detaching from the current session.
type DetachMsg struct {
Detach bool `json:"detach"`
}
// --- Server-to-client message types ---
// AttachedMsg confirms a successful attach.
type AttachedMsg struct {
Attached string `json:"attached"`
}
// ErrorMsg reports an error to the client.
type ErrorMsg struct {
Error string `json:"error"`
}
// ExitedMsg reports that a session's process has exited.
type ExitedMsg struct {
Exited string `json:"exited"`
}
// ParseControlMessage unmarshals a JSON control message and returns the
// message kind and a pointer to the typed value. Returns ("invalid", nil)
// for unparseable JSON and ("unknown", nil) for unrecognised keys.
func ParseControlMessage(data []byte) (string, interface{}) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return "invalid", nil
}
if _, ok := raw["attach"]; ok {
var msg AttachMsg
if err := json.Unmarshal(data, &msg); err != nil {
return "invalid", nil
}
return "attach", &msg
}
if _, ok := raw["resize"]; ok {
var msg ResizeMsg
if err := json.Unmarshal(data, &msg); err != nil {
return "invalid", nil
}
return "resize", &msg
}
if _, ok := raw["detach"]; ok {
var msg DetachMsg
if err := json.Unmarshal(data, &msg); err != nil {
return "invalid", nil
}
return "detach", &msg
}
return "unknown", nil
}
// MarshalAttached returns JSON for an AttachedMsg.
func MarshalAttached(session string) []byte {
b, _ := json.Marshal(AttachedMsg{Attached: session})
return b
}
// MarshalError returns JSON for an ErrorMsg.
func MarshalError(msg string) []byte {
b, _ := json.Marshal(ErrorMsg{Error: msg})
return b
}
// MarshalExited returns JSON for an ExitedMsg.
func MarshalExited(session string) []byte {
b, _ := json.Marshal(ExitedMsg{Exited: session})
return b
}
+202
View File
@@ -0,0 +1,202 @@
package main
import (
"encoding/json"
"testing"
)
// --- Client-to-server message type tests ---
func TestAttachMsgJSON(t *testing.T) {
msg := AttachMsg{Attach: "my-session"}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal AttachMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
if m["attach"] != "my-session" {
t.Errorf("expected attach=my-session, got %v", m["attach"])
}
}
func TestResizeMsgJSON(t *testing.T) {
msg := ResizeMsg{Resize: struct {
Cols int `json:"cols"`
Rows int `json:"rows"`
}{Cols: 80, Rows: 24}}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal ResizeMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
resize, ok := m["resize"].(map[string]interface{})
if !ok {
t.Fatalf("expected resize to be a map, got %T", m["resize"])
}
if resize["cols"].(float64) != 80 {
t.Errorf("expected cols=80, got %v", resize["cols"])
}
if resize["rows"].(float64) != 24 {
t.Errorf("expected rows=24, got %v", resize["rows"])
}
}
func TestDetachMsgJSON(t *testing.T) {
msg := DetachMsg{Detach: true}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal DetachMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
if m["detach"] != true {
t.Errorf("expected detach=true, got %v", m["detach"])
}
}
// --- Server-to-client message type tests ---
func TestAttachedMsgJSON(t *testing.T) {
msg := AttachedMsg{Attached: "sess1"}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal AttachedMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
if m["attached"] != "sess1" {
t.Errorf("expected attached=sess1, got %v", m["attached"])
}
}
func TestErrorMsgJSON(t *testing.T) {
msg := ErrorMsg{Error: "something broke"}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal ErrorMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
if m["error"] != "something broke" {
t.Errorf("expected error='something broke', got %v", m["error"])
}
}
func TestExitedMsgJSON(t *testing.T) {
msg := ExitedMsg{Exited: "sess1"}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal ExitedMsg: %v", err)
}
var m map[string]interface{}
json.Unmarshal(data, &m)
if m["exited"] != "sess1" {
t.Errorf("expected exited=sess1, got %v", m["exited"])
}
}
// --- ParseControlMessage tests ---
func TestParseControlMessage_Attach(t *testing.T) {
data := []byte(`{"attach":"dev"}`)
kind, val := ParseControlMessage(data)
if kind != "attach" {
t.Fatalf("expected kind=attach, got %q", kind)
}
msg, ok := val.(*AttachMsg)
if !ok {
t.Fatalf("expected *AttachMsg, got %T", val)
}
if msg.Attach != "dev" {
t.Errorf("expected Attach=dev, got %q", msg.Attach)
}
}
func TestParseControlMessage_Resize(t *testing.T) {
data := []byte(`{"resize":{"cols":120,"rows":40}}`)
kind, val := ParseControlMessage(data)
if kind != "resize" {
t.Fatalf("expected kind=resize, got %q", kind)
}
msg, ok := val.(*ResizeMsg)
if !ok {
t.Fatalf("expected *ResizeMsg, got %T", val)
}
if msg.Resize.Cols != 120 || msg.Resize.Rows != 40 {
t.Errorf("expected 120x40, got %dx%d", msg.Resize.Cols, msg.Resize.Rows)
}
}
func TestParseControlMessage_Detach(t *testing.T) {
data := []byte(`{"detach":true}`)
kind, val := ParseControlMessage(data)
if kind != "detach" {
t.Fatalf("expected kind=detach, got %q", kind)
}
msg, ok := val.(*DetachMsg)
if !ok {
t.Fatalf("expected *DetachMsg, got %T", val)
}
if !msg.Detach {
t.Errorf("expected Detach=true")
}
}
func TestParseControlMessage_Unknown(t *testing.T) {
data := []byte(`{"foo":"bar"}`)
kind, val := ParseControlMessage(data)
if kind != "unknown" {
t.Errorf("expected kind=unknown, got %q", kind)
}
if val != nil {
t.Errorf("expected nil val for unknown, got %v", val)
}
}
func TestParseControlMessage_Invalid(t *testing.T) {
data := []byte(`not json at all`)
kind, val := ParseControlMessage(data)
if kind != "invalid" {
t.Errorf("expected kind=invalid, got %q", kind)
}
if val != nil {
t.Errorf("expected nil val for invalid, got %v", val)
}
}
// --- Marshal function tests ---
func TestMarshalAttached(t *testing.T) {
data := MarshalAttached("sess1")
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["attached"] != "sess1" {
t.Errorf("expected attached=sess1, got %v", m["attached"])
}
}
func TestMarshalError(t *testing.T) {
data := MarshalError("bad thing")
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["error"] != "bad thing" {
t.Errorf("expected error='bad thing', got %v", m["error"])
}
}
func TestMarshalExited(t *testing.T) {
data := MarshalExited("sess1")
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["exited"] != "sess1" {
t.Errorf("expected exited=sess1, got %v", m["exited"])
}
}