Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
marwan-at-work committed Aug 10, 2020
0 parents commit 37e2f47
Show file tree
Hide file tree
Showing 10 changed files with 20,907 additions and 0 deletions.
18,933 changes: 18,933 additions & 0 deletions api/api.pb.go

Large diffs are not rendered by default.

1,543 changes: 1,543 additions & 0 deletions api/api.proto

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions api/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package api

//go:generate protoc --go2_out=paths=source_relative:. api.proto
80 changes: 80 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package iterm2

import (
"fmt"
"io"

"marwan.io/iterm2/api"
"marwan.io/iterm2/client"
)

// App represents an open iTerm2 application
type App interface {
io.Closer

CreateWindow() (Window, error)
ListWindows() ([]Window, error)
}

// NewApp establishes a connection
// with iTerm2 and returns an App
func NewApp() (App, error) {
c, err := client.New()
if err != nil {
return nil, err
}

return &app{c: c}, nil
}

type app struct {
c *client.Client
}

func (a *app) CreateWindow() (Window, error) {
resp, err := a.c.Call(&api.ClientOriginatedMessage{
Submessage: &api.ClientOriginatedMessage_CreateTabRequest{
CreateTabRequest: &api.CreateTabRequest{},
},
})
if err != nil {
return nil, fmt.Errorf("could not create window tab: %w", err)
}
ctr := resp.GetCreateTabResponse()
if ctr.GetStatus() != api.CreateTabResponse_OK {
return nil, fmt.Errorf("unexpected window tab status: %s", ctr.GetStatus())
}
return &window{
c: a.c,
id: ctr.GetWindowId(),
session: ctr.GetSessionId(),
}, nil
}

func (a *app) ListWindows() ([]Window, error) {
list := []Window{}
resp, err := a.c.Call(&api.ClientOriginatedMessage{
Submessage: &api.ClientOriginatedMessage_ListSessionsRequest{
ListSessionsRequest: &api.ListSessionsRequest{},
},
})
if err != nil {
return nil, fmt.Errorf("could not list sessions: %w", err)
}
fmt.Println(resp.GetListSessionsResponse().GetBuriedSessions())
for _, w := range resp.GetListSessionsResponse().GetWindows() {
list = append(list, &window{
c: a.c,
id: w.GetWindowId(),
})
}
return list, nil
}

func (a *app) Close() error {
return a.c.Close()
}

func str(s string) *string {
return &s
}
126 changes: 126 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package client

import (
"context"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"os"
"sync"

"github.com/gorilla/websocket"
"google.golang.org/protobuf/proto"
"marwan.io/iterm2/api"
)

// New returns a new websocket connection
// that talks to the iTerm2 application.New
// Callers must call the Close() method when done.
// The cookie parameter is optional. If provided,
// it will bypass script authentication prompts.
func New() (*Client, error) {
h := http.Header{}
h.Set("origin", "ws://localhost/")
h.Set("x-iterm2-library-version", "go 3.6")
h.Set("x-iterm2-disable-auth-ui", "true")
if cookie := os.Getenv("ITERM2_COOKIE"); cookie != "" {
h.Set("x-iterm2-cookie", cookie)
}
c, resp, err := websocket.DefaultDialer.Dial("ws://localhost:1912", h)
if err != nil && resp != nil {
b, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("error connecting to iTerm2: %v - body: %s", err, b)
}
if err != nil {
return nil, fmt.Errorf("error connecting to iTerm2: %v", err)
}
cl := &Client{
c: c,
rpcs: make(map[int64]chan<- *api.ServerOriginatedMessage),
}
ctx, cancel := context.WithCancel(context.Background())
cl.cancel = cancel
go cl.worker(ctx)
return cl, nil
}

// Client wraps a websocket client connection to iTerm2.
// Must be instantiated with NewClient.
type Client struct {
c *websocket.Conn
rpcs map[int64]chan<- *api.ServerOriginatedMessage
mu sync.Mutex
cancel context.CancelFunc
}

