267 lines
7.1 KiB
Go
267 lines
7.1 KiB
Go
// Package cli implements the termd subcommands as functions over the REST
|
|
// client, writing to an io.Writer — so tests can run them in-process against
|
|
// a test server.
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"termd/internal/api"
|
|
"termd/internal/client"
|
|
"termd/internal/keymap"
|
|
)
|
|
|
|
func printJSON(out io.Writer, v any) error {
|
|
enc := json.NewEncoder(out)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(v)
|
|
}
|
|
|
|
// New implements `termd new [flags] [command args...]`.
|
|
func New(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("new", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
cols := fs.Int("cols", 80, "terminal width")
|
|
rows := fs.Int("rows", 24, "terminal height")
|
|
cwd := fs.String("cwd", "", "working directory")
|
|
termName := fs.String("term", "", "TERM value (default xterm-256color)")
|
|
var envs envFlags
|
|
fs.Var(&envs, "env", "extra K=V environment entry (repeatable)")
|
|
asJSON := fs.Bool("json", false, "print the full API response")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
info, err := c.Create(ctx, api.CreateSessionRequest{
|
|
Command: fs.Args(),
|
|
Cwd: *cwd,
|
|
Env: envs.m,
|
|
Cols: *cols,
|
|
Rows: *rows,
|
|
Term: *termName,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return printJSON(out, info)
|
|
}
|
|
_, err = fmt.Fprintln(out, info.ID)
|
|
return err
|
|
}
|
|
|
|
type envFlags struct{ m map[string]string }
|
|
|
|
func (e *envFlags) String() string { return "" }
|
|
func (e *envFlags) Set(v string) error {
|
|
k, val, ok := strings.Cut(v, "=")
|
|
if !ok {
|
|
return fmt.Errorf("want K=V, got %q", v)
|
|
}
|
|
if e.m == nil {
|
|
e.m = make(map[string]string)
|
|
}
|
|
e.m[k] = val
|
|
return nil
|
|
}
|
|
|
|
// Ls implements `termd ls`.
|
|
func Ls(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("ls", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
asJSON := fs.Bool("json", false, "print the full API response")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
list, err := c.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return printJSON(out, list)
|
|
}
|
|
tw := newTabWriter(out)
|
|
fmt.Fprintln(tw, "ID\tPID\tSIZE\tSTATE\tTITLE\tCOMMAND")
|
|
for _, s := range list.Sessions {
|
|
state := "running"
|
|
if s.Exited {
|
|
state = fmt.Sprintf("exited(%d)", deref(s.ExitCode))
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%d\t%dx%d\t%s\t%s\t%s\n",
|
|
s.ID, s.PID, s.Cols, s.Rows, state, s.Title, strings.Join(s.Command, " "))
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func deref(p *int) int {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return *p
|
|
}
|
|
|
|
// Info implements `termd info <id>`.
|
|
func Info(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("info", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 1 {
|
|
return fmt.Errorf("usage: termd info <id>")
|
|
}
|
|
info, err := c.Info(ctx, fs.Arg(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printJSON(out, info)
|
|
}
|
|
|
|
// Kill implements `termd kill <id>`.
|
|
func Kill(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("kill", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
sig := fs.String("signal", "HUP", "signal to send (HUP, TERM, KILL, INT, ...)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 1 {
|
|
return fmt.Errorf("usage: termd kill [--signal SIG] <id>")
|
|
}
|
|
return c.Delete(ctx, fs.Arg(0), *sig)
|
|
}
|
|
|
|
// Send implements `termd send <id> <arg>...` — tmux send-keys style: an arg
|
|
// that parses as a key name is sent as that key, anything else as literal
|
|
// text. -l forces all args literal.
|
|
func Send(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("send", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
literal := fs.Bool("l", false, "treat all arguments as literal text")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() < 2 {
|
|
return fmt.Errorf("usage: termd send [-l] <id> <keys-or-text>...")
|
|
}
|
|
id := fs.Arg(0)
|
|
var items []api.InputItem
|
|
for _, a := range fs.Args()[1:] {
|
|
a := a
|
|
if !*literal {
|
|
if _, err := keymap.Parse(a); err == nil {
|
|
items = append(items, api.InputItem{Key: &a})
|
|
continue
|
|
}
|
|
}
|
|
items = append(items, api.InputItem{Text: &a})
|
|
}
|
|
_, err := c.Input(ctx, id, items)
|
|
return err
|
|
}
|
|
|
|
// Screen implements `termd screen <id>`.
|
|
func Screen(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("screen", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
asJSON := fs.Bool("json", false, "print the full screen state as JSON")
|
|
raw := fs.Bool("raw", false, "print the styled ANSI rendering")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 1 {
|
|
return fmt.Errorf("usage: termd screen [--json|--raw] <id>")
|
|
}
|
|
scr, err := c.Screen(ctx, fs.Arg(0), *raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case *asJSON:
|
|
return printJSON(out, scr)
|
|
case *raw:
|
|
_, err := io.WriteString(out, scr.Raw+"\n")
|
|
return err
|
|
default:
|
|
_, err := io.WriteString(out, strings.Join(scr.Lines, "\n")+"\n")
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Resize implements `termd resize <id> <cols>x<rows>`.
|
|
func Resize(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("resize", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 2 {
|
|
return fmt.Errorf("usage: termd resize <id> <cols>x<rows>")
|
|
}
|
|
colsStr, rowsStr, ok := strings.Cut(fs.Arg(1), "x")
|
|
if !ok {
|
|
return fmt.Errorf("want <cols>x<rows>, got %q", fs.Arg(1))
|
|
}
|
|
cols, err := strconv.Atoi(colsStr)
|
|
if err != nil {
|
|
return fmt.Errorf("bad cols %q: %w", colsStr, err)
|
|
}
|
|
rows, err := strconv.Atoi(rowsStr)
|
|
if err != nil {
|
|
return fmt.Errorf("bad rows %q: %w", rowsStr, err)
|
|
}
|
|
return c.Resize(ctx, fs.Arg(0), cols, rows)
|
|
}
|
|
|
|
// Mouse implements `termd mouse <id> <type> <x>,<y>`.
|
|
func Mouse(ctx context.Context, c *client.Client, args []string, out io.Writer) error {
|
|
fs := flag.NewFlagSet("mouse", flag.ContinueOnError)
|
|
fs.SetOutput(out)
|
|
button := fs.String("button", "", "left|middle|right|wheel-up|wheel-down (default left, or wheel-up for scroll)")
|
|
var mods modFlags
|
|
fs.Var(&mods, "mod", "modifier: ctrl, alt or shift (repeatable)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() != 3 {
|
|
return fmt.Errorf("usage: termd mouse [--button B] [--mod M]... <id> <click|press|release|move|drag|scroll> <x>,<y>")
|
|
}
|
|
id, typ, at := fs.Arg(0), fs.Arg(1), fs.Arg(2)
|
|
xStr, yStr, ok := strings.Cut(at, ",")
|
|
if !ok {
|
|
return fmt.Errorf("want <x>,<y>, got %q", at)
|
|
}
|
|
x, err := strconv.Atoi(xStr)
|
|
if err != nil {
|
|
return fmt.Errorf("bad x %q: %w", xStr, err)
|
|
}
|
|
y, err := strconv.Atoi(yStr)
|
|
if err != nil {
|
|
return fmt.Errorf("bad y %q: %w", yStr, err)
|
|
}
|
|
b := *button
|
|
if b == "" {
|
|
if typ == "scroll" {
|
|
b = "wheel-up"
|
|
} else if typ != "move" {
|
|
b = "left"
|
|
}
|
|
}
|
|
return c.Mouse(ctx, id, api.MouseRequest{
|
|
Type: typ, Button: b, X: x, Y: y, Modifiers: mods.v,
|
|
})
|
|
}
|
|
|
|
type modFlags struct{ v []string }
|
|
|
|
func (m *modFlags) String() string { return strings.Join(m.v, ",") }
|
|
func (m *modFlags) Set(v string) error {
|
|
m.v = append(m.v, v)
|
|
return nil
|
|
}
|