// 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 `. 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 ") } info, err := c.Info(ctx, fs.Arg(0)) if err != nil { return err } return printJSON(out, info) } // Kill implements `termd kill `. 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] ") } return c.Delete(ctx, fs.Arg(0), *sig) } // Send implements `termd send ...` — 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 := 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 `. 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] ") } 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 x`. 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 x") } colsStr, rowsStr, ok := strings.Cut(fs.Arg(1), "x") if !ok { return fmt.Errorf("want x, 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 ,`. 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, typ, at := fs.Arg(0), fs.Arg(1), fs.Arg(2) xStr, yStr, ok := strings.Cut(at, ",") if !ok { return fmt.Errorf("want ,, 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 }