func (c *Client) worker(ctx context.Context) {
for {
_, msg, err := c.c.ReadMessage()
if ctx.Err() != nil {
return
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
var resp api.ServerOriginatedMessage
err = proto.Unmarshal(msg, &resp)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
c.mu.Lock()
ch, ok := c.rpcs[resp.GetId()]
c.mu.Unlock()
if !ok {
fmt.Fprintf(os.Stderr, "could not find call for %d: %v\n", resp.GetId(), &resp)
continue
}
delete(c.rpcs, resp.GetId())
ch <- &resp
}
}

// Call sends a request to the iTerm2 server
func (c *Client) Call(req *api.ClientOriginatedMessage) (*api.ServerOriginatedMessage, error) {
req.Id = id(rand.Int63())
ch := make(chan *api.ServerOriginatedMessage, 1)
c.mu.Lock()
c.rpcs[req.GetId()] = ch
c.mu.Unlock()
msg, err := proto.Marshal(req)
if err != nil {
return nil, err
}
err = c.c.WriteMessage(websocket.BinaryMessage, msg)
if err != nil {
return nil, fmt.Errorf("error writing to websocket: %w", err)
}
resp := <-ch
if resp.GetError() != "" {
return nil, fmt.Errorf("error from server: %v", resp.GetError())
}
return resp, nil
}

// Close closes the websocket connection
// and frees any goroutine resources
func (c *Client) Close() error {
c.cancel()
return c.c.Close()
}

func id(i int64) *int64 {
return &i
}

func str(s string) *string {
return &s
}

func must(err error) {
if err != nil {
panic(err)
}
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module marwan.io/iterm2

go 1.15

require (
github.com/golang/protobuf v1.4.2
github.com/gorilla/websocket v1.4.2
google.golang.org/protobuf v1.23.0
)
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
37 changes: 37 additions & 0 deletions session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package iterm2

import (
"fmt"

"marwan.io/iterm2/api"
"marwan.io/iterm2/client"
)

// Session represents an iTerm2 Session which is a pane
// within a Tab where the terminal is active
type Session interface {
SendText(s string) error
}

type session struct {
c *client.Client
id string
}

func (s *session) SendText(t string) error {
resp, err := s.c.Call(&api.ClientOriginatedMessage{
Submessage: &api.ClientOriginatedMessage_SendTextRequest{
SendTextRequest: &api.SendTextRequest{
Session: &s.id,
Text: &t,
},
},
})
if err != nil {
return fmt.Errorf("error sending text to session %q: %w", s.id, err)
}
if status := resp.GetSendTextResponse().GetStatus(); status != api.SendTextResponse_OK {
return fmt.Errorf("unexpected status for session %q: %s", s.id, status)
}
return nil
}
69 changes: 69 additions & 0 deletions tab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package iterm2

import (
"fmt"

"marwan.io/iterm2/api"
"marwan.io/iterm2/client"
)

// Tab abstracts an iTerm2 window tab
type Tab interface {
SetTitle(string) error
ListSessions() ([]Session, error)
}

type tab struct {
c *client.Client
id string
windowID string
}

func (t *tab) SetTitle(s string) error {
_, err := t.c.Call(&api.ClientOriginatedMessage{
Submessage: &api.ClientOriginatedMessage_InvokeFunctionRequest{
InvokeFunctionRequest: &api.InvokeFunctionRequest{
Invocation: str(fmt.Sprintf(`iterm2.set_title(title: "%s")`, s)),
Context: &api.InvokeFunctionRequest_Method_{
Method: &api.InvokeFunctionRequest_Method{
Receiver: &t.id,
},
},
},
},
})
if err != nil {
return fmt.Errorf("could not call set_title: %w", err)
}
return nil
}

func (t *tab) ListSessions() ([]Session, error) {
list := []Session{}
resp, err := t.c.Call(&api.ClientOriginatedMessage{
Submessage: &api.ClientOriginatedMessage_ListSessionsRequest{
ListSessionsRequest: &api.ListSessionsRequest{},
},
})
if err != nil {
return nil, fmt.Errorf("error listing sessions for tab %q: %w", t.id, err)
}
lsr := resp.GetListSessionsResponse()
for _, window := range lsr.GetWindows() {
if window.GetWindowId() != t.windowID {
continue
}
for _, wt := range window.GetTabs() {
if wt.GetTabId() != t.id {
continue
}
for _, link := range wt.GetRoot().GetLinks() {
list = append(list, &session{
c: t.c,
id: link.GetSession().GetUniqueIdentifier(),
})
}
}
}
return list, nil
}
Loading

0 comments on commit 37e2f47

Please sign in to comment.