Merge remote-tracking branch 'origin/trunk' into gh-ext-search
This commit is contained in:
commit
036e16608f
11 changed files with 1124 additions and 66 deletions
5
go.mod
5
go.mod
|
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.2
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/gdamore/tcell/v2 v2.5.3
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
|
|
@ -28,6 +29,7 @@ require (
|
|||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
|
|
@ -50,6 +52,7 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
|
|
@ -63,7 +66,7 @@ require (
|
|||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
|
|
|
|||
11
go.sum
11
go.sum
|
|
@ -88,6 +88,10 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
|||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
|
||||
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
|
|
@ -218,9 +222,12 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
|
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
|
@ -370,12 +377,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
|
|||
16
internal/codespaces/grpc/generate.md
Normal file
16
internal/codespaces/grpc/generate.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Protocol Buffers for Codespaces
|
||||
|
||||
Instructions for generating and adding gRPC protocol buffers.
|
||||
|
||||
## Generate Protocol Buffers
|
||||
|
||||
1. [Download `protoc`](https://grpc.io/docs/protoc-installation/)
|
||||
2. [Download protocol compiler plugins for Go](https://grpc.io/docs/languages/go/quickstart/)
|
||||
3. Run `./generate.sh` from the `internal/codespaces/grpc` directory
|
||||
|
||||
## Add New Protocol Buffers
|
||||
|
||||
1. Download a `.proto` contract from the service repo
|
||||
2. Create a new directory and copy the `.proto` to it
|
||||
3. Update `generate.sh` to include the include the new `.proto`
|
||||
4. Follow the instructions to [Generate Protocol Buffers](#generate-protocol-buffers)
|
||||
26
internal/codespaces/grpc/generate.sh
Executable file
26
internal/codespaces/grpc/generate.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if ! protoc --version; then
|
||||
echo 'ERROR: protoc is not on your PATH'
|
||||
exit 1
|
||||
fi
|
||||
if ! protoc-gen-go --version; then
|
||||
echo 'ERROR: protoc-gen-go is not on your PATH'
|
||||
exit 1
|
||||
fi
|
||||
if ! protoc-gen-go-grpc --version; then
|
||||
echo 'ERROR: protoc-gen-go-grpc is not on your PATH'
|
||||
fi
|
||||
|
||||
function generate {
|
||||
local contract="$1"
|
||||
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative "$contract"
|
||||
echo "Generated protocol buffers for $contract"
|
||||
}
|
||||
|
||||
generate jupyter/JupyterServerHostService.v1.proto
|
||||
|
||||
echo 'Done!'
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.21.3
|
||||
// source: JupyterServerHostService.v1.proto
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.12.4
|
||||
// source: jupyter/JupyterServerHostService.v1.proto
|
||||
|
||||
package jupyter
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ type GetRunningServerRequest struct {
|
|||
func (x *GetRunningServerRequest) Reset() {
|
||||
*x = GetRunningServerRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_JupyterServerHostService_v1_proto_msgTypes[0]
|
||||
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ func (x *GetRunningServerRequest) String() string {
|
|||
func (*GetRunningServerRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetRunningServerRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_JupyterServerHostService_v1_proto_msgTypes[0]
|
||||
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -55,7 +55,7 @@ func (x *GetRunningServerRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use GetRunningServerRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetRunningServerRequest) Descriptor() ([]byte, []int) {
|
||||
return file_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{0}
|
||||
return file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type GetRunningServerResponse struct {
|
||||
|
|
@ -72,7 +72,7 @@ type GetRunningServerResponse struct {
|
|||
func (x *GetRunningServerResponse) Reset() {
|
||||
*x = GetRunningServerResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_JupyterServerHostService_v1_proto_msgTypes[1]
|
||||
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ func (x *GetRunningServerResponse) String() string {
|
|||
func (*GetRunningServerResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetRunningServerResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_JupyterServerHostService_v1_proto_msgTypes[1]
|
||||
mi := &file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -98,7 +98,7 @@ func (x *GetRunningServerResponse) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use GetRunningServerResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetRunningServerResponse) Descriptor() ([]byte, []int) {
|
||||
return file_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{1}
|
||||
return file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *GetRunningServerResponse) GetResult() bool {
|
||||
|
|
@ -129,57 +129,57 @@ func (x *GetRunningServerResponse) GetServerUrl() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
var File_JupyterServerHostService_v1_proto protoreflect.FileDescriptor
|
||||
var File_jupyter_JupyterServerHostService_v1_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_JupyterServerHostService_v1_proto_rawDesc = []byte{
|
||||
0x0a, 0x21, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48,
|
||||
0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x12, 0x2b, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e,
|
||||
0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31,
|
||||
0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x18, 0x47,
|
||||
0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c,
|
||||
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72,
|
||||
0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x32, 0xb5, 0x01, 0x0a, 0x11,
|
||||
0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73,
|
||||
0x74, 0x12, 0x9f, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67,
|
||||
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x44, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61,
|
||||
0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72,
|
||||
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x45, 0x2e, 0x43,
|
||||
0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a,
|
||||
0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74,
|
||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75,
|
||||
0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
var file_jupyter_JupyterServerHostService_v1_proto_rawDesc = []byte{
|
||||
0x0a, 0x29, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x2f, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65,
|
||||
0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||
0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2b, 0x43, 0x6f, 0x64,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70,
|
||||
0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65,
|
||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52,
|
||||
0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e,
|
||||
0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x16, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61,
|
||||
0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||
0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55,
|
||||
0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x55, 0x72, 0x6c, 0x32, 0xb5, 0x01, 0x0a, 0x11, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x9f, 0x01, 0x0a, 0x10, 0x47, 0x65,
|
||||
0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x44,
|
||||
0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x47, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x6f,
|
||||
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
|
||||
0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x45, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x73, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x4a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2e,
|
||||
0x2f, 0x6a, 0x75, 0x70, 0x79, 0x74, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_JupyterServerHostService_v1_proto_rawDescOnce sync.Once
|
||||
file_JupyterServerHostService_v1_proto_rawDescData = file_JupyterServerHostService_v1_proto_rawDesc
|
||||
file_jupyter_JupyterServerHostService_v1_proto_rawDescOnce sync.Once
|
||||
file_jupyter_JupyterServerHostService_v1_proto_rawDescData = file_jupyter_JupyterServerHostService_v1_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_JupyterServerHostService_v1_proto_rawDescGZIP() []byte {
|
||||
file_JupyterServerHostService_v1_proto_rawDescOnce.Do(func() {
|
||||
file_JupyterServerHostService_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_JupyterServerHostService_v1_proto_rawDescData)
|
||||
func file_jupyter_JupyterServerHostService_v1_proto_rawDescGZIP() []byte {
|
||||
file_jupyter_JupyterServerHostService_v1_proto_rawDescOnce.Do(func() {
|
||||
file_jupyter_JupyterServerHostService_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_jupyter_JupyterServerHostService_v1_proto_rawDescData)
|
||||
})
|
||||
return file_JupyterServerHostService_v1_proto_rawDescData
|
||||
return file_jupyter_JupyterServerHostService_v1_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_JupyterServerHostService_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_JupyterServerHostService_v1_proto_goTypes = []interface{}{
|
||||
var file_jupyter_JupyterServerHostService_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_jupyter_JupyterServerHostService_v1_proto_goTypes = []interface{}{
|
||||
(*GetRunningServerRequest)(nil), // 0: Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerRequest
|
||||
(*GetRunningServerResponse)(nil), // 1: Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerResponse
|
||||
}
|
||||
var file_JupyterServerHostService_v1_proto_depIdxs = []int32{
|
||||
var file_jupyter_JupyterServerHostService_v1_proto_depIdxs = []int32{
|
||||
0, // 0: Codespaces.Grpc.JupyterServerHostService.v1.JupyterServerHost.GetRunningServer:input_type -> Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerRequest
|
||||
1, // 1: Codespaces.Grpc.JupyterServerHostService.v1.JupyterServerHost.GetRunningServer:output_type -> Codespaces.Grpc.JupyterServerHostService.v1.GetRunningServerResponse
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
|
|
@ -189,13 +189,13 @@ var file_JupyterServerHostService_v1_proto_depIdxs = []int32{
|
|||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_JupyterServerHostService_v1_proto_init() }
|
||||
func file_JupyterServerHostService_v1_proto_init() {
|
||||
if File_JupyterServerHostService_v1_proto != nil {
|
||||
func init() { file_jupyter_JupyterServerHostService_v1_proto_init() }
|
||||
func file_jupyter_JupyterServerHostService_v1_proto_init() {
|
||||
if File_jupyter_JupyterServerHostService_v1_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_JupyterServerHostService_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_jupyter_JupyterServerHostService_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetRunningServerRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
|
|
@ -207,7 +207,7 @@ func file_JupyterServerHostService_v1_proto_init() {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
file_JupyterServerHostService_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_jupyter_JupyterServerHostService_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetRunningServerResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
|
|
@ -224,18 +224,18 @@ func file_JupyterServerHostService_v1_proto_init() {
|
|||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_JupyterServerHostService_v1_proto_rawDesc,
|
||||
RawDescriptor: file_jupyter_JupyterServerHostService_v1_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_JupyterServerHostService_v1_proto_goTypes,
|
||||
DependencyIndexes: file_JupyterServerHostService_v1_proto_depIdxs,
|
||||
MessageInfos: file_JupyterServerHostService_v1_proto_msgTypes,
|
||||
GoTypes: file_jupyter_JupyterServerHostService_v1_proto_goTypes,
|
||||
DependencyIndexes: file_jupyter_JupyterServerHostService_v1_proto_depIdxs,
|
||||
MessageInfos: file_jupyter_JupyterServerHostService_v1_proto_msgTypes,
|
||||
}.Build()
|
||||
File_JupyterServerHostService_v1_proto = out.File
|
||||
file_JupyterServerHostService_v1_proto_rawDesc = nil
|
||||
file_JupyterServerHostService_v1_proto_goTypes = nil
|
||||
file_JupyterServerHostService_v1_proto_depIdxs = nil
|
||||
File_jupyter_JupyterServerHostService_v1_proto = out.File
|
||||
file_jupyter_JupyterServerHostService_v1_proto_rawDesc = nil
|
||||
file_jupyter_JupyterServerHostService_v1_proto_goTypes = nil
|
||||
file_jupyter_JupyterServerHostService_v1_proto_depIdxs = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.3
|
||||
// source: JupyterServerHostService.v1.proto
|
||||
// - protoc v3.12.4
|
||||
// source: jupyter/JupyterServerHostService.v1.proto
|
||||
|
||||
package jupyter
|
||||
|
||||
|
|
@ -101,5 +101,5 @@ var JupyterServerHost_ServiceDesc = grpc.ServiceDesc{
|
|||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "JupyterServerHostService.v1.proto",
|
||||
Metadata: "jupyter/JupyterServerHostService.v1.proto",
|
||||
}
|
||||
|
|
|
|||
527
pkg/cmd/extension/browse/browse.go
Normal file
527
pkg/cmd/extension/browse/browse.go
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
package browse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const pagingOffset = 24
|
||||
|
||||
type ExtBrowseOpts struct {
|
||||
Cmd *cobra.Command
|
||||
Browser ibrowser
|
||||
IO *iostreams.IOStreams
|
||||
Searcher search.Searcher
|
||||
Em extensions.ExtensionManager
|
||||
Client *http.Client
|
||||
Logger *log.Logger
|
||||
Cfg config.Config
|
||||
Rg *readmeGetter
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type ibrowser interface {
|
||||
Browse(string) error
|
||||
}
|
||||
|
||||
type uiRegistry struct {
|
||||
// references to some of the heavily cross-referenced tview primitives. Not
|
||||
// everything is in here because most things are just used once in one place
|
||||
// and don't need to be easy to look up like this.
|
||||
App *tview.Application
|
||||
Outerflex *tview.Flex
|
||||
List *tview.List
|
||||
Readme *tview.TextView
|
||||
}
|
||||
|
||||
type extEntry struct {
|
||||
URL string
|
||||
Name string
|
||||
FullName string
|
||||
Installed bool
|
||||
Official bool
|
||||
description string
|
||||
}
|
||||
|
||||
func (e extEntry) Title() string {
|
||||
var installed string
|
||||
var official string
|
||||
|
||||
if e.Installed {
|
||||
installed = " [green](installed)"
|
||||
}
|
||||
|
||||
if e.Official {
|
||||
official = " [yellow](official)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", e.FullName, official, installed)
|
||||
}
|
||||
|
||||
func (e extEntry) Description() string {
|
||||
if e.description == "" {
|
||||
return "no description provided"
|
||||
}
|
||||
return e.description
|
||||
}
|
||||
|
||||
type extList struct {
|
||||
ui uiRegistry
|
||||
extEntries []extEntry
|
||||
app *tview.Application
|
||||
filter string
|
||||
opts ExtBrowseOpts
|
||||
}
|
||||
|
||||
func newExtList(opts ExtBrowseOpts, ui uiRegistry, extEntries []extEntry) *extList {
|
||||
ui.List.SetTitleColor(tcell.ColorWhite)
|
||||
ui.List.SetSelectedTextColor(tcell.ColorBlack)
|
||||
ui.List.SetSelectedBackgroundColor(tcell.ColorWhite)
|
||||
ui.List.SetWrapAround(false)
|
||||
ui.List.SetBorderPadding(1, 1, 1, 1)
|
||||
|
||||
el := &extList{
|
||||
ui: ui,
|
||||
extEntries: extEntries,
|
||||
app: ui.App,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
el.Reset()
|
||||
return el
|
||||
}
|
||||
|
||||
func (el *extList) createModal() *tview.Modal {
|
||||
m := tview.NewModal()
|
||||
m.SetBackgroundColor(tcell.ColorPurple)
|
||||
m.SetDoneFunc(func(_ int, _ string) {
|
||||
el.ui.App.SetRoot(el.ui.Outerflex, true)
|
||||
el.Refresh()
|
||||
})
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (el *extList) InstallSelected() {
|
||||
ee, ix := el.FindSelected()
|
||||
if ix < 0 {
|
||||
el.opts.Logger.Println("failed to find selected entry")
|
||||
return
|
||||
}
|
||||
repo, err := ghrepo.FromFullName(ee.FullName)
|
||||
if err != nil {
|
||||
el.opts.Logger.Println(fmt.Errorf("failed to install '%s't: %w", ee.FullName, err))
|
||||
return
|
||||
}
|
||||
|
||||
modal := el.createModal()
|
||||
|
||||
modal.SetText(fmt.Sprintf("Installing %s...", ee.FullName))
|
||||
el.ui.App.SetRoot(modal, true)
|
||||
// I could eliminate this with a goroutine but it seems to be working fine
|
||||
el.app.ForceDraw()
|
||||
err = el.opts.Em.Install(repo, "")
|
||||
if err != nil {
|
||||
modal.SetText(fmt.Sprintf("Failed to install %s: %s", ee.FullName, err.Error()))
|
||||
} else {
|
||||
modal.SetText(fmt.Sprintf("Installed %s!", ee.FullName))
|
||||
modal.AddButtons([]string{"ok"})
|
||||
el.ui.App.SetFocus(modal)
|
||||
}
|
||||
|
||||
el.toggleInstalled(ix)
|
||||
}
|
||||
|
||||
func (el *extList) RemoveSelected() {
|
||||
ee, ix := el.FindSelected()
|
||||
if ix < 0 {
|
||||
el.opts.Logger.Println("failed to find selected extension")
|
||||
return
|
||||
}
|
||||
|
||||
modal := el.createModal()
|
||||
|
||||
modal.SetText(fmt.Sprintf("Removing %s...", ee.FullName))
|
||||
el.ui.App.SetRoot(modal, true)
|
||||
// I could eliminate this with a goroutine but it seems to be working fine
|
||||
el.ui.App.ForceDraw()
|
||||
|
||||
err := el.opts.Em.Remove(strings.TrimPrefix(ee.Name, "gh-"))
|
||||
if err != nil {
|
||||
modal.SetText(fmt.Sprintf("Failed to remove %s: %s", ee.FullName, err.Error()))
|
||||
} else {
|
||||
modal.SetText(fmt.Sprintf("Removed %s.", ee.FullName))
|
||||
modal.AddButtons([]string{"ok"})
|
||||
el.ui.App.SetFocus(modal)
|
||||
}
|
||||
el.toggleInstalled(ix)
|
||||
}
|
||||
|
||||
func (el *extList) toggleInstalled(ix int) {
|
||||
ee := el.extEntries[ix]
|
||||
ee.Installed = !ee.Installed
|
||||
el.extEntries[ix] = ee
|
||||
}
|
||||
|
||||
func (el *extList) Focus() {
|
||||
el.app.SetFocus(el.ui.List)
|
||||
}
|
||||
|
||||
func (el *extList) Refresh() {
|
||||
el.Reset()
|
||||
el.Filter(el.filter)
|
||||
}
|
||||
|
||||
func (el *extList) Reset() {
|
||||
el.ui.List.Clear()
|
||||
for _, ee := range el.extEntries {
|
||||
el.ui.List.AddItem(ee.Title(), ee.Description(), rune(0), func() {})
|
||||
}
|
||||
}
|
||||
|
||||
func (el *extList) PageDown() {
|
||||
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + pagingOffset)
|
||||
}
|
||||
|
||||
func (el *extList) PageUp() {
|
||||
i := el.ui.List.GetCurrentItem() - pagingOffset
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
el.ui.List.SetCurrentItem(i)
|
||||
}
|
||||
|
||||
func (el *extList) ScrollDown() {
|
||||
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + 1)
|
||||
}
|
||||
|
||||
func (el *extList) ScrollUp() {
|
||||
i := el.ui.List.GetCurrentItem() - 1
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
el.ui.List.SetCurrentItem(i)
|
||||
}
|
||||
|
||||
func (el *extList) FindSelected() (extEntry, int) {
|
||||
if el.ui.List.GetItemCount() == 0 {
|
||||
return extEntry{}, -1
|
||||
}
|
||||
title, desc := el.ui.List.GetItemText(el.ui.List.GetCurrentItem())
|
||||
for x, e := range el.extEntries {
|
||||
if e.Title() == title && e.Description() == desc {
|
||||
return e, x
|
||||
}
|
||||
}
|
||||
return extEntry{}, -1
|
||||
}
|
||||
|
||||
func (el *extList) Filter(text string) {
|
||||
el.filter = text
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
el.ui.List.Clear()
|
||||
for _, ee := range el.extEntries {
|
||||
if strings.Contains(ee.Title()+ee.Description(), text) {
|
||||
el.ui.List.AddItem(ee.Title(), ee.Description(), rune(0), func() {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSelectedReadme(opts ExtBrowseOpts, readme *tview.TextView, el *extList) (string, error) {
|
||||
ee, ix := el.FindSelected()
|
||||
if ix < 0 {
|
||||
return "", errors.New("failed to find selected entry")
|
||||
}
|
||||
fullName := ee.FullName
|
||||
rm, err := opts.Rg.Get(fullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, _, wrap, _ := readme.GetInnerRect()
|
||||
|
||||
// using glamour directly because if I don't horrible things happen
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithStylePath("dark"),
|
||||
glamour.WithWordWrap(wrap))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rendered, err := renderer.Render(rm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func getExtensions(opts ExtBrowseOpts) ([]extEntry, error) {
|
||||
extEntries := []extEntry{}
|
||||
|
||||
installed := opts.Em.List()
|
||||
|
||||
result, err := opts.Searcher.Repositories(search.Query{
|
||||
Kind: search.KindRepositories,
|
||||
Limit: 1000,
|
||||
Qualifiers: search.Qualifiers{
|
||||
Topic: []string{"gh-extension"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return extEntries, fmt.Errorf("failed to search for extensions: %w", err)
|
||||
}
|
||||
|
||||
host, _ := opts.Cfg.DefaultHost()
|
||||
|
||||
for _, repo := range result.Items {
|
||||
if !strings.HasPrefix(repo.Name, "gh-") {
|
||||
continue
|
||||
}
|
||||
ee := extEntry{
|
||||
URL: "https://" + host + "/" + repo.FullName,
|
||||
FullName: repo.FullName,
|
||||
Name: repo.Name,
|
||||
description: repo.Description,
|
||||
}
|
||||
for _, v := range installed {
|
||||
// TODO consider a Repo() on Extension interface
|
||||
var installedRepo string
|
||||
if u, err := git.ParseURL(v.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
installedRepo = ghrepo.FullName(r)
|
||||
}
|
||||
}
|
||||
if repo.FullName == installedRepo {
|
||||
ee.Installed = true
|
||||
}
|
||||
}
|
||||
if repo.Owner.Login == "cli" || repo.Owner.Login == "github" {
|
||||
ee.Official = true
|
||||
}
|
||||
|
||||
extEntries = append(extEntries, ee)
|
||||
}
|
||||
|
||||
return extEntries, nil
|
||||
}
|
||||
|
||||
func ExtBrowse(opts ExtBrowseOpts) error {
|
||||
if opts.Debug {
|
||||
f, err := os.CreateTemp("", "extBrowse-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
opts.Logger = log.New(f, "", log.Lshortfile)
|
||||
} else {
|
||||
opts.Logger = log.New(io.Discard, "", 0)
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
extEntries, err := getExtensions(opts)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.Rg = newReadmeGetter(opts.Client, time.Hour*24)
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
outerFlex := tview.NewFlex()
|
||||
innerFlex := tview.NewFlex()
|
||||
|
||||
header := tview.NewTextView().SetText(fmt.Sprintf("browsing %d gh extensions", len(extEntries)))
|
||||
header.SetTextAlign(tview.AlignCenter).SetTextColor(tcell.ColorWhite)
|
||||
|
||||
filter := tview.NewInputField().SetLabel("filter: ")
|
||||
filter.SetFieldBackgroundColor(tcell.ColorGray)
|
||||
filter.SetBorderPadding(0, 0, 20, 20)
|
||||
|
||||
list := tview.NewList()
|
||||
|
||||
readme := tview.NewTextView()
|
||||
readme.SetBorderPadding(1, 1, 0, 1)
|
||||
readme.SetBorder(true).SetBorderColor(tcell.ColorPurple)
|
||||
|
||||
help := tview.NewTextView()
|
||||
help.SetText(
|
||||
"/: filter i/r: install/remove w: open in browser pgup/pgdn: scroll readme q: quit")
|
||||
help.SetTextAlign(tview.AlignCenter)
|
||||
|
||||
ui := uiRegistry{
|
||||
App: app,
|
||||
Outerflex: outerFlex,
|
||||
List: list,
|
||||
}
|
||||
|
||||
extList := newExtList(opts, ui, extEntries)
|
||||
|
||||
loadSelectedReadme := func() {
|
||||
rendered, err := getSelectedReadme(opts, readme, extList)
|
||||
if err != nil {
|
||||
opts.Logger.Println(err.Error())
|
||||
readme.SetText("unable to fetch readme :(")
|
||||
return
|
||||
}
|
||||
|
||||
app.QueueUpdateDraw(func() {
|
||||
readme.SetText("")
|
||||
readme.SetDynamicColors(true)
|
||||
|
||||
w := tview.ANSIWriter(readme)
|
||||
_, _ = w.Write([]byte(rendered))
|
||||
|
||||
readme.ScrollToBeginning()
|
||||
})
|
||||
}
|
||||
|
||||
filter.SetChangedFunc(func(text string) {
|
||||
extList.Filter(text)
|
||||
go loadSelectedReadme()
|
||||
})
|
||||
|
||||
filter.SetDoneFunc(func(key tcell.Key) {
|
||||
switch key {
|
||||
case tcell.KeyEnter:
|
||||
extList.Focus()
|
||||
case tcell.KeyEscape:
|
||||
filter.SetText("")
|
||||
extList.Reset()
|
||||
extList.Focus()
|
||||
}
|
||||
})
|
||||
|
||||
innerFlex.SetDirection(tview.FlexColumn)
|
||||
innerFlex.AddItem(list, 0, 1, true)
|
||||
innerFlex.AddItem(readme, 0, 1, false)
|
||||
|
||||
outerFlex.SetDirection(tview.FlexRow)
|
||||
outerFlex.AddItem(header, 1, -1, false)
|
||||
outerFlex.AddItem(filter, 1, -1, false)
|
||||
outerFlex.AddItem(innerFlex, 0, 1, true)
|
||||
outerFlex.AddItem(help, 1, -1, false)
|
||||
|
||||
app.SetRoot(outerFlex, true)
|
||||
|
||||
// Force fetching of initial readme by loading it just prior to the first
|
||||
// draw. The callback is removed immediately after draw.
|
||||
app.SetBeforeDrawFunc(func(_ tcell.Screen) bool {
|
||||
go loadSelectedReadme()
|
||||
return false // returning true would halt drawing which we do not want
|
||||
})
|
||||
|
||||
app.SetAfterDrawFunc(func(_ tcell.Screen) {
|
||||
app.SetBeforeDrawFunc(nil)
|
||||
app.SetAfterDrawFunc(nil)
|
||||
})
|
||||
|
||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if filter.HasFocus() {
|
||||
return event
|
||||
}
|
||||
|
||||
switch event.Rune() {
|
||||
case 'q':
|
||||
app.Stop()
|
||||
case 'k':
|
||||
extList.ScrollUp()
|
||||
readme.SetText("...fetching readme...")
|
||||
go loadSelectedReadme()
|
||||
case 'j':
|
||||
extList.ScrollDown()
|
||||
readme.SetText("...fetching readme...")
|
||||
go loadSelectedReadme()
|
||||
case 'w':
|
||||
ee, ix := extList.FindSelected()
|
||||
if ix < 0 {
|
||||
opts.Logger.Println("failed to find selected entry")
|
||||
return nil
|
||||
}
|
||||
err = opts.Browser.Browse(ee.URL)
|
||||
if err != nil {
|
||||
opts.Logger.Println(fmt.Errorf("could not open browser for '%s': %w", ee.URL, err))
|
||||
}
|
||||
case 'i':
|
||||
extList.InstallSelected()
|
||||
case 'r':
|
||||
extList.RemoveSelected()
|
||||
case ' ':
|
||||
// The shift check works on windows and not linux/mac:
|
||||
if event.Modifiers()&tcell.ModShift != 0 {
|
||||
extList.PageUp()
|
||||
} else {
|
||||
extList.PageDown()
|
||||
}
|
||||
go loadSelectedReadme()
|
||||
case '/':
|
||||
app.SetFocus(filter)
|
||||
return nil
|
||||
}
|
||||
switch event.Key() {
|
||||
case tcell.KeyUp:
|
||||
extList.ScrollUp()
|
||||
go loadSelectedReadme()
|
||||
return nil
|
||||
case tcell.KeyDown:
|
||||
extList.ScrollDown()
|
||||
go loadSelectedReadme()
|
||||
return nil
|
||||
case tcell.KeyEscape:
|
||||
filter.SetText("")
|
||||
extList.Reset()
|
||||
case tcell.KeyCtrlSpace:
|
||||
// The ctrl check works on windows/mac and not windows:
|
||||
extList.PageUp()
|
||||
go loadSelectedReadme()
|
||||
case tcell.KeyCtrlJ:
|
||||
extList.PageDown()
|
||||
go loadSelectedReadme()
|
||||
case tcell.KeyCtrlK:
|
||||
extList.PageUp()
|
||||
go loadSelectedReadme()
|
||||
case tcell.KeyPgUp:
|
||||
row, col := readme.GetScrollOffset()
|
||||
if row > 0 {
|
||||
readme.ScrollTo(row-2, col)
|
||||
}
|
||||
return nil
|
||||
case tcell.KeyPgDn:
|
||||
row, col := readme.GetScrollOffset()
|
||||
readme.ScrollTo(row+2, col)
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
|
||||
// Without this redirection, the git client inside of the extension manager
|
||||
// will dump git output to the terminal.
|
||||
opts.IO.ErrOut = io.Discard
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
366
pkg/cmd/extension/browse/browse_test.go
Normal file
366
pkg/cmd/extension/browse/browse_test.go
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
package browse
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/view"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getSelectedReadme(t *testing.T) {
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
content := base64.StdEncoding.EncodeToString([]byte("lol"))
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/cli/gh-cool/readme"),
|
||||
httpmock.JSONResponse(view.RepoReadme{Content: content}))
|
||||
|
||||
client := &http.Client{Transport: ®}
|
||||
|
||||
rg := newReadmeGetter(client, time.Second)
|
||||
opts := ExtBrowseOpts{
|
||||
Rg: rg,
|
||||
}
|
||||
readme := tview.NewTextView()
|
||||
ui := uiRegistry{
|
||||
List: tview.NewList(),
|
||||
}
|
||||
extEntries := []extEntry{
|
||||
{
|
||||
Name: "gh-cool",
|
||||
FullName: "cli/gh-cool",
|
||||
Installed: false,
|
||||
Official: true,
|
||||
description: "it's just cool ok",
|
||||
},
|
||||
{
|
||||
Name: "gh-screensaver",
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Installed: true,
|
||||
Official: false,
|
||||
description: "animations in your terminal",
|
||||
},
|
||||
}
|
||||
el := newExtList(opts, ui, extEntries)
|
||||
|
||||
content, err := getSelectedReadme(opts, readme, el)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, content, "lol")
|
||||
}
|
||||
|
||||
func Test_getExtensionRepos(t *testing.T) {
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
client := &http.Client{Transport: ®}
|
||||
|
||||
values := url.Values{
|
||||
"page": []string{"1"},
|
||||
"per_page": []string{"100"},
|
||||
"q": []string{"topic:gh-extension"},
|
||||
}
|
||||
cfg := config.NewBlankConfig()
|
||||
|
||||
cfg.DefaultHostFunc = func() (string, string) { return "github.com", "" }
|
||||
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(search.RepositoriesResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Repository{
|
||||
{
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
Description: "terminal animations",
|
||||
Owner: search.User{
|
||||
Login: "vilmibm",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
Description: "it's just cool ok",
|
||||
Owner: search.User{
|
||||
Login: "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
Description: "helps with triage",
|
||||
Owner: search.User{
|
||||
Login: "samcoe",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
Description: "something something enterprise",
|
||||
Owner: search.User{
|
||||
Login: "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 4,
|
||||
}),
|
||||
)
|
||||
|
||||
searcher := search.NewSearcher(client, "github.com")
|
||||
emMock := &extensions.ExtensionManagerMock{}
|
||||
emMock.ListFunc = func() []extensions.Extension {
|
||||
return []extensions.Extension{
|
||||
&extensions.ExtensionMock{
|
||||
URLFunc: func() string {
|
||||
return "https://github.com/vilmibm/gh-screensaver"
|
||||
},
|
||||
},
|
||||
&extensions.ExtensionMock{
|
||||
URLFunc: func() string {
|
||||
return "https://github.com/github/gh-gei"
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
opts := ExtBrowseOpts{
|
||||
Searcher: searcher,
|
||||
Em: emMock,
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
extEntries, err := getExtensions(opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedEntries := []extEntry{
|
||||
{
|
||||
URL: "https://github.com/vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Installed: true,
|
||||
Official: false,
|
||||
description: "terminal animations",
|
||||
},
|
||||
{
|
||||
URL: "https://github.com/cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
FullName: "cli/gh-cool",
|
||||
Installed: false,
|
||||
Official: true,
|
||||
description: "it's just cool ok",
|
||||
},
|
||||
{
|
||||
URL: "https://github.com/samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
FullName: "samcoe/gh-triage",
|
||||
Installed: false,
|
||||
Official: false,
|
||||
description: "helps with triage",
|
||||
},
|
||||
{
|
||||
URL: "https://github.com/github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
FullName: "github/gh-gei",
|
||||
Installed: true,
|
||||
Official: true,
|
||||
description: "something something enterprise",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedEntries, extEntries)
|
||||
}
|
||||
|
||||
func Test_extEntry(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ee extEntry
|
||||
expectedTitle string
|
||||
expectedDesc string
|
||||
}{
|
||||
{
|
||||
name: "official",
|
||||
ee: extEntry{
|
||||
Name: "gh-cool",
|
||||
FullName: "cli/gh-cool",
|
||||
Installed: false,
|
||||
Official: true,
|
||||
description: "it's just cool ok",
|
||||
},
|
||||
expectedTitle: "cli/gh-cool [yellow](official)",
|
||||
expectedDesc: "it's just cool ok",
|
||||
},
|
||||
{
|
||||
name: "no description",
|
||||
ee: extEntry{
|
||||
Name: "gh-nodesc",
|
||||
FullName: "barryburton/gh-nodesc",
|
||||
Installed: false,
|
||||
Official: false,
|
||||
description: "",
|
||||
},
|
||||
expectedTitle: "barryburton/gh-nodesc",
|
||||
expectedDesc: "no description provided",
|
||||
},
|
||||
{
|
||||
name: "installed",
|
||||
ee: extEntry{
|
||||
Name: "gh-screensaver",
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Installed: true,
|
||||
Official: false,
|
||||
description: "animations in your terminal",
|
||||
},
|
||||
expectedTitle: "vilmibm/gh-screensaver [green](installed)",
|
||||
expectedDesc: "animations in your terminal",
|
||||
},
|
||||
{
|
||||
name: "neither",
|
||||
ee: extEntry{
|
||||
Name: "gh-triage",
|
||||
FullName: "samcoe/gh-triage",
|
||||
Installed: false,
|
||||
Official: false,
|
||||
description: "help with triage",
|
||||
},
|
||||
expectedTitle: "samcoe/gh-triage",
|
||||
expectedDesc: "help with triage",
|
||||
},
|
||||
{
|
||||
name: "both",
|
||||
ee: extEntry{
|
||||
Name: "gh-gei",
|
||||
FullName: "github/gh-gei",
|
||||
Installed: true,
|
||||
Official: true,
|
||||
description: "something something enterprise",
|
||||
},
|
||||
expectedTitle: "github/gh-gei [yellow](official) [green](installed)",
|
||||
expectedDesc: "something something enterprise",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expectedTitle, tt.ee.Title())
|
||||
assert.Equal(t, tt.expectedDesc, tt.ee.Description())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extList(t *testing.T) {
|
||||
opts := ExtBrowseOpts{
|
||||
Logger: log.New(io.Discard, "", 0),
|
||||
Em: &extensions.ExtensionManagerMock{
|
||||
InstallFunc: func(repo ghrepo.Interface, _ string) error {
|
||||
assert.Equal(t, "cli/gh-cool", ghrepo.FullName(repo))
|
||||
return nil
|
||||
},
|
||||
RemoveFunc: func(name string) error {
|
||||
assert.Equal(t, "cool", name)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
app := tview.NewApplication()
|
||||
list := tview.NewList()
|
||||
ui := uiRegistry{
|
||||
List: list,
|
||||
App: app,
|
||||
}
|
||||
extEntries := []extEntry{
|
||||
{
|
||||
Name: "gh-cool",
|
||||
FullName: "cli/gh-cool",
|
||||
Installed: false,
|
||||
Official: true,
|
||||
description: "it's just cool ok",
|
||||
},
|
||||
{
|
||||
Name: "gh-screensaver",
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Installed: true,
|
||||
Official: false,
|
||||
description: "animations in your terminal",
|
||||
},
|
||||
{
|
||||
Name: "gh-triage",
|
||||
FullName: "samcoe/gh-triage",
|
||||
Installed: false,
|
||||
Official: false,
|
||||
description: "help with triage",
|
||||
},
|
||||
{
|
||||
Name: "gh-gei",
|
||||
FullName: "github/gh-gei",
|
||||
Installed: true,
|
||||
Official: true,
|
||||
description: "something something enterprise",
|
||||
},
|
||||
}
|
||||
|
||||
extList := newExtList(opts, ui, extEntries)
|
||||
|
||||
extList.Filter("cool")
|
||||
assert.Equal(t, 1, extList.ui.List.GetItemCount())
|
||||
|
||||
title, _ := extList.ui.List.GetItemText(0)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official)", title)
|
||||
|
||||
extList.InstallSelected()
|
||||
assert.True(t, extList.extEntries[0].Installed)
|
||||
|
||||
extList.Refresh()
|
||||
assert.Equal(t, 1, extList.ui.List.GetItemCount())
|
||||
|
||||
title, _ = extList.ui.List.GetItemText(0)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official) [green](installed)", title)
|
||||
|
||||
extList.RemoveSelected()
|
||||
assert.False(t, extList.extEntries[0].Installed)
|
||||
|
||||
extList.Refresh()
|
||||
assert.Equal(t, 1, extList.ui.List.GetItemCount())
|
||||
|
||||
title, _ = extList.ui.List.GetItemText(0)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official)", title)
|
||||
|
||||
extList.Reset()
|
||||
assert.Equal(t, 4, extList.ui.List.GetItemCount())
|
||||
|
||||
ee, ix := extList.FindSelected()
|
||||
assert.Equal(t, 0, ix)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
|
||||
|
||||
extList.ScrollDown()
|
||||
ee, ix = extList.FindSelected()
|
||||
assert.Equal(t, 1, ix)
|
||||
assert.Equal(t, "vilmibm/gh-screensaver [green](installed)", ee.Title())
|
||||
|
||||
extList.ScrollUp()
|
||||
ee, ix = extList.FindSelected()
|
||||
assert.Equal(t, 0, ix)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
|
||||
|
||||
extList.PageDown()
|
||||
ee, ix = extList.FindSelected()
|
||||
assert.Equal(t, 3, ix)
|
||||
assert.Equal(t, "github/gh-gei [yellow](official) [green](installed)", ee.Title())
|
||||
|
||||
extList.PageUp()
|
||||
ee, ix = extList.FindSelected()
|
||||
assert.Equal(t, 0, ix)
|
||||
assert.Equal(t, "cli/gh-cool [yellow](official)", ee.Title())
|
||||
}
|
||||
33
pkg/cmd/extension/browse/rg.go
Normal file
33
pkg/cmd/extension/browse/rg.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package browse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/view"
|
||||
)
|
||||
|
||||
type readmeGetter struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newReadmeGetter(client *http.Client, cacheTTL time.Duration) *readmeGetter {
|
||||
cachingClient := api.NewCachedHTTPClient(client, cacheTTL)
|
||||
return &readmeGetter{
|
||||
client: cachingClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *readmeGetter) Get(repoFullName string) (string, error) {
|
||||
repo, err := ghrepo.FromFullName(repoFullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
readme, err := view.RepositoryReadme(g.client, repo, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return readme.Content, nil
|
||||
}
|
||||
|
|
@ -5,12 +5,15 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/extension/browse"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
|
|
@ -239,6 +242,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
|
||||
t := utils.NewTablePrinter(io)
|
||||
for _, c := range cmds {
|
||||
// TODO consider a Repo() on Extension interface
|
||||
var repo string
|
||||
if u, err := git.ParseURL(c.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
|
|
@ -406,6 +410,74 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
func() *cobra.Command {
|
||||
var debug bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "browse",
|
||||
Short: "Enter a UI for browsing, adding, and removing extensions",
|
||||
Long: heredoc.Doc(`
|
||||
This command will take over your terminal and run a fully interactive
|
||||
interface for browsing, adding, and removing gh extensions.
|
||||
|
||||
The extension list is navigated with the arrow keys or with j/k.
|
||||
Space and control+space (or control + j/k) page the list up and down.
|
||||
Extension readmes can be scrolled with page up/page down keys
|
||||
(fn + arrow up/down on a mac keyboard).
|
||||
|
||||
For highlighted extensions, you can press:
|
||||
|
||||
- w to open the extension in your web browser
|
||||
- i to install the extension
|
||||
- r to remove the extension
|
||||
|
||||
Press / to focus the filter input. Press enter to scroll the results.
|
||||
Press Escape to clear the filter and return to the full list.
|
||||
|
||||
Press q to quit.
|
||||
|
||||
The output of this command may be difficult to navigate for screen reader
|
||||
users, users operating at high zoom and other users of assistive technology. It
|
||||
is also not advised for automation scripts. We advise those users to use the
|
||||
alternative command:
|
||||
|
||||
gh ext search
|
||||
|
||||
along with gh ext install, gh ext remove, and gh repo view.
|
||||
`),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !io.CanPrompt() {
|
||||
return errors.New("this command runs an interactive UI and needs to be run in a terminal")
|
||||
}
|
||||
cfg, err := config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host, _ := cfg.DefaultHost()
|
||||
client, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
searcher := search.NewSearcher(api.NewCachedHTTPClient(client, time.Hour*24), host)
|
||||
|
||||
opts := browse.ExtBrowseOpts{
|
||||
Cmd: cmd,
|
||||
IO: io,
|
||||
Browser: browser,
|
||||
Searcher: searcher,
|
||||
Em: m,
|
||||
Client: client,
|
||||
Cfg: cfg,
|
||||
Debug: debug,
|
||||
}
|
||||
|
||||
return browse.ExtBrowse(opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&debug, "debug", false, "log to /tmp/extBrowse-*")
|
||||
return cmd
|
||||
}(),
|
||||
&cobra.Command{
|
||||
Use: "exec <name> [args]",
|
||||
Short: "Execute an installed extension",
|
||||
|
|
|
|||
|
|
@ -756,6 +756,12 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
},
|
||||
wantStdout: "test output",
|
||||
},
|
||||
{
|
||||
name: "browse",
|
||||
args: []string{"browse"},
|
||||
wantErr: true,
|
||||
errMsg: "this command runs an interactive UI and needs to be run in a terminal",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue