-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 37e2f47
Showing
10 changed files
with
20,907 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.